Skip to content

Commit de4a562

Browse files
authored
Merge pull request #178 from AndrewBarba/master
Automatic Compression
2 parents 292022e + a2db977 commit de4a562

File tree

6 files changed

+89
-1
lines changed

6 files changed

+89
-1
lines changed

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1342,7 +1342,17 @@ You can also use the `cors()` ([see here](#corsoptions)) convenience method to a
13421342
Conditional route support could be added via middleware or with conditional logic within the `OPTIONS` route.
13431343

13441344
## Compression
1345-
Currently, API Gateway HTTP APIs do not support automatic compression out of the box, but that doesn't mean the Lambda can't return a compressed response. In order to create a compressed response instantiate the API with `isBase64` set to true, and a custom serializer that returns a compressed response as a base64 encoded string. Also, don't forget to set the correct `content-encoding` header:
1345+
Currently, API Gateway HTTP APIs do not support automatic compression, but that doesn't mean the Lambda can't return a compressed response. Lambda API supports compression out of the box:
1346+
1347+
```javascript
1348+
const api = require('lambda-api')({
1349+
compression: true
1350+
})
1351+
```
1352+
1353+
The response will automatically be compressed based on the `Accept-Encoding` header in the request. Supported compressions are Brotli, Gzip and Deflate - in that priority order.
1354+
1355+
For full control over the response compression, instantiate the API with `isBase64` set to true, and a custom serializer that returns a compressed response as a base64 encoded string. Also, don't forget to set the correct `content-encoding` header:
13461356

13471357
```javascript
13481358
const zlib = require('zlib')

index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export declare interface Options {
103103
version?: string;
104104
errorHeaderWhitelist?: string[];
105105
isBase64?: boolean;
106+
compression?: boolean;
106107
headers?: object;
107108
}
108109

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class API {
2929
this._errorHeaderWhitelist = props && Array.isArray(props.errorHeaderWhitelist) ? props.errorHeaderWhitelist.map(header => header.toLowerCase()) : []
3030
this._isBase64 = props && typeof props.isBase64 === 'boolean' ? props.isBase64 : false
3131
this._headers = props && props.headers && typeof props.headers === 'object' ? props.headers : {}
32+
this._compression = props && typeof props.compression === 'boolean' ? props.compression : false
3233

3334
// Set sampling info
3435
this._sampleCounts = {}

lib/compression.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use strict'
2+
3+
/**
4+
* Lightweight web framework for your serverless applications
5+
* @author Jeremy Daly <[email protected]>
6+
* @license MIT
7+
*/
8+
9+
const zlib = require('zlib')
10+
11+
exports.compress = (input,headers) => {
12+
const acceptEncodingHeader = headers['accept-encoding'] || ''
13+
const acceptableEncodings = new Set(acceptEncodingHeader.toLowerCase().split(',').map(str => str.trim()))
14+
15+
// Handle Brotli compression (Only supported in Node v10 and later)
16+
if (acceptableEncodings.has('br') && typeof zlib.brotliCompressSync === 'function') {
17+
return {
18+
data: zlib.brotliCompressSync(input),
19+
contentEncoding: 'br'
20+
}
21+
}
22+
23+
// Handle Gzip compression
24+
if (acceptableEncodings.has('gzip')) {
25+
return {
26+
data: zlib.gzipSync(input),
27+
contentEncoding: 'gzip'
28+
}
29+
}
30+
31+
// Handle deflate compression
32+
if (acceptableEncodings.has('deflate')) {
33+
return {
34+
data: zlib.deflateSync(input),
35+
contentEncoding: 'deflate'
36+
}
37+
}
38+
39+
return {
40+
data: input,
41+
contentEncoding: null
42+
}
43+
}

lib/response.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const UTILS = require('./utils.js')
1010

1111
const fs = require('fs') // Require Node.js file system
1212
const path = require('path') // Require Node.js path
13+
const compression = require('./compression') // Require compression lib
1314
const { ResponseError, FileError } = require('./errors') // Require custom errors
1415

1516
// Require AWS S3 service
@@ -47,6 +48,9 @@ class RESPONSE {
4748
// base64 encoding flag
4849
this._isBase64 = app._isBase64
4950

51+
// compression flag
52+
this._compression = app._compression
53+
5054
// Default callback function
5155
this._callback = 'callback'
5256

@@ -465,6 +469,19 @@ class RESPONSE {
465469
this._request.interface === 'alb' ? { statusDescription: `${this._statusCode} ${UTILS.statusLookup(this._statusCode)}` } : {}
466470
)
467471

472+
// Compress the body
473+
if (this._compression && this._response.body) {
474+
const { data, contentEncoding } = compression.compress(this._response.body, this._request.headers)
475+
if (contentEncoding) {
476+
Object.assign(this._response, { body: data.toString('base64'), isBase64Encoded: true })
477+
if (this._response.multiValueHeaders) {
478+
this._response.multiValueHeaders['content-encoding'] = [contentEncoding]
479+
} else {
480+
this._response.headers['content-encoding'] = contentEncoding
481+
}
482+
}
483+
}
484+
468485
// Trigger the callback function
469486
this.app._callback(null, this._response, this)
470487

test/responses.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,18 @@ const api4 = require('../index')({
2727
return gzipSync(json).toString('base64')
2828
}
2929
})
30+
// Init API with compression
31+
const api5 = require('../index')({
32+
version: 'v1.0',
33+
compression: true
34+
})
3035

3136
let event = {
3237
httpMethod: 'get',
3338
path: '/test',
3439
body: {},
3540
multiValueHeaders: {
41+
'Accept-Encoding': ['deflate, gzip'],
3642
'Content-Type': ['application/json']
3743
}
3844
}
@@ -123,6 +129,10 @@ api4.get('/testGZIP', function(req,res) {
123129
res.json({ object: true })
124130
})
125131

132+
api5.get('/testGZIP', function(req,res) {
133+
res.json({ object: true })
134+
})
135+
126136
/******************************************************************************/
127137
/*** BEGIN TESTS ***/
128138
/******************************************************************************/
@@ -278,6 +288,12 @@ describe('Response Tests:', function() {
278288
expect(result).to.deep.equal({ multiValueHeaders: { 'content-encoding': ['gzip'], 'content-type': ['application/json'] }, statusCode: 200, body: 'H4sIAAAAAAAAE6tWyk/KSk0uUbIqKSpN1VGKTy4tLsnPhXOTEotTzUwg3FoAan86iy0AAAA=', isBase64Encoded: true })
279289
}) // end it
280290

291+
it('Compression (GZIP)', async function() {
292+
let _event = Object.assign({},event,{ path: '/testGZIP'})
293+
let result = await new Promise(r => api5.run(_event,{},(e,res) => { r(res) }))
294+
expect(result).to.deep.equal({ multiValueHeaders: { 'content-encoding': ['gzip'], 'content-type': ['application/json'] }, statusCode: 200, body: 'H4sIAAAAAAAAE6tWyk/KSk0uUbIqKSpNrQUAAQd5Ug8AAAA=', isBase64Encoded: true })
295+
}) // end it
296+
281297
after(function() {
282298
stub.restore()
283299
})

0 commit comments

Comments
 (0)