Share

How to return a custom error object and status code from API Gateway with Lambda



Why does it have to be so hard?

API Gateway and Lambda are great until you need to return an error object from your REST API. It's a maze of regular expressions, string conversions, and mapping templates. Don't worry! With the right boilerplate you can easily return custom error objects and status codes. This article explains how to do it.

Forging a sane response for your REST API

You want an HTTP response with a sensible status code. You want a JSON body that contains an object describing one or more errors processing the request. For example, if the request to add a user had problems with the first name and password you might return a 400 Bad Request and a list of the problems with the input. Here's what it might look like in Postman:

Template code to get you started

To get started right away create a Lambda and API Gateway and paste in what I show here. Go ahead and play around with it some first. I'll dissect each part later in the article.

The Lambda function

Create a Lambda function in AWS with the following code. It calls context.fail() on a hard-coded response object. The response object contains the status code we want to return and an array of error objects. Each error object has several properties: code, source, message, and detail.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
exports.handler = function(event, context) {
var response = {
status: 400,
errors: [
{
code: "123",
source: "/data/attributes/first-name",
message: "Value is too short",
detail: "First name must contain at least three characters."
},
{
code: "225",
source: "/data/attributes/password",
message: "Passwords must contain a letter, number, and punctuation character.",
detail: "The password provided is missing a punctuation character."
},
{
code: "226",
source: "/data/attributes/password",
message: "Password and password confirmation do not match."
}
]
}

context.fail(JSON.stringify(response));
};

After you Save and Test your Lambda you should see the following result. Since we used context.fail() to end the Lambda we expect to see Execution result: failed.

API Gateway Configuration

Create a new API Gateway and add a GET method to the root resource. Bind the method to the Lambda you just created and give it the Lambda basic execution role.

Method Response
Navigate to the Method Response for GET and add a 400 Status response. This makes 400 available to assign a regex to in Integration Response.

Integration Response
Next navigate to Integration Response and add the following Regex and Mapping Template:

Field Value
Lambda Error Regex .*"status":400.*
Mapping Template $input.path('$.errorMessage')

Now deploy your API and click the URL to test. You should see a JSON representation of the error object you created in Lambda and an HTTP Response Status of 400:

Flow of an error response

The value you pass to context.fail() in your Lambda code goes through several stages before it is eventually rendered in the HTTP Response of API Gateway. Between Lambda and API Gateway there are 4 main stages: code, context, regex, and mapping template.

Each stage takes the response value, performs an operation on it, and moves on to the next stage. Every stage except Regex transforms the response value.

Stage Operation Response Value
Code response = {status: 400, errors[]} {status: 400, errors:[]}
context.fail(JSON.stringify(response)) "{\"status\":400,\"errors\":[]}"
Context {errorMessage: resposne.toString()} {errorMessage: "{\"status\":400,\"errors\":[]}"}
Regex /.*"status":400.*/.test(response) {errorMessage: "{\"status\":400,\"errors\":[]}"}
Mapping Template $input.path('$.errorMessage') {"status":400,"errors":[]}

There's a lot going on here! Let's examine each stage in detail.

Code

This is the stage where you create your response object, call JSON.stringify() on it, then pass it to context.fail(). In this stage you transform your response object into a plain string encoded as JSON. You do this because context.fail() really only wants to work with strings.

Context

In this stage the Lambda context object handles the value you pass into context.fail(). It essentially calls toString() on whatever gets passed into it. This is why you had to call JSON.stringify() before you passed in your object. If you don't stringify your response it becomes "[object Object]". Not very helpful. Context then takes this string result and assigns it to the errorMessage property of the object it returns to API Gateway. So API Gateway gets something that looks like {errorMessage: "<your stringified response"}.

Regex

Now API Gateway steps in and tries to figure out what to do with the response. Since you called context.fail() API Gateway will go down the list of Regular Expressions you defined and mapped to HTTP Status codes. It takes each regex and applies it to the value of the errorMessage property that Lambda returned. Here is the regex you specified:

.*"status":400.*

The first thing to notice is that you use .* at the beginning and end of the regex. This allows any other text to appear before or after the field value you're trying to match. If you leave the wildcards out then API Gateway would look for a string that exactly matches "status":400. Using the wildcards lets you change the contents of your response in the Lambda code without having to go back to your API Gateway configuration.

This brings us to the next part of the regex: "status":400. This matches against the property name status with the value 400. Since it's JSON encoding status is quoted. You will need one regex entry for each of the possible HTTP Status codes your api might return. This is a pain to do manually but fortunately you can script your API Gateway definitions to make this easier. If you ever want to change the name of the status property in your Lambda code don't forget to update your API Gateway configuration.

One final note about the Regex phase. Regex matching only occurs if your Lambda function calls context.fail(). If you call context.succeed() then API Gateway always picks the default response code and mapping template.

Mapping Template

You've made it to the final phase! Congratulations! Regex was a doozy but Mapping Template is pretty straight-forward. Here is your mapping template:

$input.path('$.errorMessage')

It's short but very magical looking. $input.path() is a special function available in the mapping template. This function parses the result returned from Lambda and extracts a portion of it. The portion it extracts is specified by value passed in. So when you pass in '$.errorMessage' it looks for a property in the root of the document called errorMessage and extracts its value. This value is the JSON stringified response object you passed to context.fail(). Exactly what you want! The value of the HTTP Response body is a JSON encoded version of your response object. All that hard work has paid off!

Tell me what you are returning!

You should now have a better idea of what happens to that value after you pass it to context.fail(). The API Gateway regexes give you a lot of options but they can be tedious to configure. The Mapping Template gives you even more power!

Does your JSON look anything like the example I used? Or are you doing something totally different? Please comment below, tweet @kennbrodhagen, or email me kennbrodhagen@gmail.com and let me know what kind of responses you are returning.

References

Interested in more?

Sign up for my mailing list. You'll get each new article and other announcements delivered to your inbox. I promise not to spam you. Unsubscribe any time you like.

Read the next article in your inbox!

* indicates required