Skip to content

Commit

Permalink
Merge 48c4f2e into 3fea81d
Browse files Browse the repository at this point in the history
  • Loading branch information
danielgindi committed Jul 13, 2020
2 parents 3fea81d + 48c4f2e commit 80dd261
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 5 deletions.
20 changes: 19 additions & 1 deletion README.md
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 @@ -44,7 +47,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 @@ -101,6 +105,20 @@ 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.

##### params *(brotli only)* - [key-value object containing indexed Brotli parameters](https://nodejs.org/api/zlib.html#zlib_brotli_constants)

- `zlib.constants.BROTLI_PARAM_MODE`
- `zlib.constants.BROTLI_MODE_GENERIC` (default)
- `zlib.constants.BROTLI_MODE_TEXT`, adjusted for UTF-8 text
- `zlib.constants.BROTLI_MODE_FONT`, adjusted for WOFF 2.0 fonts
- `zlib.constants.BROTLI_PARAM_QUALITY`
- Ranges from `zlib.constants.BROTLI_MIN_QUALITY` to
`zlib.constants.BROTLI_MAX_QUALITY`, with a default of
`4` (which is not node's default but the most optimal).

Note that here the default is set to compression level 4. This is a balanced setting with a very good speed and a very good
compression ratio.

##### strategy

This is used to tune the compression algorithm. This value only affects the
Expand Down
42 changes: 38 additions & 4 deletions index.js
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,19 @@ var cacheControlNoTransformRegExp = /(?:^|,)\s*?no-transform\s*?(?:,|$)/
function compression (options) {
var opts = options || {}

if (hasBrotliSupport) {
// set the default level to a reasonable value with balanced speed/ratio
if (opts.params === undefined) {
opts = objectAssign({}, opts)
opts.params = {}
}

if (opts.params[zlib.constants.BROTLI_PARAM_QUALITY] === undefined) {
opts.params = objectAssign({}, 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 +207,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 +224,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
Expand Up @@ -13,6 +13,7 @@
"bytes": "3.0.0",
"compressible": "~2.0.17",
"debug": "2.6.9",
"object-assign": "4.1.1",
"on-headers": "~1.0.2",
"safe-buffer": "5.2.0",
"vary": "~1.1.2"
Expand Down
66 changes: 66 additions & 0 deletions test/compression.js
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 80dd261

Please sign in to comment.