@fastify/compress 8.3.1 → 9.1.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/.c8rc +2 -6
- package/.github/dependabot.yml +42 -2
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/lock-threads.yml +19 -0
- package/LICENSE +1 -3
- package/README.md +2 -3
- package/index.js +42 -29
- package/lib/utils.js +90 -1
- package/package.json +9 -15
- package/test/global-compress.test.js +119 -0
- package/test/global-decompress.test.js +4 -2
- package/test/regression/issue-382.test.js +193 -0
- package/test/regression/issue-410.test.js +163 -0
- package/test/routes-compress.test.js +0 -89
- package/test/routes-decompress.test.js +4 -87
- package/test/utils.test.js +138 -1
- package/types/index.d.ts +0 -14
- package/types/{index.test-d.ts → index.tst.ts} +31 -31
- package/.github/stale.yml +0 -21
|
@@ -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 (/premature close/i.test(chunk.toString())) {
|
|
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 (/premature close/i.test(chunk.toString())) {
|
|
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 (/premature close/i.test(msg)) {
|
|
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
|
+
})
|
|
@@ -0,0 +1,163 @@
|
|
|
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
|
+
// Node's native stream.pipeline emits ERR_STREAM_PREMATURE_CLOSE whose message
|
|
11
|
+
// is 'Premature close' (capital P), unlike the legacy pump/end-of-stream
|
|
12
|
+
// message 'premature close'. The onEnd guard must suppress both variants.
|
|
13
|
+
// See: https://github.com/fastify/fastify-compress/issues/410
|
|
14
|
+
|
|
15
|
+
function buildFastify (onErrorLog) {
|
|
16
|
+
return Fastify({
|
|
17
|
+
logger: {
|
|
18
|
+
level: 'error',
|
|
19
|
+
stream: new Writable({
|
|
20
|
+
write (chunk, encoding, callback) {
|
|
21
|
+
onErrorLog(chunk.toString())
|
|
22
|
+
callback()
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function streamRoute (reply, ReadableCtor) {
|
|
30
|
+
let chunks = 0
|
|
31
|
+
const stream = new ReadableCtor({
|
|
32
|
+
read () {
|
|
33
|
+
if (chunks >= 20) {
|
|
34
|
+
this.push(null)
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
setTimeout(() => {
|
|
38
|
+
this.push(JSON.stringify({ id: chunks, data: 'x'.repeat(1000) }) + '\n')
|
|
39
|
+
chunks++
|
|
40
|
+
}, 50)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
reply.type('text/plain')
|
|
44
|
+
return stream
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function abortClients (port) {
|
|
48
|
+
// Simulate clients that disconnect before the full response is sent
|
|
49
|
+
for (let i = 0; i < 10; i++) {
|
|
50
|
+
await new Promise((_resolve) => {
|
|
51
|
+
const sock = net.connect(port, '127.0.0.1', () => {
|
|
52
|
+
sock.write('GET /stream HTTP/1.1\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n')
|
|
53
|
+
// Disconnect after receiving partial response (100ms)
|
|
54
|
+
setTimeout(() => {
|
|
55
|
+
sock.destroy()
|
|
56
|
+
_resolve()
|
|
57
|
+
}, 100)
|
|
58
|
+
})
|
|
59
|
+
sock.on('error', () => _resolve())
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
test('should not log native pipeline "Premature close" (ERR_STREAM_PREMATURE_CLOSE) on client disconnect (global compression)', async (t) => {
|
|
65
|
+
let prematureCloseCount = 0
|
|
66
|
+
|
|
67
|
+
const fastify = buildFastify((msg) => {
|
|
68
|
+
if (/premature close/i.test(msg)) {
|
|
69
|
+
prematureCloseCount++
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
await fastify.register(fastifyCompress, {
|
|
74
|
+
global: true,
|
|
75
|
+
encodings: ['gzip'],
|
|
76
|
+
threshold: 1024
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
fastify.get('/stream', async (_request, reply) => {
|
|
80
|
+
return streamRoute(reply, require('node:stream').Readable)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
await fastify.listen({ port: 0 })
|
|
84
|
+
const port = fastify.server.address().port
|
|
85
|
+
|
|
86
|
+
await abortClients(port)
|
|
87
|
+
|
|
88
|
+
// Allow pending callbacks to fire
|
|
89
|
+
await sleep(2000)
|
|
90
|
+
await fastify.close()
|
|
91
|
+
|
|
92
|
+
t.assert.equal(
|
|
93
|
+
prematureCloseCount,
|
|
94
|
+
0,
|
|
95
|
+
`Expected no premature close errors (any casing), but got ${prematureCloseCount} errors from 10 client disconnects`
|
|
96
|
+
)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('should not log native pipeline "Premature close" (ERR_STREAM_PREMATURE_CLOSE) on client disconnect (reply.compress path)', async (t) => {
|
|
100
|
+
let prematureCloseCount = 0
|
|
101
|
+
|
|
102
|
+
const fastify = buildFastify((msg) => {
|
|
103
|
+
if (/premature close/i.test(msg)) {
|
|
104
|
+
prematureCloseCount++
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
await fastify.register(fastifyCompress, {
|
|
109
|
+
global: false,
|
|
110
|
+
encodings: ['gzip']
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
fastify.get('/stream', async (_request, reply) => {
|
|
114
|
+
return reply.compress(streamRoute(reply, require('node:stream').Readable))
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
await fastify.listen({ port: 0 })
|
|
118
|
+
const port = fastify.server.address().port
|
|
119
|
+
|
|
120
|
+
await abortClients(port)
|
|
121
|
+
|
|
122
|
+
// Allow pending callbacks to fire
|
|
123
|
+
await sleep(2000)
|
|
124
|
+
await fastify.close()
|
|
125
|
+
|
|
126
|
+
t.assert.equal(
|
|
127
|
+
prematureCloseCount,
|
|
128
|
+
0,
|
|
129
|
+
`Expected no premature close errors (any casing), but got ${prematureCloseCount} errors from 10 client disconnects`
|
|
130
|
+
)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('should still log errors that are not premature close', async (t) => {
|
|
134
|
+
const errors = []
|
|
135
|
+
|
|
136
|
+
const fastify = buildFastify((msg) => {
|
|
137
|
+
if (!/premature close/i.test(msg)) {
|
|
138
|
+
errors.push(msg)
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
await fastify.register(fastifyCompress, {
|
|
143
|
+
global: true,
|
|
144
|
+
encodings: ['gzip']
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
fastify.get('/error', async () => {
|
|
148
|
+
throw new Error('Test error')
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
await fastify.inject({
|
|
152
|
+
method: 'GET',
|
|
153
|
+
url: '/error',
|
|
154
|
+
headers: { 'accept-encoding': 'gzip' }
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
await sleep(500)
|
|
158
|
+
|
|
159
|
+
t.assert.ok(
|
|
160
|
+
errors.length > 0,
|
|
161
|
+
'Expected actual errors to still be logged'
|
|
162
|
+
)
|
|
163
|
+
})
|
|
@@ -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/test/utils.test.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
const { createReadStream } = require('node:fs')
|
|
4
4
|
const { Socket } = require('node:net')
|
|
5
5
|
const { Duplex, PassThrough, Readable, Stream, Transform, Writable } = require('node:stream')
|
|
6
|
+
const { finished } = require('node:stream/promises')
|
|
6
7
|
const { test } = require('node:test')
|
|
7
|
-
const { isStream, isZstd, isDeflate, isGzip, intoAsyncIterator } = require('../lib/utils')
|
|
8
|
+
const { isStream, isZstd, isDeflate, isGzip, intoAsyncIterator, createPeekTransform } = require('../lib/utils')
|
|
8
9
|
|
|
9
10
|
test('isStream() utility should be able to detect Streams', async (t) => {
|
|
10
11
|
t.plan(12)
|
|
@@ -124,3 +125,139 @@ test('intoAsyncIterator() utility should handle different data', async (t) => {
|
|
|
124
125
|
equal(chunk, obj)
|
|
125
126
|
}
|
|
126
127
|
})
|
|
128
|
+
|
|
129
|
+
test('createPeekTransform', async (t) => {
|
|
130
|
+
class UpperCaseTransform extends Transform {
|
|
131
|
+
_transform (chunk, encoding, callback) {
|
|
132
|
+
callback(null, chunk.toString().toUpperCase())
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await t.test('uppercase', async (t) => {
|
|
137
|
+
t.plan(2)
|
|
138
|
+
|
|
139
|
+
const transform = createPeekTransform(function (data) {
|
|
140
|
+
t.assert.strictEqual(data.toString(), 'hello\nworl')
|
|
141
|
+
return new UpperCaseTransform()
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
let data = ''
|
|
145
|
+
transform.on('data', (chunk) => { data += chunk.toString() })
|
|
146
|
+
transform.write('hello\n')
|
|
147
|
+
transform.write('world\n')
|
|
148
|
+
transform.end()
|
|
149
|
+
|
|
150
|
+
await finished(transform)
|
|
151
|
+
|
|
152
|
+
t.assert.strictEqual(data, 'HELLO\nWORLD\n')
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
await t.test('uppercase no newline', async (t) => {
|
|
156
|
+
t.plan(2)
|
|
157
|
+
|
|
158
|
+
const transform = createPeekTransform(function (data) {
|
|
159
|
+
t.assert.strictEqual(data.toString(), 'helloworld')
|
|
160
|
+
return new UpperCaseTransform()
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
let data = ''
|
|
164
|
+
transform.on('data', (chunk) => { data += chunk.toString() })
|
|
165
|
+
transform.write('hello')
|
|
166
|
+
transform.write('world')
|
|
167
|
+
transform.end()
|
|
168
|
+
|
|
169
|
+
await finished(transform)
|
|
170
|
+
|
|
171
|
+
t.assert.strictEqual(data, 'HELLOWORLD')
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
await t.test('error', async (t) => {
|
|
175
|
+
t.plan(2)
|
|
176
|
+
|
|
177
|
+
const transform = createPeekTransform(function (data) {
|
|
178
|
+
throw new Error('boom')
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
transform.on('data', (chunk) => { t.assert.fail('should not reach') })
|
|
182
|
+
transform.on('error', (error) => { t.assert.ok(error) })
|
|
183
|
+
transform.write('hello')
|
|
184
|
+
transform.write('world')
|
|
185
|
+
transform.end()
|
|
186
|
+
|
|
187
|
+
await t.assert.rejects(finished(transform), new Error('boom'))
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
await t.test('peekSize = 5', async (t) => {
|
|
191
|
+
t.plan(2)
|
|
192
|
+
|
|
193
|
+
const transform = createPeekTransform(function (data) {
|
|
194
|
+
t.assert.strictEqual(data.toString(), 'hello')
|
|
195
|
+
return new UpperCaseTransform()
|
|
196
|
+
}, 5)
|
|
197
|
+
|
|
198
|
+
let data = ''
|
|
199
|
+
transform.on('data', (chunk) => { data += chunk.toString() })
|
|
200
|
+
transform.write('hello\n')
|
|
201
|
+
transform.write('world\n')
|
|
202
|
+
transform.end()
|
|
203
|
+
|
|
204
|
+
await finished(transform)
|
|
205
|
+
|
|
206
|
+
t.assert.strictEqual(data, 'HELLO\nWORLD\n')
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
await t.test('data size < peekSize', async (t) => {
|
|
210
|
+
t.plan(2)
|
|
211
|
+
|
|
212
|
+
const transform = createPeekTransform(function (data) {
|
|
213
|
+
t.assert.strictEqual(data.toString(), 'helloworld')
|
|
214
|
+
return new UpperCaseTransform()
|
|
215
|
+
}, 100)
|
|
216
|
+
|
|
217
|
+
let data = ''
|
|
218
|
+
transform.on('data', (chunk) => { data += chunk.toString() })
|
|
219
|
+
transform.write('hello')
|
|
220
|
+
transform.write('world')
|
|
221
|
+
transform.end()
|
|
222
|
+
|
|
223
|
+
await finished(transform)
|
|
224
|
+
|
|
225
|
+
t.assert.strictEqual(data, 'HELLOWORLD')
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
await t.test('error when data size < peekSize', async (t) => {
|
|
229
|
+
t.plan(2)
|
|
230
|
+
|
|
231
|
+
const transform = createPeekTransform(function (data) {
|
|
232
|
+
throw new Error('boom')
|
|
233
|
+
}, 100)
|
|
234
|
+
|
|
235
|
+
transform.on('data', (chunk) => { t.assert.fail('should not reach') })
|
|
236
|
+
transform.on('error', (error) => { t.assert.ok(error) })
|
|
237
|
+
transform.write('hello')
|
|
238
|
+
transform.write('world')
|
|
239
|
+
transform.end()
|
|
240
|
+
|
|
241
|
+
await t.assert.rejects(finished(transform), new Error('boom'))
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
await t.test('error inside downstream', async (t) => {
|
|
245
|
+
t.plan(2)
|
|
246
|
+
|
|
247
|
+
const transform = createPeekTransform(function (data) {
|
|
248
|
+
return new Transform({
|
|
249
|
+
transform (chunk, encoding, callback) {
|
|
250
|
+
callback(new Error('boom'))
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
transform.on('data', (chunk) => { t.assert.fail('should not reach') })
|
|
256
|
+
transform.on('error', (error) => { t.assert.ok(error) })
|
|
257
|
+
transform.write('hello')
|
|
258
|
+
transform.write('world')
|
|
259
|
+
transform.end()
|
|
260
|
+
|
|
261
|
+
await t.assert.rejects(finished(transform), new Error('boom'))
|
|
262
|
+
})
|
|
263
|
+
})
|