Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ The following compression codings are supported:
- deflate
- gzip
- br (brotli)
- zstd (Zstandard)

**Note** Brotli is supported only since Node.js versions v11.7.0 and v10.16.0.
**Note** Brotli is supported only since Node.js versions v11.7.0 and v10.16.0. Zstd is supported only since Node.js v23.8.0 and v22.15.0.

## Install

Expand Down Expand Up @@ -46,9 +47,10 @@ as compressing will transform the body.
#### Options

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

##### chunkSize

Expand Down Expand Up @@ -116,6 +118,11 @@ Type: `Object`

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.

##### zstd

Type: `Object`

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

##### strategy

Expand Down
33 changes: 29 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,33 @@ module.exports.filter = shouldCompress
*/
var hasBrotliSupport = 'createBrotliCompress' in zlib

/**
* @const
* whether current node version has zstd support
*/
var hasZstdSupport = 'createZstdCompress' 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', 'br']
var SUPPORTED_ENCODING = (function () {
var supported = ['gzip', 'deflate', 'identity']
if (hasZstdSupport) supported.unshift('zstd')
if (hasBrotliSupport) supported.unshift('br')
return supported
})()

var PREFERRED_ENCODING = (function () {
var preferred = ['gzip']
if (hasZstdSupport) preferred.unshift('zstd') // prefer zstd over gzip
if (hasBrotliSupport) preferred.unshift('br') // prefer br over zstd or gzip
return preferred
})()

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

/**
* Compress response data with gzip / deflate.
Expand All @@ -57,6 +75,7 @@ var encodingSupported = ['gzip', 'deflate', 'identity', 'br']
function compression (options) {
var opts = options || {}
var optsBrotli = {}
var optsZstd = {}

if (hasBrotliSupport) {
Object.assign(optsBrotli, opts.brotli)
Expand All @@ -68,6 +87,10 @@ function compression (options) {
optsBrotli.params = Object.assign(brotliParams, optsBrotli.params)
}

if (hasZstdSupport) {
Object.assign(optsZstd, opts.zstd)
}

// options
var filter = opts.filter || shouldCompress
var threshold = bytes.parse(opts.threshold)
Expand Down Expand Up @@ -215,7 +238,9 @@ function compression (options) {
? zlib.createGzip(opts)
: method === 'br'
? zlib.createBrotliCompress(optsBrotli)
: zlib.createDeflate(opts)
: method === 'zstd'
? zlib.createZstdCompress(optsZstd)
: zlib.createDeflate(opts)

// add buffered listeners to stream
addListeners(stream, stream.on, listeners)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"type": "opencollective",
"url": "https://opencollective.com/express"
},
"keywords": ["compression", "gzip", "deflate", "middleware", "express", "brotli", "http", "stream"],
"keywords": ["compression", "gzip", "deflate", "middleware", "express", "brotli", "zstd", "http", "stream"],
"dependencies": {
"bytes": "3.1.2",
"compressible": "~2.0.18",
Expand Down
193 changes: 193 additions & 0 deletions test/compression.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ var compression = require('..')
var hasBrotliSupport = 'createBrotliCompress' in zlib
var brotli = hasBrotliSupport ? it : it.skip

/**
* @const
* whether current node version has zstd support
*/
var hasZstdSupport = 'createZstdCompress' in zlib
var zstd = hasZstdSupport ? it : it.skip

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

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

describe('when "Accept-Encoding: zstd" and passing compression level', function () {
zstd('should respond with zstd', function (done) {
var params = {}
params[zlib.constants.ZSTD_c_compressionLevel] = 10

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

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

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

var server = createServer({ threshold: 0, zstd: { 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 @@ -634,6 +687,20 @@ describe('compression()', function () {
})
})

describe('when "Accept-Encoding: deflate, gzip, br, zstd"', 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, zstd')
.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) {
Expand All @@ -648,6 +715,20 @@ describe('compression()', function () {
})
})

describe('when "Accept-Encoding: gzip;q=1, br;q=0.3, zstd;q=0.5"', function () {
it('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, zstd;q=0.5')
.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) {
Expand All @@ -662,6 +743,62 @@ describe('compression()', function () {
})
})

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

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

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

describe('when "Accept-Encoding: gzip;q=0.1, br;q=0.2, zstd"', function () {
zstd('should respond with zstd', 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.1, br;q=0.2, zstd')
.expect('Content-Encoding', 'zstd', 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) {
Expand Down Expand Up @@ -690,6 +827,20 @@ describe('compression()', function () {
})
})

describe('when "Accept-Encoding: deflate, zstd"', function () {
zstd('should respond with zstd', 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, zstd')
.expect('Content-Encoding', 'zstd', 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 @@ -854,6 +1005,32 @@ describe('compression()', function () {
.end()
})

zstd('should flush small chunks for zstd', 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', 'zstd')
.request()
.on('response', unchunk('zstd', 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 @@ -947,6 +1124,19 @@ describe('compression()', function () {
.expect(200, done)
})

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

request(server)
.get('/')
.set('Accept-Encoding', '')
.expect('Content-Encoding', 'zstd')
.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 @@ -1070,6 +1260,9 @@ function unchunk (encoding, onchunk, onend) {
case 'br':
stream = res.pipe(zlib.createBrotliDecompress())
break
case 'zstd':
stream = res.pipe(zlib.createZstdDecompress())
break
}

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