@fastify/compress 8.3.0 → 9.0.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 +19 -0
- package/LICENSE +1 -3
- package/index.js +19 -21
- package/package.json +7 -12
- package/test/global-decompress.test.js +4 -2
- package/test/regression/issue-350.test.js +43 -0
- package/test/regression/issue-382.test.js +193 -0
- package/test/routes-compress.test.js +0 -89
- package/test/routes-decompress.test.js +4 -87
- package/types/index.d.ts +0 -14
- package/types/{index.test-d.ts → index.tst.ts} +31 -31
- package/.github/stale.yml +0 -21
package/.github/workflows/ci.yml
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: Lock Threads
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
schedule:
|
|
5
|
+
- cron: '0 0 1 * *'
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
group: lock
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
lock-threads:
|
|
16
|
+
permissions:
|
|
17
|
+
issues: write
|
|
18
|
+
pull-requests: write
|
|
19
|
+
uses: fastify/workflows/.github/workflows/lock-threads.yml@v6
|
package/LICENSE
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2017-present The Fastify team
|
|
4
|
-
|
|
5
|
-
The Fastify team members are listed at https://github.com/fastify/fastify#team.
|
|
3
|
+
Copyright (c) 2017-present The Fastify team <https://github.com/fastify/fastify#team>
|
|
6
4
|
|
|
7
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
6
|
of this software and associated documentation files (the "Software"), to deal
|
package/index.js
CHANGED
|
@@ -2,14 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
const zlib = require('node:zlib')
|
|
4
4
|
const { inherits, format } = require('node:util')
|
|
5
|
+
const { pipeline, compose } = require('node:stream')
|
|
5
6
|
|
|
6
7
|
const fp = require('fastify-plugin')
|
|
7
8
|
const encodingNegotiator = require('@fastify/accept-negotiator')
|
|
8
|
-
const pump = require('pump')
|
|
9
9
|
const mimedb = require('mime-db')
|
|
10
10
|
const peek = require('peek-stream')
|
|
11
11
|
const { Minipass } = require('minipass')
|
|
12
|
-
const pumpify = require('pumpify')
|
|
13
12
|
const { Readable } = require('readable-stream')
|
|
14
13
|
|
|
15
14
|
const { isStream, isGzip, isDeflate, intoAsyncIterator, isWebReadableStream, isFetchResponse, webStreamToNodeReadable } = require('./lib/utils')
|
|
@@ -50,11 +49,6 @@ function fastifyCompress (fastify, opts, next) {
|
|
|
50
49
|
|
|
51
50
|
// add onSend hook onto each route as needed
|
|
52
51
|
fastify.addHook('onRoute', (routeOptions) => {
|
|
53
|
-
// If route config.compress has been set it takes precedence over compress
|
|
54
|
-
if (routeOptions.config?.compress !== undefined) {
|
|
55
|
-
routeOptions.compress = routeOptions.config.compress
|
|
56
|
-
}
|
|
57
|
-
|
|
58
52
|
// Manage compression options
|
|
59
53
|
if (routeOptions.compress !== undefined) {
|
|
60
54
|
if (typeof routeOptions.compress === 'object') {
|
|
@@ -79,11 +73,6 @@ function fastifyCompress (fastify, opts, next) {
|
|
|
79
73
|
buildRouteCompress(fastify, globalCompressParams, routeOptions, true)
|
|
80
74
|
}
|
|
81
75
|
|
|
82
|
-
// If route config.decompress has been set it takes precedence over compress
|
|
83
|
-
if (routeOptions.config?.decompress !== undefined) {
|
|
84
|
-
routeOptions.decompress = routeOptions.config.decompress
|
|
85
|
-
}
|
|
86
|
-
|
|
87
76
|
// Manage decompression options
|
|
88
77
|
if (routeOptions.decompress !== undefined) {
|
|
89
78
|
if (typeof routeOptions.decompress === 'object') {
|
|
@@ -289,7 +278,7 @@ function buildRouteCompress (_fastify, params, routeOptions, decorateOnly) {
|
|
|
289
278
|
encoding === undefined
|
|
290
279
|
? reply.removeHeader('Content-Encoding')
|
|
291
280
|
: reply.header('Content-Encoding', 'identity')
|
|
292
|
-
|
|
281
|
+
pipeline(stream, payload = unzipStream(params.uncompressStream), onEnd.bind(reply))
|
|
293
282
|
}
|
|
294
283
|
return next(null, payload)
|
|
295
284
|
}
|
|
@@ -316,7 +305,7 @@ function buildRouteCompress (_fastify, params, routeOptions, decorateOnly) {
|
|
|
316
305
|
}
|
|
317
306
|
|
|
318
307
|
stream = zipStream(params.compressStream, encoding)
|
|
319
|
-
|
|
308
|
+
pipeline(payload, stream, onEnd.bind(reply))
|
|
320
309
|
next(null, stream)
|
|
321
310
|
}
|
|
322
311
|
}
|
|
@@ -378,7 +367,10 @@ function buildRouteDecompress (_fastify, params, routeOptions) {
|
|
|
378
367
|
raw.on('data', trackEncodedLength.bind(decompresser))
|
|
379
368
|
raw.on('end', removeEncodedLengthTracking)
|
|
380
369
|
|
|
381
|
-
|
|
370
|
+
pipeline(raw, decompresser, () => {
|
|
371
|
+
// Cleanup callback - decompression errors are handled by decompresser's error handler
|
|
372
|
+
})
|
|
373
|
+
next(null, decompresser)
|
|
382
374
|
}
|
|
383
375
|
}
|
|
384
376
|
|
|
@@ -415,7 +407,7 @@ function compress (params) {
|
|
|
415
407
|
encoding === undefined
|
|
416
408
|
? this.removeHeader('Content-Encoding')
|
|
417
409
|
: this.header('Content-Encoding', 'identity')
|
|
418
|
-
|
|
410
|
+
pipeline(stream, payload = unzipStream(params.uncompressStream), onEnd.bind(this))
|
|
419
411
|
}
|
|
420
412
|
return this.send(payload)
|
|
421
413
|
}
|
|
@@ -446,8 +438,8 @@ function compress (params) {
|
|
|
446
438
|
}
|
|
447
439
|
|
|
448
440
|
stream = zipStream(params.compressStream, encoding)
|
|
449
|
-
|
|
450
|
-
this.send(stream)
|
|
441
|
+
pipeline(payload, stream, onEnd.bind(this))
|
|
442
|
+
return this.send(stream)
|
|
451
443
|
}
|
|
452
444
|
}
|
|
453
445
|
|
|
@@ -464,7 +456,13 @@ function setVaryHeader (reply) {
|
|
|
464
456
|
}
|
|
465
457
|
|
|
466
458
|
function onEnd (err) {
|
|
467
|
-
|
|
459
|
+
// Client disconnection during streaming is expected and handled by Fastify.
|
|
460
|
+
// Do not log "premature close" errors at error level since they are not
|
|
461
|
+
// actual errors - they occur when clients disconnect mid-response.
|
|
462
|
+
// See: https://github.com/fastify/fastify-compress/issues/382
|
|
463
|
+
if (err && err.message !== 'premature close') {
|
|
464
|
+
this.log.error(err)
|
|
465
|
+
}
|
|
468
466
|
}
|
|
469
467
|
|
|
470
468
|
function trackEncodedLength (chunk) {
|
|
@@ -565,8 +563,8 @@ function unzipStream (inflate, maxRecursion) {
|
|
|
565
563
|
/* c8 ignore next */
|
|
566
564
|
if (maxRecursion < 0) return swap(new Error('Maximum recursion reached'))
|
|
567
565
|
switch (isCompressed(data)) {
|
|
568
|
-
case 1: return swap(null,
|
|
569
|
-
case 2: return swap(null,
|
|
566
|
+
case 1: return swap(null, compose(inflate.gzip(), unzipStream(inflate, maxRecursion - 1)))
|
|
567
|
+
case 2: return swap(null, compose(inflate.deflate(), unzipStream(inflate, maxRecursion - 1)))
|
|
570
568
|
}
|
|
571
569
|
return swap(null, new Minipass())
|
|
572
570
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fastify/compress",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.0.0",
|
|
4
4
|
"description": "Fastify compression utils",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -11,26 +11,24 @@
|
|
|
11
11
|
"mime-db": "^1.52.0",
|
|
12
12
|
"minipass": "^7.0.4",
|
|
13
13
|
"peek-stream": "^1.1.3",
|
|
14
|
-
"pump": "^3.0.0",
|
|
15
|
-
"pumpify": "^2.0.1",
|
|
16
14
|
"readable-stream": "^4.5.2"
|
|
17
15
|
},
|
|
18
16
|
"devDependencies": {
|
|
19
|
-
"@types/node": "^
|
|
17
|
+
"@types/node": "^25.0.3",
|
|
20
18
|
"adm-zip": "^0.5.12",
|
|
21
|
-
"c8": "^
|
|
19
|
+
"c8": "^11.0.0",
|
|
22
20
|
"eslint": "^9.17.0",
|
|
23
21
|
"fastify": "^5.0.0",
|
|
24
22
|
"jsonstream": "^1.0.3",
|
|
25
|
-
"neostandard": "^0.
|
|
26
|
-
"
|
|
27
|
-
"typescript": "~
|
|
23
|
+
"neostandard": "^0.13.0",
|
|
24
|
+
"tstyche": "^7.1.0",
|
|
25
|
+
"typescript": "~6.0.2"
|
|
28
26
|
},
|
|
29
27
|
"scripts": {
|
|
30
28
|
"lint": "eslint",
|
|
31
29
|
"lint:fix": "eslint --fix",
|
|
32
30
|
"test": "npm run test:unit && npm run test:typescript",
|
|
33
|
-
"test:typescript": "
|
|
31
|
+
"test:typescript": "tstyche",
|
|
34
32
|
"test:unit": "node --test",
|
|
35
33
|
"test:coverage": "c8 node --test && c8 report --reporter=html",
|
|
36
34
|
"test:unit:verbose": "npm run test:unit -- -Rspec"
|
|
@@ -82,9 +80,6 @@
|
|
|
82
80
|
"type": "git",
|
|
83
81
|
"url": "git+https://github.com/fastify/fastify-compress.git"
|
|
84
82
|
},
|
|
85
|
-
"tsd": {
|
|
86
|
-
"directory": "types"
|
|
87
|
-
},
|
|
88
83
|
"publishConfig": {
|
|
89
84
|
"access": "public"
|
|
90
85
|
}
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
const { test, describe } = require('node:test')
|
|
4
4
|
const { createReadStream } = require('node:fs')
|
|
5
|
+
const { pipeline } = require('node:stream')
|
|
5
6
|
const path = require('node:path')
|
|
6
7
|
const zlib = require('node:zlib')
|
|
7
|
-
const pump = require('pump')
|
|
8
8
|
const Fastify = require('fastify')
|
|
9
9
|
const compressPlugin = require('../index')
|
|
10
10
|
|
|
@@ -12,7 +12,9 @@ function createPayload (compressor) {
|
|
|
12
12
|
let payload = createReadStream(path.resolve(__dirname, '../package.json'))
|
|
13
13
|
|
|
14
14
|
if (compressor) {
|
|
15
|
-
|
|
15
|
+
const compressed = compressor()
|
|
16
|
+
pipeline(payload, compressed, () => {})
|
|
17
|
+
payload = compressed
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
return payload
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test')
|
|
4
|
+
const zlib = require('node:zlib')
|
|
5
|
+
const Fastify = require('fastify')
|
|
6
|
+
const fastifyCompress = require('../..')
|
|
7
|
+
|
|
8
|
+
const largeJsonString = JSON.stringify({
|
|
9
|
+
text: 'Lorem ipsum dolor sit amet. '.repeat(2048),
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
async function routes (fastify) {
|
|
13
|
+
fastify.route({
|
|
14
|
+
method: 'GET',
|
|
15
|
+
url: '/api/compress-test',
|
|
16
|
+
handler: async function (_request, reply) {
|
|
17
|
+
reply.type('application/json')
|
|
18
|
+
return reply.compress(largeJsonString)
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test('should compress large payload without premature close', async (t) => {
|
|
24
|
+
const fastify = Fastify()
|
|
25
|
+
await fastify.register(fastifyCompress, { encodings: ['gzip'], global: true })
|
|
26
|
+
await fastify.register(routes)
|
|
27
|
+
|
|
28
|
+
const response = await fastify.inject({
|
|
29
|
+
url: '/api/compress-test',
|
|
30
|
+
method: 'GET',
|
|
31
|
+
headers: { 'accept-encoding': 'gzip' }
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
t.assert.equal(response.statusCode, 200)
|
|
35
|
+
t.assert.equal(response.headers['content-encoding'], 'gzip')
|
|
36
|
+
t.assert.ok(
|
|
37
|
+
response.rawPayload.length > 0,
|
|
38
|
+
`Expected compressed response body, got ${response.rawPayload.length} bytes`
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const decompressed = zlib.gunzipSync(response.rawPayload)
|
|
42
|
+
t.assert.equal(decompressed.toString('utf-8'), largeJsonString)
|
|
43
|
+
})
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test')
|
|
4
|
+
const { setTimeout: sleep } = require('node:timers/promises')
|
|
5
|
+
const net = require('node:net')
|
|
6
|
+
const { Writable } = require('node:stream')
|
|
7
|
+
const Fastify = require('fastify')
|
|
8
|
+
const fastifyCompress = require('../..')
|
|
9
|
+
|
|
10
|
+
test('should not log "premature close" errors on client disconnect (global compression)', async (t) => {
|
|
11
|
+
let prematureCloseCount = 0
|
|
12
|
+
|
|
13
|
+
const fastify = Fastify({
|
|
14
|
+
logger: {
|
|
15
|
+
level: 'error',
|
|
16
|
+
stream: new Writable({
|
|
17
|
+
write (chunk, encoding, callback) {
|
|
18
|
+
if (chunk.toString().includes('premature close')) {
|
|
19
|
+
prematureCloseCount++
|
|
20
|
+
}
|
|
21
|
+
callback()
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
await fastify.register(fastifyCompress, {
|
|
28
|
+
global: true,
|
|
29
|
+
encodings: ['gzip'],
|
|
30
|
+
threshold: 1024
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// Streaming endpoint that takes time to produce data
|
|
34
|
+
fastify.get('/stream', async (_request, reply) => {
|
|
35
|
+
let chunks = 0
|
|
36
|
+
const stream = new (require('node:stream').Readable)({
|
|
37
|
+
read () {
|
|
38
|
+
if (chunks >= 20) {
|
|
39
|
+
this.push(null)
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
setTimeout(() => {
|
|
43
|
+
this.push(JSON.stringify({ id: chunks, data: 'x'.repeat(1000) }) + '\n')
|
|
44
|
+
chunks++
|
|
45
|
+
}, 50)
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
reply.type('text/plain')
|
|
49
|
+
return stream
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
await fastify.listen({ port: 0 })
|
|
53
|
+
const port = fastify.server.address().port
|
|
54
|
+
|
|
55
|
+
// Simulate clients that disconnect before the full response is sent
|
|
56
|
+
for (let i = 0; i < 10; i++) {
|
|
57
|
+
await new Promise((_resolve) => {
|
|
58
|
+
const sock = net.connect(port, '127.0.0.1', () => {
|
|
59
|
+
sock.write('GET /stream HTTP/1.1\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n')
|
|
60
|
+
// Disconnect after receiving partial response (100ms)
|
|
61
|
+
setTimeout(() => {
|
|
62
|
+
sock.destroy()
|
|
63
|
+
_resolve()
|
|
64
|
+
}, 100)
|
|
65
|
+
})
|
|
66
|
+
sock.on('error', () => _resolve())
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Allow pending callbacks to fire
|
|
71
|
+
await sleep(2000)
|
|
72
|
+
await fastify.close()
|
|
73
|
+
|
|
74
|
+
t.assert.equal(
|
|
75
|
+
prematureCloseCount,
|
|
76
|
+
0,
|
|
77
|
+
`Expected no "premature close" errors, but got ${prematureCloseCount} errors from 10 client disconnects`
|
|
78
|
+
)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('should not log "premature close" errors on client disconnect (reply.compress path)', async (t) => {
|
|
82
|
+
let prematureCloseCount = 0
|
|
83
|
+
|
|
84
|
+
const fastify = Fastify({
|
|
85
|
+
logger: {
|
|
86
|
+
level: 'error',
|
|
87
|
+
stream: new Writable({
|
|
88
|
+
write (chunk, encoding, callback) {
|
|
89
|
+
if (chunk.toString().includes('premature close')) {
|
|
90
|
+
prematureCloseCount++
|
|
91
|
+
}
|
|
92
|
+
callback()
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
await fastify.register(fastifyCompress, {
|
|
99
|
+
global: false,
|
|
100
|
+
encodings: ['gzip']
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// Streaming endpoint using reply.compress
|
|
104
|
+
fastify.get('/stream', async (_request, reply) => {
|
|
105
|
+
let chunks = 0
|
|
106
|
+
const stream = new (require('node:stream').Readable)({
|
|
107
|
+
read () {
|
|
108
|
+
if (chunks >= 20) {
|
|
109
|
+
this.push(null)
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
setTimeout(() => {
|
|
113
|
+
this.push(JSON.stringify({ id: chunks, data: 'x'.repeat(1000) }) + '\n')
|
|
114
|
+
chunks++
|
|
115
|
+
}, 50)
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
reply.type('text/plain')
|
|
119
|
+
return reply.compress(stream)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
await fastify.listen({ port: 0 })
|
|
123
|
+
const port = fastify.server.address().port
|
|
124
|
+
|
|
125
|
+
// Simulate clients that disconnect before the full response is sent
|
|
126
|
+
for (let i = 0; i < 10; i++) {
|
|
127
|
+
await new Promise((_resolve) => {
|
|
128
|
+
const sock = net.connect(port, '127.0.0.1', () => {
|
|
129
|
+
sock.write('GET /stream HTTP/1.1\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n')
|
|
130
|
+
// Disconnect after receiving partial response (100ms)
|
|
131
|
+
setTimeout(() => {
|
|
132
|
+
sock.destroy()
|
|
133
|
+
_resolve()
|
|
134
|
+
}, 100)
|
|
135
|
+
})
|
|
136
|
+
sock.on('error', () => _resolve())
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Allow pending callbacks to fire
|
|
141
|
+
await sleep(2000)
|
|
142
|
+
await fastify.close()
|
|
143
|
+
|
|
144
|
+
t.assert.equal(
|
|
145
|
+
prematureCloseCount,
|
|
146
|
+
0,
|
|
147
|
+
`Expected no "premature close" errors, but got ${prematureCloseCount} errors from 10 client disconnects`
|
|
148
|
+
)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test('should still log actual errors (not premature close)', async (t) => {
|
|
152
|
+
const errors = []
|
|
153
|
+
|
|
154
|
+
const fastify = Fastify({
|
|
155
|
+
logger: {
|
|
156
|
+
level: 'error',
|
|
157
|
+
stream: new Writable({
|
|
158
|
+
write (chunk, encoding, callback) {
|
|
159
|
+
const msg = chunk.toString()
|
|
160
|
+
if (msg.includes('premature close')) {
|
|
161
|
+
// Ignore premature close errors
|
|
162
|
+
} else {
|
|
163
|
+
errors.push(msg)
|
|
164
|
+
}
|
|
165
|
+
callback()
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
await fastify.register(fastifyCompress, {
|
|
172
|
+
global: true,
|
|
173
|
+
encodings: ['gzip']
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
// Route that throws an error
|
|
177
|
+
fastify.get('/error', async () => {
|
|
178
|
+
throw new Error('Test error')
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
await fastify.inject({
|
|
182
|
+
method: 'GET',
|
|
183
|
+
url: '/error',
|
|
184
|
+
headers: { 'accept-encoding': 'gzip' }
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
await sleep(500)
|
|
188
|
+
|
|
189
|
+
t.assert.ok(
|
|
190
|
+
errors.length > 0,
|
|
191
|
+
'Expected actual errors to still be logged'
|
|
192
|
+
)
|
|
193
|
+
})
|
|
@@ -330,95 +330,6 @@ describe('When `compress.removeContentLengthHeader` is `false`, it should not re
|
|
|
330
330
|
})
|
|
331
331
|
})
|
|
332
332
|
|
|
333
|
-
describe('When using the old routes `{ config: compress }` option :', async () => {
|
|
334
|
-
test('it should compress data using the route custom provided `createGzip` method', async (t) => {
|
|
335
|
-
t.plan(10)
|
|
336
|
-
const equal = t.assert.equal
|
|
337
|
-
|
|
338
|
-
let usedCustomGlobal = false
|
|
339
|
-
let usedCustom = false
|
|
340
|
-
const customZlibGlobal = { createGzip: () => (usedCustomGlobal = true) && zlib.createGzip() }
|
|
341
|
-
const customZlib = { createGzip: () => (usedCustom = true) && zlib.createGzip() }
|
|
342
|
-
|
|
343
|
-
const fastify = Fastify()
|
|
344
|
-
await fastify.register(compressPlugin, { global: false, zlib: customZlibGlobal })
|
|
345
|
-
|
|
346
|
-
fastify.get('/', (_request, reply) => {
|
|
347
|
-
reply
|
|
348
|
-
.type('text/plain')
|
|
349
|
-
.compress(createReadStream('./package.json'))
|
|
350
|
-
})
|
|
351
|
-
|
|
352
|
-
fastify.get('/custom', {
|
|
353
|
-
config: {
|
|
354
|
-
compress: { zlib: customZlib }
|
|
355
|
-
}
|
|
356
|
-
}, (_request, reply) => {
|
|
357
|
-
reply
|
|
358
|
-
.type('text/plain')
|
|
359
|
-
.compress(createReadStream('./package.json'))
|
|
360
|
-
})
|
|
361
|
-
|
|
362
|
-
await fastify.inject({
|
|
363
|
-
url: '/',
|
|
364
|
-
method: 'GET',
|
|
365
|
-
headers: {
|
|
366
|
-
'accept-encoding': 'gzip'
|
|
367
|
-
}
|
|
368
|
-
}).then((response) => {
|
|
369
|
-
equal(usedCustom, false)
|
|
370
|
-
equal(usedCustomGlobal, true)
|
|
371
|
-
|
|
372
|
-
const file = readFileSync('./package.json', 'utf8')
|
|
373
|
-
const payload = zlib.gunzipSync(response.rawPayload)
|
|
374
|
-
equal(response.headers.vary, 'accept-encoding')
|
|
375
|
-
equal(response.headers['content-encoding'], 'gzip')
|
|
376
|
-
equal(payload.toString('utf-8'), file)
|
|
377
|
-
|
|
378
|
-
usedCustom = false
|
|
379
|
-
usedCustomGlobal = false
|
|
380
|
-
})
|
|
381
|
-
|
|
382
|
-
const response = await fastify.inject({
|
|
383
|
-
url: '/custom',
|
|
384
|
-
method: 'GET',
|
|
385
|
-
headers: {
|
|
386
|
-
'accept-encoding': 'gzip'
|
|
387
|
-
}
|
|
388
|
-
})
|
|
389
|
-
equal(usedCustom, true)
|
|
390
|
-
equal(usedCustomGlobal, false)
|
|
391
|
-
|
|
392
|
-
const file = readFileSync('./package.json', 'utf8')
|
|
393
|
-
const payload = zlib.gunzipSync(response.rawPayload)
|
|
394
|
-
equal(response.headers.vary, 'accept-encoding')
|
|
395
|
-
equal(response.headers['content-encoding'], 'gzip')
|
|
396
|
-
equal(payload.toString('utf-8'), file)
|
|
397
|
-
})
|
|
398
|
-
|
|
399
|
-
test('it should use the old routes `{ config: compress }` options over routes `compress` options', async (t) => {
|
|
400
|
-
t.plan(1)
|
|
401
|
-
|
|
402
|
-
const fastify = Fastify()
|
|
403
|
-
await fastify.register(compressPlugin, { global: false })
|
|
404
|
-
|
|
405
|
-
try {
|
|
406
|
-
fastify.get('/', {
|
|
407
|
-
compress: {
|
|
408
|
-
zlib: { createGzip: () => zlib.createGzip() }
|
|
409
|
-
},
|
|
410
|
-
config: {
|
|
411
|
-
compress: 'bad config'
|
|
412
|
-
}
|
|
413
|
-
}, (_request, reply) => {
|
|
414
|
-
reply.send('')
|
|
415
|
-
})
|
|
416
|
-
} catch (err) {
|
|
417
|
-
t.assert.equal(err.message, 'Unknown value for route compress configuration')
|
|
418
|
-
}
|
|
419
|
-
})
|
|
420
|
-
})
|
|
421
|
-
|
|
422
333
|
test('It should avoid to trigger `onSend` hook twice', async (t) => {
|
|
423
334
|
t.plan(1)
|
|
424
335
|
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
const { test, describe } = require('node:test')
|
|
4
4
|
const { createReadStream } = require('node:fs')
|
|
5
|
+
const { pipeline } = require('node:stream')
|
|
5
6
|
const path = require('node:path')
|
|
6
7
|
const zlib = require('node:zlib')
|
|
7
|
-
const pump = require('pump')
|
|
8
8
|
const Fastify = require('fastify')
|
|
9
9
|
const compressPlugin = require('../index')
|
|
10
10
|
|
|
@@ -12,7 +12,9 @@ function createPayload (compressor) {
|
|
|
12
12
|
let payload = createReadStream(path.resolve(__dirname, '../package.json'))
|
|
13
13
|
|
|
14
14
|
if (compressor) {
|
|
15
|
-
|
|
15
|
+
const compressed = compressor()
|
|
16
|
+
pipeline(payload, compressed, () => {})
|
|
17
|
+
payload = compressed
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
return payload
|
|
@@ -231,88 +233,3 @@ describe('When using routes `decompress` settings :', async () => {
|
|
|
231
233
|
}
|
|
232
234
|
})
|
|
233
235
|
})
|
|
234
|
-
|
|
235
|
-
describe('When using the old routes `{ config: decompress }` option :', async () => {
|
|
236
|
-
test('it should decompress data using the route custom provided `createGunzip` method', async (t) => {
|
|
237
|
-
t.plan(8)
|
|
238
|
-
const equal = t.assert.equal
|
|
239
|
-
|
|
240
|
-
let usedCustomGlobal = false
|
|
241
|
-
let usedCustom = false
|
|
242
|
-
const customZlibGlobal = { createGunzip: () => (usedCustomGlobal = true) && zlib.createGunzip() }
|
|
243
|
-
const customZlib = { createGunzip: () => (usedCustom = true) && zlib.createGunzip() }
|
|
244
|
-
|
|
245
|
-
const fastify = Fastify()
|
|
246
|
-
await fastify.register(compressPlugin, { zlib: customZlibGlobal })
|
|
247
|
-
|
|
248
|
-
fastify.post('/', (request, reply) => {
|
|
249
|
-
reply.send(request.body.name)
|
|
250
|
-
})
|
|
251
|
-
|
|
252
|
-
fastify.post('/custom', {
|
|
253
|
-
config: {
|
|
254
|
-
decompress: {
|
|
255
|
-
zlib: customZlib
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}, (request, reply) => {
|
|
259
|
-
reply.send(request.body.name)
|
|
260
|
-
})
|
|
261
|
-
|
|
262
|
-
await fastify.inject({
|
|
263
|
-
url: '/',
|
|
264
|
-
method: 'POST',
|
|
265
|
-
headers: {
|
|
266
|
-
'content-type': 'application/json',
|
|
267
|
-
'content-encoding': 'gzip'
|
|
268
|
-
},
|
|
269
|
-
payload: createPayload(zlib.createGzip)
|
|
270
|
-
}).then((response) => {
|
|
271
|
-
equal(usedCustom, false)
|
|
272
|
-
equal(usedCustomGlobal, true)
|
|
273
|
-
|
|
274
|
-
equal(response.statusCode, 200)
|
|
275
|
-
equal(response.body, '@fastify/compress')
|
|
276
|
-
|
|
277
|
-
usedCustom = false
|
|
278
|
-
usedCustomGlobal = false
|
|
279
|
-
})
|
|
280
|
-
|
|
281
|
-
const response = await fastify.inject({
|
|
282
|
-
url: '/custom',
|
|
283
|
-
method: 'POST',
|
|
284
|
-
headers: {
|
|
285
|
-
'content-type': 'application/json',
|
|
286
|
-
'content-encoding': 'gzip'
|
|
287
|
-
},
|
|
288
|
-
payload: createPayload(zlib.createGzip)
|
|
289
|
-
})
|
|
290
|
-
equal(usedCustom, true)
|
|
291
|
-
equal(usedCustomGlobal, false)
|
|
292
|
-
|
|
293
|
-
equal(response.statusCode, 200)
|
|
294
|
-
equal(response.body, '@fastify/compress')
|
|
295
|
-
})
|
|
296
|
-
|
|
297
|
-
test('it should use the old routes `{ config: decompress }` options over routes `decompress` options', async (t) => {
|
|
298
|
-
t.plan(1)
|
|
299
|
-
|
|
300
|
-
const fastify = Fastify()
|
|
301
|
-
await fastify.register(compressPlugin, { global: false })
|
|
302
|
-
|
|
303
|
-
try {
|
|
304
|
-
fastify.post('/', {
|
|
305
|
-
decompress: {
|
|
306
|
-
zlib: { createGunzip: () => zlib.createGunzip() }
|
|
307
|
-
},
|
|
308
|
-
config: {
|
|
309
|
-
decompress: 'bad config'
|
|
310
|
-
}
|
|
311
|
-
}, (request, reply) => {
|
|
312
|
-
reply.send(request.body.name)
|
|
313
|
-
})
|
|
314
|
-
} catch (err) {
|
|
315
|
-
t.assert.equal(err.message, 'Unknown value for route decompress configuration')
|
|
316
|
-
}
|
|
317
|
-
})
|
|
318
|
-
})
|
package/types/index.d.ts
CHANGED
|
@@ -10,13 +10,6 @@ import { Stream } from 'node:stream'
|
|
|
10
10
|
import { BrotliOptions, ZlibOptions } from 'node:zlib'
|
|
11
11
|
|
|
12
12
|
declare module 'fastify' {
|
|
13
|
-
export interface FastifyContextConfig {
|
|
14
|
-
/** @deprecated `config.compress` is deprecated, use `compress` shorthand option instead */
|
|
15
|
-
compress?: RouteCompressOptions | false;
|
|
16
|
-
/** @deprecated `config.decompress` is deprecated, use `decompress` shorthand option instead */
|
|
17
|
-
decompress?: RouteDecompressOptions | false;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
13
|
export interface RouteShorthandOptions<
|
|
21
14
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
22
15
|
RawServer extends RawServerBase = RawServerDefault
|
|
@@ -97,13 +90,6 @@ declare namespace fastifyCompress {
|
|
|
97
90
|
|
|
98
91
|
export interface RouteOptions extends FastifyRouteOptions, FastifyCompressRouteOptions { }
|
|
99
92
|
|
|
100
|
-
export interface RoutesConfigCompressOptions {
|
|
101
|
-
/** @deprecated `config.compress` is deprecated, use `compress` shorthand option instead */
|
|
102
|
-
compress?: RouteCompressOptions | false;
|
|
103
|
-
/** @deprecated `config.decompress` is deprecated, use `decompress` shorthand option instead */
|
|
104
|
-
decompress?: RouteDecompressOptions | false;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
93
|
export const fastifyCompress: FastifyCompress
|
|
108
94
|
export { fastifyCompress as default }
|
|
109
95
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fastify, { FastifyInstance } from 'fastify'
|
|
2
2
|
import { createReadStream } from 'node:fs'
|
|
3
|
-
import {
|
|
3
|
+
import { expect } from 'tstyche'
|
|
4
4
|
import * as zlib from 'node:zlib'
|
|
5
5
|
import fastifyCompress, { FastifyCompressOptions } from '..'
|
|
6
6
|
|
|
@@ -39,11 +39,11 @@ app.register(fastifyCompress, {
|
|
|
39
39
|
})
|
|
40
40
|
|
|
41
41
|
app.get('/test-one', async (_request, reply) => {
|
|
42
|
-
|
|
42
|
+
expect(reply.compress(stream)).type.toBe<void>()
|
|
43
43
|
})
|
|
44
44
|
|
|
45
45
|
app.get('/test-two', async (_request, reply) => {
|
|
46
|
-
|
|
46
|
+
expect(reply.compress).type.not.toBeCallableWith()
|
|
47
47
|
})
|
|
48
48
|
|
|
49
49
|
// Instantiation of an app without global
|
|
@@ -64,7 +64,7 @@ appWithoutGlobal.get('/one', {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
}, (_request, reply) => {
|
|
67
|
-
|
|
67
|
+
expect(reply.type('text/plain').compress(stream)).type.toBe<void>()
|
|
68
68
|
})
|
|
69
69
|
|
|
70
70
|
appWithoutGlobal.get('/two', {
|
|
@@ -82,27 +82,27 @@ appWithoutGlobal.get('/two', {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
}, (_request, reply) => {
|
|
85
|
-
|
|
85
|
+
expect(reply.type('text/plain').compress(stream)).type.toBe<void>()
|
|
86
86
|
})
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
)
|
|
88
|
+
appWithoutGlobal.get('/throw-a-ts-arg-error-on-shorthand-route', {
|
|
89
|
+
// @ts-expect-error Type 'string' is not assignable
|
|
90
|
+
compress: 'bad compress route option value',
|
|
91
|
+
// @ts-expect-error Type 'string' is not assignable
|
|
92
|
+
decompress: 'bad decompress route option value'
|
|
93
|
+
}, (_request, reply) => {
|
|
94
|
+
reply.type('text/plain').compress(stream)
|
|
95
|
+
})
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
)
|
|
97
|
+
appWithoutGlobal.route({
|
|
98
|
+
method: 'GET',
|
|
99
|
+
path: '/throw-a-ts-arg-error',
|
|
100
|
+
// @ts-expect-error Type 'string' is not assignable
|
|
101
|
+
compress: 'bad compress route option value',
|
|
102
|
+
// @ts-expect-error Type 'string' is not assignable
|
|
103
|
+
decompress: 'bad decompress route option value',
|
|
104
|
+
handler: (_request, reply) => { reply.type('text/plain').compress(stream) }
|
|
105
|
+
})
|
|
106
106
|
|
|
107
107
|
appWithoutGlobal.inject(
|
|
108
108
|
{
|
|
@@ -113,29 +113,29 @@ appWithoutGlobal.inject(
|
|
|
113
113
|
}
|
|
114
114
|
},
|
|
115
115
|
(err) => {
|
|
116
|
-
|
|
116
|
+
expect(err).type.toBe<Error | undefined>()
|
|
117
117
|
}
|
|
118
118
|
)
|
|
119
119
|
|
|
120
120
|
// Test that invalid encoding values trigger TypeScript errors
|
|
121
|
-
|
|
121
|
+
expect(fastify().register).type.not.toBeCallableWith(fastifyCompress, {
|
|
122
122
|
encodings: ['invalid-encoding']
|
|
123
|
-
})
|
|
123
|
+
})
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
expect(fastify().register).type.not.toBeCallableWith(fastifyCompress, {
|
|
126
126
|
requestEncodings: ['another-invalid-encoding']
|
|
127
|
-
})
|
|
127
|
+
})
|
|
128
128
|
|
|
129
129
|
// Instantiation of an app that should trigger a typescript error
|
|
130
130
|
const appThatTriggerAnError = fastify()
|
|
131
|
-
|
|
131
|
+
expect(appThatTriggerAnError.register).type.not.toBeCallableWith(fastifyCompress, {
|
|
132
132
|
global: true,
|
|
133
133
|
thisOptionDoesNotExist: 'trigger a typescript error'
|
|
134
|
-
})
|
|
134
|
+
})
|
|
135
135
|
|
|
136
136
|
app.get('/ts-fetch-response', async (_request, reply) => {
|
|
137
137
|
const resp = new Response('ok', { headers: { 'content-type': 'text/plain' } })
|
|
138
|
-
|
|
138
|
+
expect(reply.compress(resp)).type.toBe<void>()
|
|
139
139
|
})
|
|
140
140
|
|
|
141
141
|
app.get('/ts-web-readable-stream', async (_request, reply) => {
|
|
@@ -145,5 +145,5 @@ app.get('/ts-web-readable-stream', async (_request, reply) => {
|
|
|
145
145
|
controller.close()
|
|
146
146
|
}
|
|
147
147
|
})
|
|
148
|
-
|
|
148
|
+
expect(reply.compress(stream)).type.toBe<void>()
|
|
149
149
|
})
|
package/.github/stale.yml
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# Number of days of inactivity before an issue becomes stale
|
|
2
|
-
daysUntilStale: 15
|
|
3
|
-
# Number of days of inactivity before a stale issue is closed
|
|
4
|
-
daysUntilClose: 7
|
|
5
|
-
# Issues with these labels will never be considered stale
|
|
6
|
-
exemptLabels:
|
|
7
|
-
- "discussion"
|
|
8
|
-
- "feature request"
|
|
9
|
-
- "bug"
|
|
10
|
-
- "help wanted"
|
|
11
|
-
- "plugin suggestion"
|
|
12
|
-
- "good first issue"
|
|
13
|
-
# Label to use when marking an issue as stale
|
|
14
|
-
staleLabel: stale
|
|
15
|
-
# Comment to post when marking an issue as stale. Set to `false` to disable
|
|
16
|
-
markComment: >
|
|
17
|
-
This issue has been automatically marked as stale because it has not had
|
|
18
|
-
recent activity. It will be closed if no further activity occurs. Thank you
|
|
19
|
-
for your contributions.
|
|
20
|
-
# Comment to post when closing a stale issue. Set to `false` to disable
|
|
21
|
-
closeComment: false
|