Contents

Returning HTTP Errors from API Gateway in AWS Lambda Functions using Python and Serverless Framework

There are many examples available for how to return an error from an AWS Lambda function through API Gateway to a client in Node.js, but relatively few for how to do so using the Python runtime. Here we will try to give some basic info using Python with a POST action.

Prereqs

This assumes you already have an AWS account, have configured your AWS access credentials or profile and set the AWS_REGION environment variable to your region of choice. Otherwise it will use the us-west-2 region by default.

You will also need the Serverless Framework to be installed.

Lambda-Proxy Integration

In this blog post we are going to cover the Lambda-Proxy integration method versus the Lambda method. This is the simplest and from what I’ve found least documented because it just seems obvious to AWS technical writers, Serverless framework developers and blog authors. Most examples are written in Node.js because of that because that’s what both AWS and Serverless framework documents discuss. Our example hopes to make it a little more clear.

Create Serverless project

Let’s start by creating a new Serverless framework project. We will be using the aws-python3 template for this, which populates a basic serverless.yml configuration file and a basic handler function. To do so we use the serverless create command (abbreviated as sls create for brevity) to generate the skeleton to our project.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[user@linux serverless]$ sls create --template aws-python3 --path test-lambda-exception-handling
Serverless: Generating boilerplate...
Serverless: Generating boilerplate in "/serverless/test-lambda-exception-handling"
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.38.0
 -------'

Serverless: Successfully generated boilerplate for template: "aws-python3"

Once we have this template in place, you can modify the files yourself, or copy the below into your project. It is also available in my GitHub repository if you’d rather look at that.

The handler function and YAML are:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import json

def throw_error(event, context):
    data = json.loads(event['body'])
    print(data) # this can be seen in CloudWatch Logs

    response = {
        "statusCode": 400,
        "headers": {
            "Content-Type": 'application/json',
            "context.aws_request_id": context.aws_request_id # for fun show the Lambda requestID
        },
        "body": "Bad Request",
        "isBase64Encoded": False
    }

    return response
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
service: test-lambda-exception-handling

provider:
  name: aws
  runtime: python3.7
  region: ${AWS_REGION, 'us-east-2'}

  stage: dev

functions:
  throw_error:
    handler: handler.throw_error
    events:
      - http:
          path: throw
          method: post
          cors: true

What this does

The serverless.yml file defines the FaaS or Cloud provider where you are going to run the function, as well as the function itself. This is a very basic single function for the purposes of this demo. The documentation is here. As you can see we are running in AWS Lambda, and using the function named throw_error.

The handler can be named anything, but must be referenced in the .yml file. We’re using the name that the Serverless template provided for us. In a Lambda runtime environment, you’re passed an event object and a context object. You are provided event details including any data passed to the function via the POST request. The context object has information about the execution environment, invocation and other things, described here.

Our handler simply loads the event data, and then prints it so that it can be viewed in Cloudwatch Logs. This isn’t returned to the calling client. Because we’re using API Gateway, the response must conform to a specific format.

In our function we simply return a 400 Bad Request response immediately after decoding the data passed in.

Deploying the project

If we’re cautios we can look at what will be deployed by first running the sls package command. Then we can look in the .zip file that it will upload to AWS Lambda.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[user@linux test-lambda-exception-handling]$ sls package
Serverless: Packaging service...
Serverless: Excluding development dependencies...
[user@linux test-lambda-exception-handling]$ ls -a
.  ..  .gitignore  handler.py  .serverless  serverless.yml
[user@linux test-lambda-exception-handling]$ ls .serverless/
cloudformation-template-create-stack.json  cloudformation-template-update-stack.json  serverless-state.json  test-lambda-exception-handling.zip
[user@linux test-lambda-exception-handling]$ zipinfo -l .serverless/test-lambda-exception-handling.zip 
Archive:  .serverless/test-lambda-exception-handling.zip
Zip file size: 407 bytes, number of entries: 1
-rw-rw-r--  4.5 unx      451 bl      273 defN 80-Jan-01 00:00 handler.py
1 file, 451 bytes uncompressed, 273 bytes compressed:  39.5%

To deploy we use the sls deploy command.

 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
27
28
29
[user@linux test-lambda-exception-handling]$ sls deploy
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
.....
Serverless: Stack create finished...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service test-lambda-exception-handling.zip file to S3 (407 B)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
.................................
Serverless: Stack update finished...
Service Information
service: test-lambda-exception-handling
stage: dev
region: us-east-2
stack: test-lambda-exception-handling-dev
resources: 11
api keys:
  None
endpoints:
  POST - https://7vt4w7lb4l.execute-api.us-east-2.amazonaws.com/dev/throw
functions:
  throw_error: test-lambda-exception-handling-dev-throw_error
layers:
  None

This returns the URL for POST actions, which we will use in our next step.

Invoking the Function

Now we can simply run a POST against our new function URL and see how it will respond.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
[user@linux test-lambda-exception-handling]$ curl -i -X POST https://7vt4w7lb4l.execute-api.us-east-2.amazonaws.com/dev/throw --data '{"blah": "blah1"}'
HTTP/2 400 
content-type: application/json
content-length: 11
date: Wed, 22 May 2019 01:34:50 GMT
x-amzn-requestid: ca8164e7-7c31-11e9-b114-d91ebd2032b3
context.aws_request_id: 87f10bcd-7c85-46d5-bcd7-493aca45e1f0
x-amz-apigw-id: aD8RIG3MCYcFk2w=
x-amzn-trace-id: Root=1-5ce4a73a-af05ec85fe34062d036b396f;Sampled=0
x-cache: Error from cloudfront
via: 1.1 c4fb40b7909e4dd897bba2e297b284e7.cloudfront.net (CloudFront)
x-amz-cf-id: Sovagss83OsF-hv8b7QHcf9GkmV2r1A_XEF8KzI18GMrhWzGvfaGSQ==

Bad Request[user@linux test-lambda-exception-handling]$ 

This shows that we receive the Bad Request response, with HTTP status code 400 as expected.

In a future post I’ll describe how to return errors in Python with the Lambda integration method.