This post is written by Rudolf Potucek, Startup Solutions Architect.

Amazon API Gateway REST APIs have supported binary data since 2016. API Gateway HTTP APIs makes it easier to work with both binary and text media types. It supports a new payload format version and infers encoding based on request and response formats. In this post, I show how to use HTTP APIs and AWS Lambda to build an API that accepts and returns either text or images.

API Gateway’s base64-encoding unifies the handling of text and binary data in Lambda. Binary and non-binary data is passed to a Lambda function as a string in a JSON object. The HTTP API Lambda integration automatically infers the need for encoding based on the content-type header passed with the request.

When using curl to pass a plaintext object:

curl -X POST -H 'content-type: text/plain' --data-binary "Hello World" $ECHO_JSON_API | jq .

Lambda receives the following:

{ ... "body": "Hello World", "isBase64Encoded": false
}

When passing a binary object:

curl -X POST -H 'content-type: image/jpeg' --data-binary @../testdata/rainbow-small.jpg $ECHO_JSON_API | jq .

Lambda receives the following:

{ ... "body": "/9j/4AAQSkZ...", "isBase64Encoded": true
}

A Lambda function can inspect the isBase64Encoded flag and reverse the encoding to obtain the original data.

For the response path, API Gateway inspects the isBase64Encoding flag returned from Lambda.

The following response from Lambda results in the corresponding text “base64 encoded”:

{ "statusCode": 200, "body": "YmFzZTY0IGVuY29kZWQK", "isBase64Encoded": true
}

The following response from Lambda results in “plaintext”:

{ "statusCode": 200, "body": "plain text", "isBase64Encoded": false
}

The content-type is text/plain in both cases.

Showing how HTTP APIs handle binary data

This example uses AWS Serverless Application Model (AWS SAM) to show how HTTP APIs with Lambda integration handle binary and text data. The code is available in GitHub.

To follow the example, you need a git client, AWS SAM CLI (minimum version 0.48), and an AWS account:

  1. Create an AWS account if you do not already have one and login.
  2. Clone the repo to your local development machine:
  3. git clone https://github.com/aws-samples/handling-binary-data-using-api-gateway-http-apis-blog.git
    cd aws-samples/handling-binary-data-using-api-gateway-http-apis-blog/sam-code
  4. Build the function dependencies
  5. sam build --use-container
    SAM build output

    SAM build output

  6. Deploy the AWS resources specified in the template.yaml file.
  7. sam deploy --guided
  8. During the prompts, enter the stack name http-api and enter the AWS Region for deployment. Accept the defaults for the remaining questions, answering Y to the three “function may not have authorization defined questions.
  9. SAM deploy output

    SAM deploy output

    Once deployed, AWS SAM outputs the locations of three APIs.

    SAM outputs

    SAM outputs

  10. Assign the values of the API outputs to the following environment variables, in this post’s example:
  11. ECHO_RAW_API=https://99mx7f1kg0.execute-api.us-east-1.amazonaws.com/prod/echoraw
    ECHO_JSON_API=https://99mx7f1kg0.execute-api.us-east-1.amazonaws.com/prod/echojson
    NOISE_API=https://p5kbm51wm7.execute-api.us-east-1.amazonaws.com/prod/noise

Exploring the API

The AWS SAM template defines an HTTP API with a /noise endpoint that forwards ANY HTTP request to a Lambda function. Selection logic routes the request within the single function handler. Separate functions could be created to separate GET and POST functionality. This example uses a single handler to mimic the behavior of frameworks such as FastAPI and Mangum.

The Lambda function uses the Python Pillow library to generate and process images. It accepts query string parameters to define an image to generate. The function also accepts a query string parameter flag called demo64Flag to specify how to return text data and an Accept header to define the mime type or default to JPEG.

Once deployed, the Lambda function performs the following steps depending on the route. A GET request generates an image and a POST request overlays noise on an uploaded image.

HTTP GET to generate JPEGs

  1. Browse to the NoiseHttpApi URL that AWS SAM outputted. The HTTP API endpoint forwards the request to the Lambda function, which routes it as a GET request. The Pillow library converts some random noise and generates a binary JPEG image, resulting in a large horizontal rectangle filled with noise.
  2. Image of noise with default setting

    Image of noise with default setting

    The Lambda function checks if any query string parameters have been passed. The code accepts image height and width, in addition to min and max brightness of noise pixels.

  3. Append the following to the URL: ?w=50&h=100&min=96&max=192
  4. This requests a noise image 50 pixels wide, 100 pixels high, with brightness from 96-192/255, which results in a smaller vertical rectangle filled with noise.

    Image of noise with parameters

    Image of noise with parameters

    Based on the Accept header the Lambda function sets the output image format.

  5. Use curl to add the Accept header and download a GIF image.
  6. # Ask for GIF
    curl "${NOISE_API}?w=50&h=50" --output test-get.gif -H 'Accept: image/gif'
  7. Use file to confirm the returned file is of type GIF.
  8. file test-get.gif
    test-get.gif: GIF image data, version 87a, 50 x 50

    The Lambda function also accepts a query string parameter called demo64Flag. This specifies how to return text data when asked for an unknown image type via the Accept header.

    By invoking with demo64Flag=0, the code returns a plaintext string Text path: Unknown encoding requested. This tells API Gateway that the string is of type text/plain and not base64 encoded.

  9. Use curl to request an unknown image format and return a plaintext answer.
  10. curl "${NOISE_API}?demo64Flag=0" -H 'Accept: image/unknown' Text path: Unknown encoding requested

    Setting demo64Flag=1, the code returns QmluYXJ5IHBhdGg6IFVua25vd24gZW5jb2RpbmcgcmVxdWVzdGVk. This is the base64 encoded equivalent of the string Binary path: Unknown encoding requested, telling API Gateway that the string is of type text/plain and is base64 encoded.

    The HTTP API endpoint uses the isBase64Encoded flag to decode the text before returning the response to the caller.

  11. Use curl to request an unknown image format and return a base64 encoded answer.
  12. curl "${NOISE_API}?demo64Flag=1" -H 'Accept: image/unknown'
    Binary path: Unknown encoding requested

The HTTP API endpoint has converted the base64 encoded string back into text.

HTTP POST to generate overlay noise

The following examples show how to handle both text and binary media types on the same API. The Lambda function POST path accepts images and adds some noise. It also allows uploading text and rendering it to an image before applying noise to it

The repo contains a test image rainbow-small.jpg and a test text file multiline.txt.

Rainbow and text source

Rainbow and text source

  1. Use curl to upload the JPEG rainbow image, overlay noise, and request a GIF file.
  2. curl "${NOISE_API}" -X POST --data-binary @../testdata/rainbow-small.jpg -H 'content-type: image/jpeg' -H 'Accept: image/gif' --output test-post-image.gif
  3. Use file to confirm that the returned file is of type GIF.
  4. $ file test-post-image.gif
    test-post-image.gif: GIF image data, version 87a, 100 x 100
  5. View the resulting image to see the generated noise.
  6. Rainbow with Noise

    Rainbow with Noise

  7. Use curl to upload the text file, render to an image, overlay noise, and request a GIF file. The text file is uploaded with the –data-binary flag to ensure that curl does not remove newlines.
  8. curl "${NOISE_API}?w=100&amp;h=100" -X POST --data-binary @../testdata/multiline.txt -H 'content-type: text/plain' -H 'Accept: image/gif' --output /tmp/test-post-text.gif<br />file test-post-text.gif
  9. Use file to confirm that the returned file is of type GIF.
  10. $ file test-post-text.gif
    test-post-text.gif: GIF image data, version 87a, 100 x 100
    
  11. View the resulting image to see the generated noise.
  12. Text with Noise

    Text with Noise

For both examples, the HTTP API endpoint received an unaltered binary file, using the --data-binary flag. Based on the content-type header, the API understands it is a binary file, base64 encodes it, and notifies the Lambda code with the isBase64Encoded flag. The Lambda function receives the data and base64 decodes it if necessary. It reads the uploaded image or renders text to a new image, and finally adds noise.

CloudWatch Logs confirms that the text is received without being base64 encoded.

... 'body': 'Lorem ipsum dolor sit amet, \nconsectetur adipiscing elit,...', 'isBase64Encoded': False ...

Supporting CORS for browser calls

To show the functionality using a static webpage, the HTTP APIs endpoint must accept CORS requests from anywhere. This is enabled by the CorsConfiguration statement of the HTTP API within the AWS SAM template. The Lambda function must also support the OPTIONS verb and return an Access-Control-Allow-Origin header in the response data.

The example uses a static webpage containing HTML and JavaScript. It lets you draw something with your pointer and makes a fetch() call to the API to overlay some noise once you choose the submit button.

Test this by browsing to this GitHub pages project page. On the page, draw in the top-left box to generate some image data. Enter the NOISE_API URL in the API URL and choose Submit. The Lambda function receives the image data via the HTTP API POST. The function generates the noise and returns the binary file to the client resulting in an image similar to the following:

Noise created from image on web page

Noise created from image on web page

Conclusion

API Gateway HTTP APIs makes it easier to work with both binary and text media types. HTTP API Lambda integration can automatically infer the need for encoding based on the content-type header passed with the request. Passing an isBase64Encoded boolean value with the data simplifies the encoding and decoding of binary data.

HTTP APIs also make it easier to use CORS to provide domain name-based access controls to APIs. This enables use cases such as calling an API from statically hosted sites.

For more serverless learning resources, visit https://serverlessland.com.