Skip to content

Commit

Permalink
Merge pull request #178 from AndrewBarba/master
Browse files Browse the repository at this point in the history
Automatic Compression
  • Loading branch information
jeremydaly authored Mar 24, 2021
2 parents 292022e + a2db977 commit de4a562
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 1 deletion.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1342,7 +1342,17 @@ You can also use the `cors()` ([see here](#corsoptions)) convenience method to a
Conditional route support could be added via middleware or with conditional logic within the `OPTIONS` route.

## Compression
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:
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:

```javascript
const api = require('lambda-api')({
compression: true
})
```

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.

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:

```javascript
const zlib = require('zlib')
Expand Down
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export declare interface Options {
version?: string;
errorHeaderWhitelist?: string[];
isBase64?: boolean;
compression?: boolean;
headers?: object;
}

Expand Down
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class API {
this._errorHeaderWhitelist = props && Array.isArray(props.errorHeaderWhitelist) ? props.errorHeaderWhitelist.map(header => header.toLowerCase()) : []
this._isBase64 = props && typeof props.isBase64 === 'boolean' ? props.isBase64 : false
this._headers = props && props.headers && typeof props.headers === 'object' ? props.headers : {}
this._compression = props && typeof props.compression === 'boolean' ? props.compression : false

// Set sampling info
this._sampleCounts = {}
Expand Down
43 changes: 43 additions & 0 deletions lib/compression.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use strict'

/**
* Lightweight web framework for your serverless applications
* @author Jeremy Daly <jeremy@jeremydaly.com>
* @license MIT
*/

const zlib = require('zlib')

exports.compress = (input,headers) => {
const acceptEncodingHeader = headers['accept-encoding'] || ''
const acceptableEncodings = new Set(acceptEncodingHeader.toLowerCase().split(',').map(str => str.trim()))

// Handle Brotli compression (Only supported in Node v10 and later)
if (acceptableEncodings.has('br') && typeof zlib.brotliCompressSync === 'function') {
return {
data: zlib.brotliCompressSync(input),
contentEncoding: 'br'
}
}

// Handle Gzip compression
if (acceptableEncodings.has('gzip')) {
return {
data: zlib.gzipSync(input),
contentEncoding: 'gzip'
}
}

// Handle deflate compression
if (acceptableEncodings.has('deflate')) {
return {
data: zlib.deflateSync(input),
contentEncoding: 'deflate'
}
}

return {
data: input,
contentEncoding: null
}
}
17 changes: 17 additions & 0 deletions lib/response.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const UTILS = require('./utils.js')

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

// Require AWS S3 service
Expand Down Expand Up @@ -47,6 +48,9 @@ class RESPONSE {
// base64 encoding flag
this._isBase64 = app._isBase64

// compression flag
this._compression = app._compression

// Default callback function
this._callback = 'callback'

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

// Compress the body
if (this._compression && this._response.body) {
const { data, contentEncoding } = compression.compress(this._response.body, this._request.headers)
if (contentEncoding) {
Object.assign(this._response, { body: data.toString('base64'), isBase64Encoded: true })
if (this._response.multiValueHeaders) {
this._response.multiValueHeaders['content-encoding'] = [contentEncoding]
} else {
this._response.headers['content-encoding'] = contentEncoding
}
}
}

// Trigger the callback function
this.app._callback(null, this._response, this)

Expand Down
16 changes: 16 additions & 0 deletions test/responses.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,18 @@ const api4 = require('../index')({
return gzipSync(json).toString('base64')
}
})
// Init API with compression
const api5 = require('../index')({
version: 'v1.0',
compression: true
})

let event = {
httpMethod: 'get',
path: '/test',
body: {},
multiValueHeaders: {
'Accept-Encoding': ['deflate, gzip'],
'Content-Type': ['application/json']
}
}
Expand Down Expand Up @@ -123,6 +129,10 @@ api4.get('/testGZIP', function(req,res) {
res.json({ object: true })
})

api5.get('/testGZIP', function(req,res) {
res.json({ object: true })
})

/******************************************************************************/
/*** BEGIN TESTS ***/
/******************************************************************************/
Expand Down Expand Up @@ -278,6 +288,12 @@ describe('Response Tests:', function() {
expect(result).to.deep.equal({ multiValueHeaders: { 'content-encoding': ['gzip'], 'content-type': ['application/json'] }, statusCode: 200, body: 'H4sIAAAAAAAAE6tWyk/KSk0uUbIqKSpN1VGKTy4tLsnPhXOTEotTzUwg3FoAan86iy0AAAA=', isBase64Encoded: true })
}) // end it

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

after(function() {
stub.restore()
})
Expand Down

0 comments on commit de4a562

Please sign in to comment.