Skip to content

Commit

Permalink
feat: support for brotli (#194)
Browse files Browse the repository at this point in the history
* Added support for brotli ('br') content-encoding

* Update README.md

* Update README.md

* Update README.md

* Apply default value also when params is specified

* Increase coverage for specifying params

* Updated brotli detection method

* Prefer br over gzip and deflate

* feat: use "koa-compress" logic to determine the preferred encoding

* test: adding one more test case br/gzip with quality params

* chore: fix linting errors

* fix: hand write encodings lib to be compatible with node 0.8

* Fix: fixing lint errors in new lib

* Fix: fixing lint errors in new lib

* implemented required encoding negotiator without 3rd party dependency

* fix

* use negotiator

* improve negotiateEnconding

* fix support

* update history

* add new test

* add new test

* Update test/compression.js

Co-authored-by: Wes Todd <[email protected]>

* improve parse options

* don't directly manipulate the object.

* remove .npmrc

* use object assign in params

* test: add test for enforceEnconding

* deps: remove  object-assign

---------

Co-authored-by: Daniel Cohen Gindi <[email protected]>
Co-authored-by: Nick Randall <[email protected]>
Co-authored-by: Ulises Gascón <[email protected]>
Co-authored-by: Wes Todd <[email protected]>
  • Loading branch information
5 people authored Jan 8, 2025
1 parent 96df7c5 commit d272132
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 5 deletions.
1 change: 1 addition & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
unreleased
==========
* Use `res.headersSent` when available
* add brotli support for versions of node that support it
* Add the enforceEncoding option for requests without `Accept-Encoding` header

1.7.5 / 2024-10-31
Expand Down
11 changes: 10 additions & 1 deletion 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 is supported only since Node.js versions v11.7.0 and v10.16.0.

## Install

Expand Down Expand Up @@ -42,7 +45,8 @@ as compressing will transform the body.

`compression()` accepts these properties in the options object. In addition to
those listed below, [zlib](http://nodejs.org/api/zlib.html) options may be
passed in to the options object.
passed in to the options object or
[brotli](https://nodejs.org/api/zlib.html#zlib_class_brotlioptions) options.

##### chunkSize

Expand Down Expand Up @@ -99,6 +103,11 @@ The default value is `zlib.Z_DEFAULT_MEMLEVEL`, or `8`.
See [Node.js documentation](http://nodejs.org/api/zlib.html#zlib_memory_usage_tuning)
regarding the usage.

##### brotli

This specifies the options for configuring Brotli. See [Node.js documentation](https://nodejs.org/api/zlib.html#class-brotlioptions) for a complete list of available options.


##### strategy

This is used to tune the compression algorithm. This value only affects the
Expand Down
28 changes: 24 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,21 @@ var zlib = require('zlib')
module.exports = compression
module.exports.filter = shouldCompress

/**
* @const
* whether current node version has brotli support
*/
var hasBrotliSupport = 'createBrotliCompress' in zlib

/**
* Module variables.
* @private
*/

var cacheControlNoTransformRegExp = /(?:^|,)\s*?no-transform\s*?(?:,|$)/
var SUPPORTED_ENCODING = hasBrotliSupport ? ['br', 'gzip', 'deflate', 'identity'] : ['gzip', 'deflate', 'identity']
var PREFERRED_ENCODING = hasBrotliSupport ? ['br', 'gzip'] : ['gzip']

var encodingSupported = ['*', 'gzip', 'deflate', 'identity']
var encodingSupported = ['*', 'gzip', 'deflate', 'identity', 'br']

/**
* Compress response data with gzip / deflate.
Expand All @@ -49,6 +56,17 @@ var encodingSupported = ['*', 'gzip', 'deflate', 'identity']

function compression (options) {
var opts = options || {}
var optsBrotli = {}

if (hasBrotliSupport) {
Object.assign(optsBrotli, opts.brotli)

var brotliParams = {}
brotliParams[zlib.constants.BROTLI_PARAM_QUALITY] = 4

// set the default level to a reasonable value with balanced speed/ratio
optsBrotli.params = Object.assign(brotliParams, optsBrotli.params)
}

// options
var filter = opts.filter || shouldCompress
Expand Down Expand Up @@ -178,7 +196,7 @@ function compression (options) {

// compression method
var negotiator = new Negotiator(req)
var method = negotiator.encoding(['gzip', 'deflate', 'identity'], ['gzip'])
var method = negotiator.encoding(SUPPORTED_ENCODING, PREFERRED_ENCODING)

// if no method is found, use the default encoding
if (!req.headers['accept-encoding'] && encodingSupported.indexOf(enforceEncoding) !== -1) {
Expand All @@ -195,7 +213,9 @@ function compression (options) {
debug('%s compression', method)
stream = method === 'gzip'
? zlib.createGzip(opts)
: zlib.createDeflate(opts)
: method === 'br'
? zlib.createBrotliCompress(optsBrotli)
: zlib.createDeflate(opts)

// add buffered listeners to stream
addListeners(stream, stream.on, listeners)
Expand Down
194 changes: 194 additions & 0 deletions test/compression.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ try {

var compression = require('..')

/**
* @const
* whether current node version has brotli support
*/
var hasBrotliSupport = 'createBrotliCompress' in zlib
var brotli = hasBrotliSupport ? it : it.skip

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

describe('when "Accept-Encoding: br"', function () {
brotli('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: br" and passing compression level', function () {
brotli('should respond with br', function (done) {
var params = {}
params[zlib.constants.BROTLI_PARAM_QUALITY] = 11

var server = createServer({ threshold: 0, brotli: { params: params } }, 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)
})

brotli('shouldn\'t break compression when gzip is requested', function (done) {
var params = {}
params[zlib.constants.BROTLI_PARAM_QUALITY] = 8

var server = createServer({ threshold: 0, brotli: { params: params } }, function (req, res) {
res.setHeader('Content-Type', 'text/plain')
res.end('hello, world')
})

request(server)
.get('/')
.set('Accept-Encoding', 'gzip')
.expect('Content-Encoding', 'gzip', 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 @@ -538,6 +591,105 @@ describe('compression()', function () {
})
})

describe('when "Accept-Encoding: gzip, br"', function () {
var brotli = hasBrotliSupport ? it : it.skip
brotli('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', 'gzip, br')
.expect('Content-Encoding', 'br', done)
})

brotli = hasBrotliSupport ? it.skip : it

brotli('should respond with gzip', 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, gzip')
.expect('Content-Encoding', 'gzip', done)
})
})

describe('when "Accept-Encoding: deflate, gzip, br"', function () {
brotli('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, gzip, br')
.expect('Content-Encoding', 'br', done)
})
})

describe('when "Accept-Encoding: gzip;q=1, br;q=0.3"', function () {
brotli('should respond with gzip', 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', 'gzip;q=1, br;q=0.3')
.expect('Content-Encoding', 'gzip', done)
})
})

describe('when "Accept-Encoding: gzip, br;q=0.8"', function () {
brotli('should respond with gzip', 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', 'gzip, br;q=0.8')
.expect('Content-Encoding', 'gzip', done)
})
})

describe('when "Accept-Encoding: gzip;q=0.001"', function () {
brotli('should respond with gzip', 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', 'gzip;q=0.001')
.expect('Content-Encoding', 'gzip', done)
})
})

describe('when "Accept-Encoding: deflate, br"', function () {
brotli('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 @@ -676,6 +828,32 @@ describe('compression()', function () {
.end()
})

brotli('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 @@ -756,6 +934,19 @@ describe('compression()', function () {
.expect(200, 'hello, world', done)
})

brotli('should compress when enforceEncoding is brotli', function (done) {
var server = createServer({ threshold: 0, enforceEncoding: 'br' }, function (req, res) {
res.setHeader('Content-Type', 'text/plain')
res.end('hello, world')
})

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

it('should not compress when enforceEncoding is unknown', function (done) {
var server = createServer({ threshold: 0, enforceEncoding: 'bogus' }, function (req, res) {
res.setHeader('Content-Type', 'text/plain')
Expand Down Expand Up @@ -876,6 +1067,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 d272132

Please sign in to comment.