iris-error-handler v1.2.0
Iris Error Handler
Error handling middleware for IrisVR microservices.
Compatible with Node.js + Express apps, with an optional MongoDB backend.
Getting started
Installation
$ npm install iris-error-handler --save
Initialization
const errorHandler = require('iris-error-handler');
Error Table
You may conjure the list of internal Iris error codes and their respective messages like this:
const errorTable = require('iris-error-handler').errorTable;
The error code acts as the key, while the value contains the meta payload to be returned to the client. The meta object contains the following fields:
code
: An integer value that categorizes the type of response according to our internal status code library. This number is unrelated to a standard HTTP status code.error_type
: A brief description of the error.error_message
: A string that describes error in full detail.
Organization
- -1: Unknown error. This is the default error returned by the middleware if no error code is specified.
- 1 - 99: Third-party errors
- 100 - 149: Request header errors
- 150 - 199: MongoDB errors
- 200 - 299: User / Password errors
- 300 - 399: Library errors
- 400 - 499: Team errors
- 500 - 599: Billing errors
- 600 - 699: Notification errors
Example
{
...
202: {
meta: {
code: 202,
error_type: 'UsernameTaken',
error_message: 'The username is already registered'
}
},
...
}
API
const errorUtils = require('iris-error-handler').utils;
General
handleError(res)(err)
Currying function that accepts an Express response object and an error argument. This method internally triggers sendError
for ultimately sending the response back to the client.
The error passed in must be one of two types:
- A Javascript Error in the form of
Error(X)
, whereX
denotes an Iris error code.
Example
/**
* route.js
*/
// Arbitrary error thrower
function calculate() {
return Promise.reject(Error(101));
};
// Route handler
const calculateNumber = (req, res) =>
calculate(req.body)
.then(n => res.send(n))
.catch(errorUtils.handleError(res));
- A Mongoose Error triggered by validators defined in the schema. There are two types of mongoose errors that we support:
Required Field Error
The required field validator comes out of the box when you define your schema. The actual error code you want to throw for specific missing fields can be configured here.
Example
/**
* userModel.js
*/
const User = new mongoose.Schema({
username: {
type: String
},
password: {
type: String,
required: true // built-in validator
}
});
module.exports = mongoose.model('User', User);
/**
* route.js
*/
const User = mongoose.model('User');
const createUser = (req, res) => {
const userWithoutPassword = { username: 'cookies@gmail.gov' };
User.create(userWithoutPassword)
.then(u => res.send(u))
.catch(errorUtils.handleError(res)); // Sends `205: PasswordMissing`
}
Custom Validation Error
You can set up custom validation on your schema using .validate(callback, [message])
. If you do so, make sure to pass in the appropriate error code as the second argument.
Custom Validation Example
/**
* userModel.js
*/
const User = new mongoose.Schema({
username: String
});
// Custom validator. Note that the second argument is an Iris error code.
User.path('username').validate(callback, '201');
function callback(value, respond) {
return validator.isEmail(value)
? respond(true) : respond(false);
}
module.exports = mongoose.model('User', User);
/**
* route.js
*/
const User = mongoose.model('User');
const createUser = (req, res) =>
User.create({ username: 'INVALID_EMAIL_FORMAT' })
.then(u => res.send(u))
.catch(errorUtils.handleError(res)); // Sends `201: UsernameInvalid`
sendError(res, code)
Accepts an Express response object and an Iris error code. The code is referenced against the error table and the appropriate response payload is sent to the client. If no code is passed, the method will default to -1: UnknownError
.
The client should always expect a successful HTTP 200
status for errored responses, as more detailed information about the error will be provided in the response body. Refer to the error table for a breakdown of the payload.
This method should not be called directly as it is invoked by the wrapper method handleError
. However, it may be convenient to use in development.
Example
const responder = (req, res) =>
errorUtils.sendError(res, 101);
MongoDB
validateObjectID(id)
Confirms whether a string is a valid Mongo ObjectID; if so, the string is passed on to the next middleware.
This method should be placed directly before making database queries that involve document ID(s).
Example
const User = mongoose.model('User');
const getUser = (req, res) => {
const id = req.user._id;
errorUtils.validateObjectID(id)
.then((id) => User.findById(id))
.then(u => res.send(u))
.catch(errorUtils.handleError(res)); // Sends `150: ObjectIDInvalid`
}
handleEntityNotFound(entity, category)
Checks whether the relevant document exists in the database; if so, the document is passed on to the next middleware.
This method should be placed directly after a database query.
An optional category argument can specify what type of document was(n't) found. If none is provided, the error will default to 160: NotFound
. A dictionary of supported category strings are available here.
Example
const User = mongoose.model('User');
const getUser = (req, res) =>
User.findById(req.user._id)
.then(u => errorUtils.handleEntityNotFound(u, 'userId'))
.then(u => res.send(u))
.catch(errorUtils.handleError(res)); // Sends `204: UserIDNotFound`
validateOwner(user)(document)
Confirms whether a user has access to a document; if so, the document is passed on to the next middleware.
This is a lesser used method that only applies to documents with an owner.username
field, such as Panos in the Library service.
Example
const Document = mongoose.model('Document');
const updateDocument = (req, res) => {
const user = req.user;
const documentId = req.body._id;
Document.findbyId(documentId)
.then(errorUtils.validateOwner(user))
.then(d => res.send(d))
.catch(errorUtils.handleError(res)); // Sends `350: PermissionsError`
}
Testing
Unit and integration tests are contained in /specs
. Make sure to install all dev dependencies required for the testing environment.
$ git clone https://github.com/IrisVR/iris-error-handler.git
$ cd iris-error-handler
$ npm install
Even though this module itself does not require Express or Mongo, it provides support for services using that tech stack. As a result, a server and DB must be mocked in testing.
Linting
$ npm run lint
Running tests
Run unit tests once
$ npm test
Run tests on file change
$ npm run test:watch
Code coverage
$ npm run coverage
All of the above
$ npm run validate
This script uses npm-run-all --parallel
under the hood to execute the processes simultaneously.
Contribution
If you'd like to contribute, please make a pull request to the develop
branch for imminent review.
Making Changes
Error Codes
Prior to adding a new error code, ensure that a synonymous one doesn't already exist in the error table. If it's indeed a new one, either find a relevant category for it (e.g. User errors fall under 200-299) or create a new category and assign a new group of 100 integers.
If applicable, you may want to update the requiredField and/or notFound dictionary.
Methods
New methods should be placed in /utils
; those specific to individual microservices should be placed in /utils/services
. All new and/or updated methods should have corresponding unit and integration tests.
Committing and Releasing
To release updates, you must be part of the IrisVR NPM Organization.
Preview
To preview the contents of the npm module prior to publishing:
$ npm pack
This will create the tar zip file that would be served by npm. You can unzip it and explore its contents, which should consist of the following:
dist/
LICENSE
README.md
package.json
When the module is require'd, package.json
will point the user to the relevant entry point at dist/index.js
.
Commit
The module uses commitizen for making commits, which is included in the dev dependencies.
$ git add -A
$ npm run commit
This will prompt a CLI to walk you through the changes you made. Only a commit type of feat
or fix
will trigger an update to the published npm module; other types such as refactor
and style
will not be a release as they don't change anything from the user's perspective.
Committing will run a githook that triggers npm run validate
, which in turn runs npm test
, npm run lint
and npm run coverage
in parallel. If there is an error at any stage, the commit will be rejected.
Publish
Once a PR is merged into develop
, CircleCI will ensure that the codebase is properly tested, linted and covered.
In the case where develop
is merged into master
, CirclCI will additionally create a release build, make a new tag according to the nature of the update (major, minor or patch), and auto-release the new version to npm.