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.
@phips28 you're making great progress! Awesome ๐
I think you are on the right track with the ApiError file. One thing that you need to account for is that we can't support dynamic templates in this way. Due to the way that API Gateway works, we need to define all of the mapping templates up front (i.e. outside of the handler), so that when the error is passed in to the config object, we can simply call
toConfig
and all of the information we need to setup the response (code and mapping template) is available.So, in order to support a custom mapping, a user would need to create their own error class, which inherits from the error code that they are going to use.
In addition to this, I'm not sure we need to export the
badRequestError
etc. helper handlers, instead I think we can just export the error classes directly.With this in mind perhaps the following works better:
Handler:
I think we'll need to just specify an error object for every status code, I don't think there is any other way of doing it.
Regarding the configuration, I thought that by passing in the class into the
additionalErrors
array, when claudia comes to configure API gateway, it would instantiate all of the classes passed in to grab their config (by automatically callingnew CustomBadRequestError().toConfig()
, saving the user needing to instantiate it).