Skip to content

Commit

Permalink
Added support for brotli ('br') content-encoding
Browse files Browse the repository at this point in the history
  • Loading branch information
danielgindi committed Jul 10, 2020
1 parent dd5055d commit d3f283f
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 4 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ The following compression codings are supported:

- deflate
- gzip
- br (brotli)

**Note** Brotli provides better and faster compression then gzip or deflate, but is supported only since Node.js versions v11.7.0 and v10.16.0.

## Install

Expand Down Expand Up @@ -46,6 +49,9 @@ as compressing will transform the body.
those listed below, [zlib](http://nodejs.org/api/zlib.html) options may be
passed in to the options object.

As for *brotli*, a default is set to compression level 4, unless
[anything else is specified](https://nodejs.org/api/zlib.html#zlib_class_brotlioptions).

##### chunkSize

The default value is `zlib.Z_DEFAULT_CHUNK`, or `16384`.
Expand Down
35 changes: 31 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ var Buffer = require('safe-buffer').Buffer
var bytes = require('bytes')
var compressible = require('compressible')
var debug = require('debug')('compression')
var objectAssign = require('object-assign')
var onHeaders = require('on-headers')
var vary = require('vary')
var zlib = require('zlib')
Expand All @@ -37,6 +38,24 @@ module.exports.filter = shouldCompress

var cacheControlNoTransformRegExp = /(?:^|,)\s*?no-transform\s*?(?:,|$)/

/**
* @const
* whether current node version has brotli support
*/
var hasBrotliSupport = 'brotli' in process.versions

var supportedEncodings = hasBrotliSupport
? ['gzip', 'deflate', 'br', 'identity']
: ['gzip', 'deflate', 'identity']

var supportedCompressionsNoDeflate = hasBrotliSupport
? ['gzip', 'br']
: ['gzip']

var supportedEncodingsNoDeflate = hasBrotliSupport
? ['gzip', 'br', 'identity']
: ['gzip', 'identity']

/**
* Compress response data with gzip / deflate.
*
Expand All @@ -48,6 +67,12 @@ var cacheControlNoTransformRegExp = /(?:^|,)\s*?no-transform\s*?(?:,|$)/
function compression (options) {
var opts = options || {}

if (hasBrotliSupport && opts.params === undefined) {
opts = objectAssign({}, opts)
opts.params = {}
opts.params[zlib.constants.BROTLI_PARAM_QUALITY] = 4
}

// options
var filter = opts.filter || shouldCompress
var threshold = bytes.parse(opts.threshold)
Expand Down Expand Up @@ -175,11 +200,11 @@ function compression (options) {

// compression method
var accept = accepts(req)
var method = accept.encoding(['gzip', 'deflate', 'identity'])
var method = accept.encoding(supportedEncodings)

// we really don't prefer deflate
if (method === 'deflate' && accept.encoding(['gzip'])) {
method = accept.encoding(['gzip', 'identity'])
if (method === 'deflate' && accept.encoding(supportedCompressionsNoDeflate)) {
method = accept.encoding(supportedEncodingsNoDeflate)
}

// negotiation failed
Expand All @@ -192,7 +217,9 @@ function compression (options) {
debug('%s compression', method)
stream = method === 'gzip'
? zlib.createGzip(opts)
: zlib.createDeflate(opts)
: method === 'br'
? zlib.createBrotliCompress(opts)
: zlib.createDeflate(opts)

// add buffered listeners to stream
addListeners(stream, stream.on, listeners)
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"bytes": "3.0.0",
"compressible": "~2.0.16",
"debug": "2.6.9",
"object-assign": "4.1.1",
"on-headers": "~1.0.2",
"safe-buffer": "5.1.2",
"vary": "~1.1.2"
Expand Down
66 changes: 66 additions & 0 deletions test/compression.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ var zlib = require('zlib')

var compression = require('..')

/**
* @const
* whether current node version has brotli support
*/
var hasBrotliSupport = 'brotli' in process.versions

describe('compression()', function () {
it('should skip HEAD', function (done) {
var server = createServer({ threshold: 0 }, function (req, res) {
Expand Down Expand Up @@ -465,6 +471,21 @@ describe('compression()', function () {
})
})

describe('when "Accept-Encoding: br"', function () {
var brotlit = hasBrotliSupport ? it : it.skip
brotlit('should respond with br', function (done) {
var server = createServer({ threshold: 0 }, function (req, res) {
res.setHeader('Content-Type', 'text/plain')
res.end('hello, world')
})

request(server)
.get('/')
.set('Accept-Encoding', 'br')
.expect('Content-Encoding', 'br', done)
})
})

describe('when "Accept-Encoding: gzip, deflate"', function () {
it('should respond with gzip', function (done) {
var server = createServer({ threshold: 0 }, function (req, res) {
Expand Down Expand Up @@ -493,6 +514,21 @@ describe('compression()', function () {
})
})

describe('when "Accept-Encoding: deflate, br"', function () {
var brotlit = hasBrotliSupport ? it : it.skip
brotlit('should respond with br', function (done) {
var server = createServer({ threshold: 0 }, function (req, res) {
res.setHeader('Content-Type', 'text/plain')
res.end('hello, world')
})

request(server)
.get('/')
.set('Accept-Encoding', 'deflate, br')
.expect('Content-Encoding', 'br', done)
})
})

describe('when "Cache-Control: no-transform" response header', function () {
it('should not compress response', function (done) {
var server = createServer({ threshold: 0 }, function (req, res) {
Expand Down Expand Up @@ -631,6 +667,33 @@ describe('compression()', function () {
.end()
})

var brotlit = hasBrotliSupport ? it : it.skip
brotlit('should flush small chunks for brotli', function (done) {
var chunks = 0
var next
var server = createServer({ threshold: 0 }, function (req, res) {
next = writeAndFlush(res, 2, Buffer.from('..'))
res.setHeader('Content-Type', 'text/plain')
next()
})

function onchunk (chunk) {
assert.ok(chunks++ < 20)
assert.strictEqual(chunk.toString(), '..')
next()
}

request(server)
.get('/')
.set('Accept-Encoding', 'br')
.request()
.on('response', unchunk('br', onchunk, function (err) {
if (err) return done(err)
server.close(done)
}))
.end()
})

it('should flush small chunks for deflate', function (done) {
var chunks = 0
var next
Expand Down Expand Up @@ -710,6 +773,9 @@ function unchunk (encoding, onchunk, onend) {
case 'gzip':
stream = res.pipe(zlib.createGunzip())
break
case 'br':
stream = res.pipe(zlib.createBrotliDecompress())
break
}

stream.on('data', onchunk)
Expand Down

0 comments on commit d3f283f

Please sign in to comment.