Alternate Resources
This article by Kenn Brodhagen is the best article I have found on this topic:
How to return a custom error object and status code from API Gateway with Lambda
Here are some others:
AWS Docs: Handle Lambda errors in API Gateway
AWS Blog: Error Handling Patterns in Amazon API Gateway and AWS Lambda
dev.classmethod.jp: How to map lambda error to API Gateway error response with Serverless Framework
Intro
One of the commonly annoying things about a serverless workflow with API Gateway and Lambda is that if you trigger a Lambda function with API Gateway and the function fails, you will still get a status code of 200 on the request.
Because as far as API Gateway is concerned, it did it’s job, it got your request through the Gateway and called the function. You will only get non 200 responses from API Gateway if your authentication fails, a mapping error, or some other error that happens within API Gateway.
This is not very useful for catching errors in your client however. So to fix this we just have to do a little bit of extra mapping magic in API Gateway and make sure we are throwing our errors properly in Lambda.
Throwing the Error
The reason we are throwing an error in Lambda is because this generates a JSON document that looks like this:
{
"errorType": "ReferenceError",
"errorMessage": "x is not defined",
"trace": [
"ReferenceError: x is not defined",
" at Runtime.exports.handler (/var/task/index.js:2:3)",
" at Runtime.handleOnce (/var/runtime/Runtime.js:63:25)",
" at process._tickCallback (internal/process/next_tick.js:68:7)"
]
}
Specifically note the key “errorMessage”. Later in this article we are going to tell API Gateway to look for this key in the response, and if it finds it to throw a 400 response. Throwing an error is how we generate this error object with the “errorMessage” key.
Here is an example where we set the status of the response to 400. Will this give us a 400 error code in the browser?
exports.handler = async event => {
let response = {
status: 200,
body: undefined,
headers: {
"Content-Type": "application/json",
},
}
try {
// purposely throw error
console.log(variableThatDoesntExist)
response.body = "This will be skipped"
} catch (error) {
response.status = 400
response.body = "We referenced a non-existent variable (on purpose)"
}
// dynamic response
return response
}
If we run this function the status code of the API Gateway response is still 200, because we have to tell API Gateway the specific conditions we want for it to throw a 400 code (IE “errorMessage” key is in Lambda response).
Let’s throw an error.
exports.handler = async (event, context) => {
let response = {
status: 200,
body: undefined,
headers: {
"Content-Type": "application/json",
},
}
try {
// purposely throw error
console.log(variableThatDoesntExist)
response.body = "This will be skipped"
} catch (error) {
response.status = 400
response.body = "We referenced a non-existent variable (on purpose)"
context.fail(JSON.stringify(response))
}
// dynamic response
return response
}
or you could use
exports.handler = async event => {
let response = {
status: 200,
body: undefined,
headers: {
"Content-Type": "application/json",
},
}
try {
// purposely throw error
console.log(variableThatDoesntExist)
response.body = "This will be skipped"
} catch (error) {
response.status = 400
response.body = error
throw new Error(JSON.stringify(response))
}
// dynamic response
return response
}
etc etc… there are many ways to throw an error.
Throwing an error generates the “errorMessage” object key in the Lambda response, which we then configure API Gateway to look for below. It is possible to throw a 400 response without this by telling API gateway to look for another key in the response, but for the sake of having a reliable and reproducible method documented, we are throwing an error.
Create Method Response
Moving over to API Gateway now, start by picking whichever method you are working with and navigate to Method Response > Add Response
Add a 400 status (or whatever) and then head over to the integration response.
Mapping the Error
Lastly we need to do two things. We need to create a very simple mapping template, and add a Regex string that will trigger the 400 code we just created.
Navigate to Integration Response > Add Integration Response
The regex that is used here is a special java format called Pattern.
The Regex string is looking for matches in the output and if it finds them it will throw the corresponding status code that it matched. We will use the following.
Lambda Error Regex: .*"status":400.*
and then we select the 400 response status that we created in the previous step.
Lastly we need to create a very short mapping template that will tell API Gateway where we are looking for this regex match.
$input.path('$.errorMessage')
We are looking inside the errorMessage
object, which is the object that is created when you throw the error in Lambda.
Response Mapping Recap
I just want to be very clear about what we are doing here. We are instructing API Gateway to look inside the “errorMessage” key if it exists in the response, and then if it see’s status:400 inside the “errorMessage” it will throw a 400 code response. Status 400 is inside the errorMessage because we manually set it that way inside the catch block of our Lambda function. If your Lambda failing returns something different, you can configure API Gateway to look for that instead. We are showing one pattern that works here, but you can change this to suit your needs. If your Lambda does not throw an error, and you are not returning a status:400 key inside that error, these specific response mapping settings won’t work for you.
CORS Headers
There is actually one last step that we need to handle here. If you are experiencing CORS errors in the client it is probably because you are missing the “Access-Control-Allow-Origin” header which is typically appended by API Gateway to a 200 response. One of the things that happens when we take manual control of the 400 responses is that we have manually control the headers. The default 4xx headers that are configured in API Gateway will no longer apply to this route. Therefore we need to manually set the Access-Control-Allow-Origin header in the response to prevent an error in the browser.
Add a header in the method response.
And then define the value in the integration response.
Conclusion
And there you have it, if we run the function again and get an error we will get the following
This was a very simple implementation. You can create more complicated regex that can capture a wider range of items from the error message object and there are examples of that elsewhere. But hopefully now the basic idea is clear.