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
:
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
iftrue
yourbody
must be a base64 encoded string. I’m always sending image data, so I hardcode this totrue
statusCode
I’m going to use200
,400
and401
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:
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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); | |
}); | |
}; |
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.
“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!