@fastify/compress 9.1.1 → 9.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/lock-threads.yml +1 -1
- package/README.md +84 -0
- package/benchmark/sync-compression.mjs +282 -0
- package/index.js +131 -15
- package/lib/utils.js +41 -0
- package/package.json +2 -1
- package/test/global-compress.test.js +161 -2
- package/test/regression/issue-350.test.js +3 -1
- package/test/routes-compress.test.js +26 -0
- package/test/sync-compress.test.js +741 -0
- package/test/utils.test.js +35 -1
- package/types/index.d.ts +2 -0
- package/types/index.tst.ts +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fastify/compress",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.2.0",
|
|
4
4
|
"description": "Fastify compression utils",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"scripts": {
|
|
27
27
|
"lint": "eslint",
|
|
28
28
|
"lint:fix": "eslint --fix",
|
|
29
|
+
"benchmark": "node benchmark/sync-compression.mjs",
|
|
29
30
|
"test": "npm run test:unit && npm run test:typescript",
|
|
30
31
|
"test:typescript": "tstyche",
|
|
31
32
|
"test:unit": "node --test",
|
|
@@ -318,6 +318,98 @@ describe('When `global` is not set, it is `true` by default :', async () => {
|
|
|
318
318
|
t.assert.equal(zlib.gunzipSync(response.rawPayload).toString('utf-8'), body)
|
|
319
319
|
})
|
|
320
320
|
|
|
321
|
+
test('it should not compress a Fetch API Response holding a non-compressible `Content-Type`', async (t) => {
|
|
322
|
+
t.plan(2)
|
|
323
|
+
|
|
324
|
+
const fastify = Fastify()
|
|
325
|
+
await fastify.register(compressPlugin, { threshold: 0 })
|
|
326
|
+
|
|
327
|
+
const body = 'data: hello from fetch response\n\n'
|
|
328
|
+
fastify.get('/fetch-resp', (_request, reply) => {
|
|
329
|
+
reply.send(new Response(body, { headers: { 'content-type': 'text/event-stream' } }))
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
const response = await fastify.inject({
|
|
333
|
+
url: '/fetch-resp',
|
|
334
|
+
method: 'GET',
|
|
335
|
+
headers: { 'accept-encoding': 'gzip' }
|
|
336
|
+
})
|
|
337
|
+
t.assert.equal(response.headers['content-encoding'], undefined)
|
|
338
|
+
t.assert.equal(response.rawPayload.toString('utf-8'), body)
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
test('it should not compress a Fetch API Response that is already compressed', async (t) => {
|
|
342
|
+
t.plan(2)
|
|
343
|
+
|
|
344
|
+
const fastify = Fastify()
|
|
345
|
+
await fastify.register(compressPlugin, { threshold: 0 })
|
|
346
|
+
|
|
347
|
+
const body = 'hello from fetch response'
|
|
348
|
+
fastify.get('/fetch-resp', (_request, reply) => {
|
|
349
|
+
reply.send(new Response(zlib.brotliCompressSync(body), {
|
|
350
|
+
headers: {
|
|
351
|
+
'content-type': 'text/plain',
|
|
352
|
+
'content-encoding': 'br'
|
|
353
|
+
}
|
|
354
|
+
}))
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
const response = await fastify.inject({
|
|
358
|
+
url: '/fetch-resp',
|
|
359
|
+
method: 'GET',
|
|
360
|
+
headers: { 'accept-encoding': 'gzip' }
|
|
361
|
+
})
|
|
362
|
+
t.assert.equal(response.headers['content-encoding'], 'br')
|
|
363
|
+
t.assert.equal(zlib.brotliDecompressSync(response.rawPayload).toString('utf-8'), body)
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
test('it should not compress a partial Fetch API Response', async (t) => {
|
|
367
|
+
t.plan(3)
|
|
368
|
+
|
|
369
|
+
const fastify = Fastify()
|
|
370
|
+
await fastify.register(compressPlugin, { threshold: 0 })
|
|
371
|
+
|
|
372
|
+
const body = 'hello fr'
|
|
373
|
+
fastify.get('/fetch-resp', (_request, reply) => {
|
|
374
|
+
reply.send(new Response(body, {
|
|
375
|
+
status: 206,
|
|
376
|
+
headers: {
|
|
377
|
+
'content-type': 'text/plain',
|
|
378
|
+
'content-range': 'bytes 0-7/25'
|
|
379
|
+
}
|
|
380
|
+
}))
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
const response = await fastify.inject({
|
|
384
|
+
url: '/fetch-resp',
|
|
385
|
+
method: 'GET',
|
|
386
|
+
headers: { 'accept-encoding': 'gzip' }
|
|
387
|
+
})
|
|
388
|
+
t.assert.equal(response.statusCode, 206)
|
|
389
|
+
t.assert.equal(response.headers['content-encoding'], undefined)
|
|
390
|
+
t.assert.equal(response.rawPayload.toString('utf-8'), body)
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
test('it should send a bodyless Fetch API Response as is', async (t) => {
|
|
394
|
+
t.plan(3)
|
|
395
|
+
|
|
396
|
+
const fastify = Fastify()
|
|
397
|
+
await fastify.register(compressPlugin, { threshold: 0 })
|
|
398
|
+
|
|
399
|
+
fastify.get('/fetch-resp', (_request, reply) => {
|
|
400
|
+
reply.send(new Response(null, { status: 204, headers: { 'x-custom': 'kept' } }))
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
const response = await fastify.inject({
|
|
404
|
+
url: '/fetch-resp',
|
|
405
|
+
method: 'GET',
|
|
406
|
+
headers: { 'accept-encoding': 'gzip' }
|
|
407
|
+
})
|
|
408
|
+
t.assert.equal(response.statusCode, 204)
|
|
409
|
+
t.assert.equal(response.headers['x-custom'], 'kept')
|
|
410
|
+
t.assert.equal(response.rawPayload.length, 0)
|
|
411
|
+
})
|
|
412
|
+
|
|
321
413
|
test('it should compress a Web ReadableStream body', async (t) => {
|
|
322
414
|
t.plan(1)
|
|
323
415
|
|
|
@@ -2035,11 +2127,14 @@ describe('It should support stream1 :', async () => {
|
|
|
2035
2127
|
})
|
|
2036
2128
|
|
|
2037
2129
|
describe('It should remove `Content-Length` header :', async () => {
|
|
2130
|
+
// `./package.json` is small enough to be compressed synchronously, and the synchronous
|
|
2131
|
+
// path knows the compressed size, so it replaces the stale `Content-Length` with an
|
|
2132
|
+
// accurate one instead of removing it. `syncThreshold: 0` restores the streamed behaviour.
|
|
2038
2133
|
test('using `reply.compress()`', async (t) => {
|
|
2039
2134
|
t.plan(4)
|
|
2040
2135
|
|
|
2041
2136
|
const fastify = Fastify()
|
|
2042
|
-
await fastify.register(compressPlugin, { global: true })
|
|
2137
|
+
await fastify.register(compressPlugin, { global: true, syncThreshold: 0 })
|
|
2043
2138
|
|
|
2044
2139
|
fastify.get('/', (_request, reply) => {
|
|
2045
2140
|
readFile('./package.json', 'utf8', (err, data) => {
|
|
@@ -2099,7 +2194,7 @@ describe('It should remove `Content-Length` header :', async () => {
|
|
|
2099
2194
|
t.plan(4)
|
|
2100
2195
|
|
|
2101
2196
|
const fastify = Fastify()
|
|
2102
|
-
await fastify.register(compressPlugin, { global: true })
|
|
2197
|
+
await fastify.register(compressPlugin, { global: true, syncThreshold: 0 })
|
|
2103
2198
|
|
|
2104
2199
|
fastify.get('/', (_request, reply) => {
|
|
2105
2200
|
readFile('./package.json', 'utf8', (err, data) => {
|
|
@@ -2851,6 +2946,70 @@ describe('It should send data compressed according to `brotliOptions` :', async
|
|
|
2851
2946
|
const compressedPayload = zlib.brotliCompressSync(file, defaultBrotliOptions)
|
|
2852
2947
|
t.assert.deepEqual(response.rawPayload, compressedPayload)
|
|
2853
2948
|
})
|
|
2949
|
+
|
|
2950
|
+
test('default BROTLI_PARAM_QUALITY to be 4 when `global` is `false`', async (t) => {
|
|
2951
|
+
t.plan(2)
|
|
2952
|
+
|
|
2953
|
+
const fastify = Fastify()
|
|
2954
|
+
await fastify.register(compressPlugin, { global: false })
|
|
2955
|
+
|
|
2956
|
+
const file = readFileSync('./package.json', 'utf8')
|
|
2957
|
+
fastify.get('/', (_request, reply) => {
|
|
2958
|
+
reply
|
|
2959
|
+
.type('text/plain')
|
|
2960
|
+
.compress(file)
|
|
2961
|
+
})
|
|
2962
|
+
|
|
2963
|
+
const response = await fastify.inject({
|
|
2964
|
+
url: '/',
|
|
2965
|
+
method: 'GET',
|
|
2966
|
+
headers: {
|
|
2967
|
+
'accept-encoding': 'br'
|
|
2968
|
+
}
|
|
2969
|
+
})
|
|
2970
|
+
|
|
2971
|
+
const defaultBrotliOptions = {
|
|
2972
|
+
params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 4 }
|
|
2973
|
+
}
|
|
2974
|
+
const compressedPayload = zlib.brotliCompressSync(file, defaultBrotliOptions)
|
|
2975
|
+
t.assert.deepEqual(response.rawPayload, compressedPayload)
|
|
2976
|
+
|
|
2977
|
+
// zlib's own default of quality 11 is far more expensive and must not be used
|
|
2978
|
+
const maxQualityPayload = zlib.brotliCompressSync(file)
|
|
2979
|
+
t.assert.notDeepEqual(response.rawPayload, maxQualityPayload)
|
|
2980
|
+
})
|
|
2981
|
+
|
|
2982
|
+
test('user supplied `brotliOptions` still take precedence when `global` is `false`', async (t) => {
|
|
2983
|
+
t.plan(1)
|
|
2984
|
+
|
|
2985
|
+
const brotliOptions = {
|
|
2986
|
+
params: {
|
|
2987
|
+
[zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT,
|
|
2988
|
+
[zlib.constants.BROTLI_PARAM_QUALITY]: 8
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
const fastify = Fastify()
|
|
2993
|
+
await fastify.register(compressPlugin, { global: false, brotliOptions })
|
|
2994
|
+
|
|
2995
|
+
const file = readFileSync('./package.json', 'utf8')
|
|
2996
|
+
fastify.get('/', (_request, reply) => {
|
|
2997
|
+
reply
|
|
2998
|
+
.type('text/plain')
|
|
2999
|
+
.compress(file)
|
|
3000
|
+
})
|
|
3001
|
+
|
|
3002
|
+
const response = await fastify.inject({
|
|
3003
|
+
url: '/',
|
|
3004
|
+
method: 'GET',
|
|
3005
|
+
headers: {
|
|
3006
|
+
'accept-encoding': 'br'
|
|
3007
|
+
}
|
|
3008
|
+
})
|
|
3009
|
+
|
|
3010
|
+
const compressedPayload = zlib.brotliCompressSync(file, brotliOptions)
|
|
3011
|
+
t.assert.deepEqual(response.rawPayload, compressedPayload)
|
|
3012
|
+
})
|
|
2854
3013
|
})
|
|
2855
3014
|
|
|
2856
3015
|
describe('It should send data compressed according to `zlibOptions` :', async () => {
|
|
@@ -22,7 +22,9 @@ async function routes (fastify) {
|
|
|
22
22
|
|
|
23
23
|
test('should compress large payload without premature close', async (t) => {
|
|
24
24
|
const fastify = Fastify()
|
|
25
|
-
|
|
25
|
+
// `syncThreshold: 0` keeps this payload on the stream pipeline this regression covers,
|
|
26
|
+
// whatever default the host parallelism would otherwise produce
|
|
27
|
+
await fastify.register(fastifyCompress, { encodings: ['gzip'], global: true, syncThreshold: 0 })
|
|
26
28
|
await fastify.register(routes)
|
|
27
29
|
|
|
28
30
|
const response = await fastify.inject({
|
|
@@ -381,6 +381,32 @@ test('reply.compress should preserve a Fetch Response status and headers', async
|
|
|
381
381
|
t.assert.equal(zlib.gunzipSync(res.rawPayload).toString('utf8'), 'from reply.compress')
|
|
382
382
|
})
|
|
383
383
|
|
|
384
|
+
test('reply.compress should not compress a Fetch Response holding a non-compressible `Content-Type`', async (t) => {
|
|
385
|
+
t.plan(2)
|
|
386
|
+
const fastify = Fastify()
|
|
387
|
+
await fastify.register(compressPlugin, { global: true, threshold: 0 })
|
|
388
|
+
fastify.get('/', (_req, reply) => {
|
|
389
|
+
reply.compress(new Response('data: from reply.compress\n\n', {
|
|
390
|
+
headers: { 'content-type': 'text/event-stream' }
|
|
391
|
+
}))
|
|
392
|
+
})
|
|
393
|
+
const res = await fastify.inject({ url: '/', method: 'GET', headers: { 'accept-encoding': 'gzip' } })
|
|
394
|
+
t.assert.equal(res.headers['content-encoding'], undefined)
|
|
395
|
+
t.assert.equal(res.rawPayload.toString('utf8'), 'data: from reply.compress\n\n')
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
test('reply.compress should send a bodyless Fetch Response as is', async (t) => {
|
|
399
|
+
t.plan(2)
|
|
400
|
+
const fastify = Fastify()
|
|
401
|
+
await fastify.register(compressPlugin, { global: true, threshold: 0 })
|
|
402
|
+
fastify.get('/', (_req, reply) => {
|
|
403
|
+
reply.compress(new Response(null, { status: 204 }))
|
|
404
|
+
})
|
|
405
|
+
const res = await fastify.inject({ url: '/', method: 'GET', headers: { 'accept-encoding': 'gzip' } })
|
|
406
|
+
t.assert.equal(res.statusCode, 204)
|
|
407
|
+
t.assert.equal(res.rawPayload.length, 0)
|
|
408
|
+
})
|
|
409
|
+
|
|
384
410
|
test('reply.compress should handle Web ReadableStream', async (t) => {
|
|
385
411
|
t.plan(1)
|
|
386
412
|
const fastify = Fastify()
|