As discussed on gitter, the plan is to implement the error handling as a two-stage process.
- Add an
additionalCodes
property to theerror
object passed in as the third argument to the api handler. This will specify the additional error responses that should be setup by API Gateway, and the associated regexes. - Create a new
claudia-api-errors
module, which will allow developers to throw a predefined set ofError
objects, which will correspond to response codes.
Only 1. needs to be implemented to allow multiple error responses, 2. is more of a nice-to-have.
API Gateway allows you to parse the error message returned from a lambda function, and apply a response code according to a regex that you configure. Here's a good primer on how this works.
For example, if you return the following error from a lambda function:
new Error('Bad Request: username is required.');
In API Gateway, you can setup the following configuration:
Selection pattern: ^Bad Request: .*
Method response: 400
Now, whenever you return an Error from your lambda function as above, API Gateway will transform this into a 400 response.
Currently, claudia only supports returning a single error code, but we can use the idea above to create multiple error responses.
The plan is to allow API handlers to be setup in the following way:
const ApiBuilder = require('claudia-api-builder');
const api = new ApiBuilder();
api.post('users', function(req) {
if(!req.body.username) {
throw new Error('Bad Request: username is required.');
}
// Otherwise, we can process the request successfully.
}, {
success: 201,
error: {
defaultCode: 500, // optional, defaults to `500`
additionalCodes: [{
code: 400,
pattern: '^Bad Request:.*',
template: '$input.path(\'$.errorMessage\')' // Optional, defaults to the value shown here
}]
}
});
The idea is to enumerate all of the error responses that you expect in your handler as items in the additionalCodes
array. Each item includes:
- the response code to issue
- the pattern to match in the message passed into the error
- the (optional) mapping template to apply to the error message
When setting up your routes, claudia will enumerate this array and apply each in turn as API gateway calls. An example of this can be seen here.
Claudia will also need to be modified so that the success
response code is setup as the default
code in API Gateway. Currently the error code is set up as the default
. The error code should be setup as the regex .+
which will act as a 'catch-all' for all errors which do not match any of the others.
The above, while functionally complete, can be a bit of a pain to manage, especially if you want to return a JSON object in the response body. The second part of the process is building a set of API Errors which simplify this process.
The idea is to modify the above API handler code so it resembles:
const ApiBuilder = require('claudia-api-builder');
const ApiErrors = require('claudia-api-errors');
const api = new ApiBuilder();
api.post('users', function(req) {
if(!req.body.username) {
throw new ApiErrors.BadRequestError({ message: 'username is required' });
}
// Otherwise, we can process the request successfully.
}, {
success: 201,
error: {
defaultCode: 500, // optional, defaults to `500`
additionalErrors: [
ApiErrors.BadRequestError
]
}
});
An ApiError will inherit from a base class:
class ApiBuilderError extends Error {
constructor(code, data) {
const serializedData = JSON.stringify(data);
super(`{"code":${code},"data":${serializedData}}`);
}
toConfig() {
return {
code: this.code,
pattern: `^{"code":${this.code}.*`,
template: '$util.parseJson($input.path(\'$.errorMessage\')).data'
};
}
}
class BadRequestError extends ApiBuilderError {
constructor(data) {
super(400, data);
}
}
Under the hood, if claudia finds an additionalErrors
property on the error
object, it will assume that you are passing in a class inherited from ApiError. It will instantiate this and call toConfig
to generate the config needed to apply to API Gateway. You'll notice that toConfig
returns the same properties as the Regex-based approach, with a couple of extras:
- Notice that the error
message
property is set to a JSON string, which begins with the code and follows with the data passed in. - The
pattern
is always set to begin with the code, this allows us to automatically detect the errors that we have create with the error builder. The^
ensures that we only match on messages which begin with the code, preventing false positives on error messages includingcode
. - The template automatically unwraps the data that you passed in to the error, and returns it as the response body.
So, for the API handler above, if the username is not present, we'll receive the following response:
< 400
{
message: 'username is required'
}
Hence, the error builder is a convenience for returning standard errors.
Template:
When you are using:
for your error template and this reject in your code:
you get
{message=User does not exist}
and this is no validJSON
!Then I tried this:
results in:
๐ juhu, but is not generic
add some generic-ness ๐ง
Test:
results in:
๐ ๐ช ๐ฏ