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 | exports.handler = function(event, context) { |
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
- How to create a Request object for your Lambda event from API Gateway: this article shows how to use the Mapping Templates on the request side to change what your Lambda receives as input.
- Stack Overflow: Is there a way to change the http status codes returned by Amazon API Gateway?
- Stack Overflow: How to return error collection/object from AWS Lambda function and map to AWS API Gateway response code
- Image "Confused" by CollegeDegrees360 used under Creative Commons
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.