fastify-compress
Advanced tools
Comparing version 0.7.1 to 0.8.0
131
index.js
@@ -7,3 +7,11 @@ 'use strict' | ||
const mimedb = require('mime-db') | ||
const isStream = require('is-stream') | ||
const intoStream = require('into-stream') | ||
const peek = require('peek-stream') | ||
const Minipass = require('minipass') | ||
const pumpify = require('pumpify') | ||
const isGzip = require('is-gzip') | ||
const isZip = require('is-zip') | ||
const unZipper = require('unzipper') | ||
const isDeflate = require('is-deflate') | ||
@@ -17,8 +25,13 @@ function compressPlugin (fastify, opts, next) { | ||
const inflateIfDeflated = opts.inflateIfDeflated === true | ||
const threshold = typeof opts.threshold === 'number' ? opts.threshold : 1024 | ||
const compressibleTypes = opts.customTypes instanceof RegExp ? opts.customTypes : /^text\/|\+json$|\+text$|\+xml$|octet-stream$/ | ||
const compressStream = { | ||
gzip: zlib.createGzip, | ||
deflate: zlib.createDeflate | ||
gzip: (opts.zlib || zlib).createGzip || zlib.createGzip, | ||
deflate: (opts.zlib || zlib).createDeflate || zlib.createDeflate | ||
} | ||
const uncompressStream = { | ||
gzip: (opts.zlib || zlib).createGunzip || zlib.createGunzip, | ||
deflate: (opts.zlib || zlib).createInflate || zlib.createInflate | ||
} | ||
@@ -40,17 +53,21 @@ const supportedEncodings = ['deflate', 'gzip', 'identity'] | ||
if (this.request.headers['x-no-compression'] !== undefined) { | ||
return this.send(payload) | ||
} | ||
var stream, encoding | ||
var noCompress = | ||
// don't compress on x-no-compression header | ||
(this.request.headers['x-no-compression'] !== undefined) || | ||
// don't compress if not one of the indiated compressible types | ||
(shouldCompress(this.getHeader('Content-Type') || 'application/json', compressibleTypes) === false) || | ||
// don't compress on missing or identity `accept-encoding` header | ||
((encoding = getEncodingHeader(supportedEncodings, this.request)) === undefined || encoding === 'identity') | ||
var type = this.getHeader('Content-Type') || 'application/json' | ||
if (shouldCompress(type, compressibleTypes) === false) { | ||
if (noCompress) { | ||
if (inflateIfDeflated && isStream(stream = maybeUnzip(payload, this.serialize.bind(this)))) { | ||
encoding === undefined | ||
? this.removeHeader('Content-Encoding') | ||
: this.header('Content-Encoding', 'identity') | ||
pump(stream, payload = unzipStream(uncompressStream), onEnd.bind(this)) | ||
} | ||
return this.send(payload) | ||
} | ||
var encoding = getEncodingHeader(supportedEncodings, this.request) | ||
if (encoding === undefined || encoding === 'identity') { | ||
return this.send(payload) | ||
} | ||
if (encoding === null) { | ||
@@ -66,2 +83,5 @@ closeStream(payload) | ||
} | ||
} | ||
if (typeof payload.pipe !== 'function') { | ||
if (Buffer.byteLength(payload) < threshold) { | ||
@@ -77,3 +97,3 @@ return this.send(payload) | ||
var stream = compressStream[encoding]() | ||
stream = zipStream(compressStream, encoding) | ||
pump(payload, stream, onEnd.bind(this)) | ||
@@ -89,13 +109,21 @@ this.send(stream) | ||
if (req.headers['x-no-compression'] !== undefined) { | ||
return next() | ||
} | ||
var stream, encoding | ||
var noCompress = | ||
// don't compress on x-no-compression header | ||
(req.headers['x-no-compression'] !== undefined) || | ||
// don't compress if not one of the indiated compressible types | ||
(shouldCompress(reply.getHeader('Content-Type') || 'application/json', compressibleTypes) === false) || | ||
// don't compress on missing or identity `accept-encoding` header | ||
((encoding = getEncodingHeader(supportedEncodings, req)) === undefined || encoding === 'identity') | ||
var type = reply.getHeader('Content-Type') || 'application/json' | ||
if (shouldCompress(type, compressibleTypes) === false) { | ||
return next() | ||
if (noCompress) { | ||
if (inflateIfDeflated && isStream(stream = maybeUnzip(payload))) { | ||
encoding === undefined | ||
? reply.removeHeader('Content-Encoding') | ||
: reply.header('Content-Encoding', 'identity') | ||
pump(stream, payload = unzipStream(uncompressStream), onEnd.bind(reply)) | ||
} | ||
return next(null, payload) | ||
} | ||
var encoding = getEncodingHeader(supportedEncodings, req) | ||
if (encoding === null) { | ||
@@ -108,6 +136,2 @@ closeStream(payload) | ||
if (encoding === undefined || encoding === 'identity') { | ||
return next() | ||
} | ||
if (typeof payload.pipe !== 'function') { | ||
@@ -123,3 +147,4 @@ if (Buffer.byteLength(payload) < threshold) { | ||
.removeHeader('content-length') | ||
var stream = compressStream[encoding]() | ||
stream = zipStream(compressStream, encoding) | ||
pump(payload, stream, onEnd.bind(reply)) | ||
@@ -167,2 +192,54 @@ next(null, stream) | ||
function isCompressed (data) { | ||
if (isGzip(data)) return 1 | ||
if (isDeflate(data)) return 2 | ||
if (isZip(data)) return 3 | ||
return 0 | ||
} | ||
function maybeUnzip (payload, serialize) { | ||
if (isStream(payload)) return payload | ||
var buf = payload; var result = payload | ||
if (ArrayBuffer.isView(payload)) { | ||
// Cast non-Buffer DataViews into a Buffer | ||
buf = result = Buffer.from( | ||
payload.buffer, | ||
payload.byteOffset, | ||
payload.byteLength | ||
) | ||
} else if (serialize && typeof payload !== 'string') { | ||
buf = result = serialize(payload) | ||
} | ||
// handle case where serialize doesn't return a string or Buffer | ||
if (!Buffer.isBuffer(buf)) return result | ||
if (isCompressed(buf) === 0) return result | ||
return intoStream(result) | ||
} | ||
function zipStream (deflate, encoding) { | ||
return peek({ newline: false, maxBuffer: 10 }, function (data, swap) { | ||
switch (isCompressed(data)) { | ||
case 1: return swap(null, new Minipass()) | ||
case 2: return swap(null, new Minipass()) | ||
} | ||
return swap(null, deflate[encoding]()) | ||
}) | ||
} | ||
function unzipStream (inflate, maxRecursion) { | ||
if (!(maxRecursion >= 0)) maxRecursion = 3 | ||
return peek({ newline: false, maxBuffer: 10 }, function (data, swap) { | ||
if (maxRecursion < 0) return swap(new Error('Maximum recursion reached')) | ||
switch (isCompressed(data)) { | ||
case 1: return swap(null, pumpify(inflate.gzip(), unzipStream(inflate, maxRecursion - 1))) | ||
case 2: return swap(null, pumpify(inflate.deflate(), unzipStream(inflate, maxRecursion - 1))) | ||
case 3: return swap(null, pumpify(unZipper.ParseOne(), unzipStream(inflate, maxRecursion - 1))) | ||
} | ||
return swap(null, new Minipass()) | ||
}) | ||
} | ||
module.exports = fp(compressPlugin, { | ||
@@ -169,0 +246,0 @@ fastify: '>=1.3.0', |
{ | ||
"name": "fastify-compress", | ||
"version": "0.7.1", | ||
"version": "0.8.0", | ||
"description": "Fastify compression utils", | ||
@@ -9,4 +9,13 @@ "main": "index.js", | ||
"into-stream": "^4.0.0", | ||
"is-deflate": "^1.0.0", | ||
"is-gzip": "^1.0.0", | ||
"is-stream": "^1.1.0", | ||
"is-zip": "^1.0.0", | ||
"mime-db": "^1.33.0", | ||
"pump": "^3.0.0" | ||
"minipass": "2.3.5", | ||
"peek-stream": "^1.1.0", | ||
"pump": "^3.0.0", | ||
"pumpify": "^1.3.3", | ||
"string-to-stream": "^1.1.0", | ||
"unzipper": "^0.8.9" | ||
}, | ||
@@ -13,0 +22,0 @@ "devDependencies": { |
@@ -89,2 +89,16 @@ # fastify-compress | ||
### Inflate pre-compressed bodies for clients that do not support compression | ||
Optional feature to inflate pre-compressed data if the client doesn't include one of the supported compression types in its `Accept-Encoding` header. | ||
```javascript | ||
fastify.register( | ||
require('fastify-compress'), | ||
{ inflateIfDeflated: true } | ||
) | ||
fastify.get('/file', (req, reply) => | ||
// will inflate the file on the way out for clients | ||
// that indicate they do not support compression | ||
reply.send(fs.createReadStream('./file.gz'))) | ||
``` | ||
## Note | ||
@@ -91,0 +105,0 @@ Please have in mind that in large scale scenarios, you should use a proxy like Nginx to handle response-compression. |
199
test.js
@@ -39,2 +39,31 @@ 'use strict' | ||
test('should send a deflated data with custom deflate', t => { | ||
t.plan(5) | ||
let usedCustom = false | ||
const customZlib = { createDeflate: () => (usedCustom = true) && zlib.createDeflate() } | ||
const fastify = Fastify() | ||
fastify.register(compressPlugin, { global: false, zlib: customZlib }) | ||
fastify.get('/', (req, reply) => { | ||
reply.type('text/plain').compress(createReadStream('./package.json')) | ||
}) | ||
fastify.inject({ | ||
url: '/', | ||
method: 'GET', | ||
headers: { | ||
'accept-encoding': 'deflate' | ||
} | ||
}, (err, res) => { | ||
t.error(err) | ||
t.strictEqual(res.headers['content-encoding'], 'deflate') | ||
t.notOk(res.headers['content-length'], 'no content length') | ||
const file = readFileSync('./package.json', 'utf8') | ||
const payload = zlib.inflateSync(res.rawPayload) | ||
t.strictEqual(payload.toString('utf-8'), file) | ||
t.strictEqual(usedCustom, true) | ||
}) | ||
}) | ||
test('should send a gzipped data', t => { | ||
@@ -64,2 +93,83 @@ t.plan(3) | ||
test('should send a gzipped data with custom zlib', t => { | ||
t.plan(4) | ||
let usedCustom = false | ||
const customZlib = { createGzip: () => (usedCustom = true) && zlib.createGzip() } | ||
const fastify = Fastify() | ||
fastify.register(compressPlugin, { global: false, zlib: customZlib }) | ||
fastify.get('/', (req, reply) => { | ||
reply.type('text/plain').compress(createReadStream('./package.json')) | ||
}) | ||
fastify.inject({ | ||
url: '/', | ||
method: 'GET', | ||
headers: { | ||
'accept-encoding': 'gzip' | ||
} | ||
}, (err, res) => { | ||
t.error(err) | ||
t.strictEqual(res.headers['content-encoding'], 'gzip') | ||
const file = readFileSync('./package.json', 'utf8') | ||
const payload = zlib.gunzipSync(res.rawPayload) | ||
t.strictEqual(payload.toString('utf-8'), file) | ||
t.strictEqual(usedCustom, true) | ||
}) | ||
}) | ||
test('should not double-compress Stream if already zipped', t => { | ||
t.plan(3) | ||
const fastify = Fastify() | ||
fastify.register(compressPlugin, { global: false }) | ||
fastify.get('/', (req, reply) => { | ||
reply.type('text/plain').compress( | ||
createReadStream('./package.json') | ||
.pipe(zlib.createGzip()) | ||
) | ||
}) | ||
fastify.inject({ | ||
url: '/', | ||
method: 'GET', | ||
headers: { | ||
'accept-encoding': 'gzip' | ||
} | ||
}, (err, res) => { | ||
t.error(err) | ||
t.strictEqual(res.headers['content-encoding'], 'gzip') | ||
const file = readFileSync('./package.json', 'utf8') | ||
const payload = zlib.gunzipSync(res.rawPayload) | ||
t.strictEqual(payload.toString('utf-8'), file) | ||
}) | ||
}) | ||
test('onSend hook should not double-compress Stream if already zipped', t => { | ||
t.plan(3) | ||
const fastify = Fastify() | ||
fastify.register(compressPlugin, { global: true }) | ||
fastify.get('/', (req, reply) => { | ||
reply.type('text/plain').compress( | ||
createReadStream('./package.json') | ||
.pipe(zlib.createGzip()) | ||
) | ||
}) | ||
fastify.inject({ | ||
url: '/', | ||
method: 'GET', | ||
headers: { | ||
'accept-encoding': 'gzip' | ||
} | ||
}, (err, res) => { | ||
t.error(err) | ||
t.strictEqual(res.headers['content-encoding'], 'gzip') | ||
const file = readFileSync('./package.json', 'utf8') | ||
const payload = zlib.gunzipSync(res.rawPayload) | ||
t.strictEqual(payload.toString('utf-8'), file) | ||
}) | ||
}) | ||
test('should send a gzipped data for * header', t => { | ||
@@ -183,2 +293,67 @@ t.plan(3) | ||
test('should decompress compressed Buffers on missing header', t => { | ||
t.plan(4) | ||
const fastify = Fastify() | ||
fastify.register(compressPlugin, { threshold: 0, inflateIfDeflated: true }) | ||
const json = { hello: 'world' } | ||
fastify.get('/', (req, reply) => { | ||
reply.send(zlib.gzipSync(JSON.stringify(json))) | ||
}) | ||
fastify.inject({ | ||
url: '/', | ||
method: 'GET' | ||
}, (err, res) => { | ||
t.error(err) | ||
t.strictEqual(res.statusCode, 200) | ||
t.notOk(res.headers['content-encoding']) | ||
t.deepEqual(JSON.parse('' + res.payload), json) | ||
}) | ||
}) | ||
test('should decompress data that has been compressed multiple times on missing header', t => { | ||
t.plan(4) | ||
const fastify = Fastify() | ||
fastify.register(compressPlugin, { threshold: 0, inflateIfDeflated: true }) | ||
const json = { hello: 'world' } | ||
fastify.get('/', (req, reply) => { | ||
reply.send([0, 1, 2, 3, 4, 5, 6].reduce( | ||
(x) => zlib.gzipSync(x), JSON.stringify(json) | ||
)) | ||
}) | ||
fastify.inject({ | ||
url: '/', | ||
method: 'GET' | ||
}, (err, res) => { | ||
t.error(err) | ||
t.strictEqual(res.statusCode, 200) | ||
t.notOk(res.headers['content-encoding']) | ||
t.deepEqual(JSON.parse('' + res.payload), json) | ||
}) | ||
}) | ||
test('should decompress compressed Streams on missing header', t => { | ||
t.plan(4) | ||
const fastify = Fastify() | ||
fastify.register(compressPlugin, { threshold: 0, inflateIfDeflated: true }) | ||
fastify.get('/', (req, reply) => { | ||
reply.send(createReadStream('./package.json').pipe(zlib.createGzip())) | ||
}) | ||
fastify.inject({ | ||
url: '/', | ||
method: 'GET' | ||
}, (err, res) => { | ||
t.error(err) | ||
t.strictEqual(res.statusCode, 200) | ||
t.notOk(res.headers['content-encoding']) | ||
const file = readFileSync('./package.json', 'utf8') | ||
t.strictEqual(res.rawPayload.toString('utf-8'), file) | ||
}) | ||
}) | ||
test('Should close the stream', t => { | ||
@@ -601,2 +776,26 @@ t.plan(4) | ||
test('Should decompress compressed payloads on x-no-compression header', t => { | ||
t.plan(4) | ||
const fastify = Fastify() | ||
fastify.register(compressPlugin, { threshold: 0, inflateIfDeflated: true }) | ||
const json = { hello: 'world' } | ||
fastify.get('/', (req, reply) => { | ||
reply.send(zlib.gzipSync(JSON.stringify(json))) | ||
}) | ||
fastify.inject({ | ||
url: '/', | ||
method: 'GET', | ||
headers: { | ||
'x-no-compression': true | ||
} | ||
}, (err, res) => { | ||
t.error(err) | ||
t.strictEqual(res.statusCode, 200) | ||
t.notOk(res.headers['content-encoding']) | ||
t.deepEqual(JSON.parse('' + res.payload), json) | ||
}) | ||
}) | ||
test('Should not try compress missing payload', t => { | ||
@@ -603,0 +802,0 @@ t.plan(4) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
45657
1262
113
0
13
+ Addedis-deflate@^1.0.0
+ Addedis-gzip@^1.0.0
+ Addedis-stream@^1.1.0
+ Addedis-zip@^1.0.0
+ Addedminipass@2.3.5
+ Addedpeek-stream@^1.1.0
+ Addedpumpify@^1.3.3
+ Addedstring-to-stream@^1.1.0
+ Addedunzipper@^0.8.9
+ Addedbalanced-match@1.0.2(transitive)
+ Addedbig-integer@1.6.52(transitive)
+ Addedbinary@0.3.0(transitive)
+ Addedbluebird@3.4.7(transitive)
+ Addedbrace-expansion@1.1.11(transitive)
+ Addedbuffer-from@1.1.2(transitive)
+ Addedbuffer-indexof-polyfill@1.0.2(transitive)
+ Addedbuffer-shims@1.0.0(transitive)
+ Addedbuffers@0.1.1(transitive)
+ Addedchainsaw@0.1.0(transitive)
+ Addedconcat-map@0.0.1(transitive)
+ Addedduplexer2@0.1.4(transitive)
+ Addedduplexify@3.7.1(transitive)
+ Addedfs.realpath@1.0.0(transitive)
+ Addedfstream@1.0.12(transitive)
+ Addedglob@7.2.3(transitive)
+ Addedgraceful-fs@4.2.11(transitive)
+ Addedinflight@1.0.6(transitive)
+ Addedis-deflate@1.0.0(transitive)
+ Addedis-gzip@1.0.0(transitive)
+ Addedis-stream@1.1.0(transitive)
+ Addedis-zip@1.0.0(transitive)
+ Addedlistenercount@1.0.1(transitive)
+ Addedminimatch@3.1.2(transitive)
+ Addedminimist@1.2.8(transitive)
+ Addedminipass@2.3.5(transitive)
+ Addedmkdirp@0.5.6(transitive)
+ Addedpath-is-absolute@1.0.1(transitive)
+ Addedpeek-stream@1.1.3(transitive)
+ Addedprocess-nextick-args@1.0.7(transitive)
+ Addedpump@2.0.1(transitive)
+ Addedpumpify@1.5.1(transitive)
+ Addedreadable-stream@2.1.5(transitive)
+ Addedrimraf@2.7.1(transitive)
+ Addedsafe-buffer@5.2.1(transitive)
+ Addedsetimmediate@1.0.5(transitive)
+ Addedstream-shift@1.0.3(transitive)
+ Addedstring-to-stream@1.1.1(transitive)
+ Addedstring_decoder@0.10.31(transitive)
+ Addedthrough2@2.0.5(transitive)
+ Addedtraverse@0.3.9(transitive)
+ Addedunzipper@0.8.14(transitive)
+ Addedxtend@4.0.2(transitive)
+ Addedyallist@3.1.1(transitive)