AWS API Gateway Binary support using Lambda: Real world example image resize

With the introduction of binary support in API Gateway (APIG) you can now send, for example, image binary blobs through API gateway.  I wanted to have the src attribute on an HTML img tag be a URI to an API gateway endpoint, backed by AWS Lambda.  I was unable to find clear documentation on how to do this.  The walkthrough blog post by AWS is a good start, however it adds un-necessary complexity.

Here is my attempt to make it crystal clear, using a sample image resize lambda.

Step 1: Setup API Gateway

Create a new API Gateway instance with a single GET resource.  Choose Lambda Function integration type and Use Lambda Proxy integration:
Screen Shot 2017-10-17 at 3.39.30 PM

A Lambda Proxy integration takes all the incoming HTTP request data (headers, query string, body etc) and transforms them into a lambda event object which it then reverse proxies to your Lambda function.

New Lambda Request/Response Interface

As you can imagine when your Lambda code gets control, the event object shape is different.  You can see the full input format here, however here is a snippet:

{
  "resource": "Resource path",
  "path": "Path parameter",
  "httpMethod": "Incoming request's method name"
  "headers": {Incoming request headers}
  "queryStringParameters": {query string parameters }
  "pathParameters": {path parameters}
  "stageVariables": {Applicable stage variables}
  "requestContext": {Request context, including authorizer-returned key-value pairs}
  "body": "A JSON string of the request payload."
  <span 				data-mce-type="bookmark" 				id="mce_SELREST_start" 				data-mce-style="overflow:hidden;line-height:0" 				style="overflow:hidden;line-height:0" 			></span>"isBase64Encoded": "A boolean flag to indicate if the applicable request payload is Base64-encode"
}

In this post, the only important attribute is queryStringParameters

You are then also responsible for responding with a specifically shaped JSON object from your Lambda that tells the APIG how to translate to an HTTP response.

The output format docs can be found here, however here is a snippet:

{
  "isBase64Encoded": true|false,
  "statusCode": httpStatusCode,
  "headers": { 'Content-Type': 'image/png', ... },
  "body": "..."
}

Important things here are:

  • isBase64Encoded if true your body must be a base64 encoded string.  I’m always sending image data, so I hardcode this to true
  • statusCode I’m going to use 200,400 and 401

Enable Binary Support

You need to tell APIG what incoming Accept headers will warrant a binary response.  In my case, my service is an image resizer that I want to use on a src attribute.  This comes from a browser, so I have no control over what your browser sends as an Accept header.  Therefore I can tell APIG that every content type should be treated as a binary response.  I do this by specifying */* in the Binary Support section of my APIG instance:
Screen Shot 2017-10-17 at 3.58.35 PM

If you know the exact Accept header the client will send (ex: server-2-server cURL) then you can hard code these.  A simple example of serving PDFs would be application/pdf​ (a simple PDF binary lambda can be found here).

Hit save and make sure to deploy your APIG to a new stage.

Step 2: Setup Lambda function

The code below is pretty straightforward, but it essentially takes an image url (u query string param) and resizes it to a new width (w query string param).  It resizes the image on disk, then returns the binary data base64 encoded (for APIG).  I also add a super simple API key (k) check.

You will then use the APIG endpoint like this in your HTML

<img src="https://blah.execute-api.us-east-1.amazonaws.com/prod?k=myKey&u=https://s3.amazonaws.com/static.me.com/images/app1234/branding/appIcon.png&w=512"><span 				data-mce-type="bookmark" 				id="mce_SELREST_start" 				data-mce-style="overflow:hidden;line-height:0" 				style="overflow:hidden;line-height:0" 			></span>

 

The Lambda code


'use strict';
const im = require('imagemagick');
const fs = require('fs');
const https = require('https');
const http = require('http');
const url = require('url');
const querystring = require('querystring');
const resize = (srcPath, width, callback) => {
const dstPath = `/tmp/resized.png`,
resizeOpts = {
width: width,
srcPath: srcPath,
dstPath: dstPath
};
try {
im.resize(resizeOpts, (err) => {
if (err) {
throw err;
} else {
callback(null, fs.readFileSync(dstPath).toString('base64'));
}
});
} catch (err) {
console.log('Resize operation failed:', err);
callback(err);
}
};
const handleError = (msg, callback, code = 400) => {
const d = {
body: msg || "Unknown error",
statusCode: code
};
callback(null, d);
};
const download = (srcUrl, dest) => {
return new Promise((resolve, reject) => {
console.log(`Downloading`,srcUrl);
let urlParts = {};
try {
urlParts = url.parse(srcUrl);
} catch (e) {
reject(e);
}
let file = fs.createWriteStream(dest);
const protocolLib = ('https:' === urlParts.protocol)
? https
: http;
let request = protocolLib.get(srcUrl, (response) => {
if (response.statusCode !== 200) {
reject(new Error('Non 200 status ' + response.statusCode));
}
response.pipe(file);
file.on('finish', () => {
file.close(resolve); // close() is async, call cb after close completes.
});
});
// check for request error too
request.on('error', (err) => {
fs.unlink(dest);
reject(err);
});
file.on('error', (err) => { // Handle errors
fs.unlink(dest); // Delete the file async. (But we don't check the result)
reject(err);
});
});
};
exports.handler = (event, context, callback) => {
const qs = event.queryStringParameters;
if ('myKey' !== qs.k) {
return handleError('Not authd', callback, 401);
} else if (!qs.w) {
return handleError('Missing width (w)', callback);
} else if (!qs.u) {
return handleError('Missing URL (u)', callback);
}
const srcDownloadPath = '/tmp/srcImg';
download(querystring.unescape(qs.u), srcDownloadPath).then(() => {
resize(srcDownloadPath, qs.w, (err, base64ImageResized) => {
callback(null, {
isBase64Encoded: true,
statusCode: 200,
headers: {
'Content-Type': 'image/png'
},
body: base64ImageResized
});
});
}).catch((err) => {
handleError(err);
});
};

view raw

index.js

hosted with ❤ by GitHub

This code could be cleaner, especially if Lambda supported non LTS NodeJs versions (>7.4  has async/await), but that is a fight for a different day.  Also the fs.unlink() are not needed, since I’m only supporting PNG and I always use the same file path.

Thanks

Hopefully this helps save someone some time.  I don’t check the comments on my blog, so hit me up on twitter if you found this useful.

Advertisement

1 thought on “AWS API Gateway Binary support using Lambda: Real world example image resize

  1. “Hit save and make sure to deploy your APIG to a new stage.”

    After fifty different AWS blogs, stackoverflow questions, and other overlappingly interesting, but otherwise equally ineffective resources, it was not until reading that statement that the “oh, DUH!” lightbulb came on to remind me that I need to create a new deployment for APIGateway whenever the configuration changes. I had been making changes to the Binary Media Types with no visible result… and it was because I hadn’t redeployed the API – ARGH! Thanks for the thorough write-up!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this:
search previous next tag category expand menu location phone mail time cart zoom edit close