@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
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { describe, test } = require('node:test')
|
|
4
|
+
const { createReadStream } = require('node:fs')
|
|
5
|
+
const { randomBytes } = require('node:crypto')
|
|
6
|
+
const zlib = require('node:zlib')
|
|
7
|
+
const Fastify = require('fastify')
|
|
8
|
+
const compressPlugin = require('../index')
|
|
9
|
+
|
|
10
|
+
// A compressible body of a given size, so tests can pick which side of
|
|
11
|
+
// `threshold` / `syncThreshold` a payload falls on
|
|
12
|
+
function buildPayload (size) {
|
|
13
|
+
return 'the quick brown fox jumps over the lazy dog '.repeat(Math.ceil(size / 44)).slice(0, size)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// The default `syncThreshold` is derived from the host parallelism, so every test that
|
|
17
|
+
// cares about which path a payload takes pins it explicitly to stay deterministic
|
|
18
|
+
const syncThreshold = 32768
|
|
19
|
+
|
|
20
|
+
const smallPayload = buildPayload(4096)
|
|
21
|
+
const largePayload = buildPayload(200 * 1024)
|
|
22
|
+
// incompressible, so that the already compressed payloads built from it stay
|
|
23
|
+
// comfortably above `threshold` and below the pinned `syncThreshold`
|
|
24
|
+
const incompressiblePayload = randomBytes(4096)
|
|
25
|
+
|
|
26
|
+
describe('When a buffered payload fits within `syncThreshold`, it should be compressed synchronously :', async () => {
|
|
27
|
+
test('using gzip', async (t) => {
|
|
28
|
+
t.plan(4)
|
|
29
|
+
|
|
30
|
+
const fastify = Fastify()
|
|
31
|
+
await fastify.register(compressPlugin, { global: true, syncThreshold })
|
|
32
|
+
|
|
33
|
+
fastify.get('/', (_request, reply) => {
|
|
34
|
+
reply.type('text/plain').send(smallPayload)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const response = await fastify.inject({
|
|
38
|
+
url: '/',
|
|
39
|
+
method: 'GET',
|
|
40
|
+
headers: { 'accept-encoding': 'gzip' }
|
|
41
|
+
})
|
|
42
|
+
t.assert.equal(response.headers.vary, 'accept-encoding')
|
|
43
|
+
t.assert.equal(response.headers['content-encoding'], 'gzip')
|
|
44
|
+
t.assert.equal(response.headers['content-length'], response.rawPayload.length.toString())
|
|
45
|
+
t.assert.equal(zlib.gunzipSync(response.rawPayload).toString('utf-8'), smallPayload)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('using deflate', async (t) => {
|
|
49
|
+
t.plan(4)
|
|
50
|
+
|
|
51
|
+
const fastify = Fastify()
|
|
52
|
+
await fastify.register(compressPlugin, { global: true, syncThreshold })
|
|
53
|
+
|
|
54
|
+
fastify.get('/', (_request, reply) => {
|
|
55
|
+
reply.type('text/plain').send(smallPayload)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const response = await fastify.inject({
|
|
59
|
+
url: '/',
|
|
60
|
+
method: 'GET',
|
|
61
|
+
headers: { 'accept-encoding': 'deflate' }
|
|
62
|
+
})
|
|
63
|
+
t.assert.equal(response.headers.vary, 'accept-encoding')
|
|
64
|
+
t.assert.equal(response.headers['content-encoding'], 'deflate')
|
|
65
|
+
t.assert.equal(response.headers['content-length'], response.rawPayload.length.toString())
|
|
66
|
+
t.assert.equal(zlib.inflateSync(response.rawPayload).toString('utf-8'), smallPayload)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('using brotli', async (t) => {
|
|
70
|
+
t.plan(4)
|
|
71
|
+
|
|
72
|
+
const fastify = Fastify()
|
|
73
|
+
await fastify.register(compressPlugin, { global: true, syncThreshold })
|
|
74
|
+
|
|
75
|
+
fastify.get('/', (_request, reply) => {
|
|
76
|
+
reply.type('text/plain').send(smallPayload)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const response = await fastify.inject({
|
|
80
|
+
url: '/',
|
|
81
|
+
method: 'GET',
|
|
82
|
+
headers: { 'accept-encoding': 'br' }
|
|
83
|
+
})
|
|
84
|
+
t.assert.equal(response.headers.vary, 'accept-encoding')
|
|
85
|
+
t.assert.equal(response.headers['content-encoding'], 'br')
|
|
86
|
+
t.assert.equal(response.headers['content-length'], response.rawPayload.length.toString())
|
|
87
|
+
t.assert.equal(zlib.brotliDecompressSync(response.rawPayload).toString('utf-8'), smallPayload)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('using zstd', async (t) => {
|
|
91
|
+
if (typeof zlib.createZstdCompress !== 'function') {
|
|
92
|
+
t.skip('zstd not supported in this Node.js version')
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
t.plan(4)
|
|
96
|
+
|
|
97
|
+
const fastify = Fastify()
|
|
98
|
+
await fastify.register(compressPlugin, { global: true, syncThreshold })
|
|
99
|
+
|
|
100
|
+
fastify.get('/', (_request, reply) => {
|
|
101
|
+
reply.type('text/plain').send(smallPayload)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const response = await fastify.inject({
|
|
105
|
+
url: '/',
|
|
106
|
+
method: 'GET',
|
|
107
|
+
headers: { 'accept-encoding': 'zstd' }
|
|
108
|
+
})
|
|
109
|
+
t.assert.equal(response.headers.vary, 'accept-encoding')
|
|
110
|
+
t.assert.equal(response.headers['content-encoding'], 'zstd')
|
|
111
|
+
t.assert.equal(response.headers['content-length'], response.rawPayload.length.toString())
|
|
112
|
+
t.assert.equal(zlib.zstdDecompressSync(response.rawPayload).toString('utf-8'), smallPayload)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('using a Buffer payload', async (t) => {
|
|
116
|
+
t.plan(3)
|
|
117
|
+
|
|
118
|
+
const fastify = Fastify()
|
|
119
|
+
await fastify.register(compressPlugin, { global: true, syncThreshold })
|
|
120
|
+
|
|
121
|
+
const buf = Buffer.from(smallPayload)
|
|
122
|
+
fastify.get('/', (_request, reply) => {
|
|
123
|
+
reply.type('text/plain').send(buf)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
const response = await fastify.inject({
|
|
127
|
+
url: '/',
|
|
128
|
+
method: 'GET',
|
|
129
|
+
headers: { 'accept-encoding': 'gzip' }
|
|
130
|
+
})
|
|
131
|
+
t.assert.equal(response.headers['content-encoding'], 'gzip')
|
|
132
|
+
t.assert.equal(response.headers['content-length'], response.rawPayload.length.toString())
|
|
133
|
+
t.assert.equal(zlib.gunzipSync(response.rawPayload).toString('utf-8'), smallPayload)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('using `reply.compress()`', async (t) => {
|
|
137
|
+
t.plan(4)
|
|
138
|
+
|
|
139
|
+
const fastify = Fastify()
|
|
140
|
+
await fastify.register(compressPlugin, { global: false, syncThreshold })
|
|
141
|
+
|
|
142
|
+
fastify.get('/', (_request, reply) => {
|
|
143
|
+
reply.type('text/plain').compress(smallPayload)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const response = await fastify.inject({
|
|
147
|
+
url: '/',
|
|
148
|
+
method: 'GET',
|
|
149
|
+
headers: { 'accept-encoding': 'gzip' }
|
|
150
|
+
})
|
|
151
|
+
t.assert.equal(response.headers.vary, 'accept-encoding')
|
|
152
|
+
t.assert.equal(response.headers['content-encoding'], 'gzip')
|
|
153
|
+
t.assert.equal(response.headers['content-length'], response.rawPayload.length.toString())
|
|
154
|
+
t.assert.equal(zlib.gunzipSync(response.rawPayload).toString('utf-8'), smallPayload)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
test('using a route level `compress` configuration', async (t) => {
|
|
158
|
+
t.plan(3)
|
|
159
|
+
|
|
160
|
+
const fastify = Fastify()
|
|
161
|
+
await fastify.register(compressPlugin, { global: false, syncThreshold })
|
|
162
|
+
|
|
163
|
+
fastify.get('/', {
|
|
164
|
+
compress: { syncThreshold: 8192 }
|
|
165
|
+
}, (_request, reply) => {
|
|
166
|
+
reply.type('text/plain').send(smallPayload)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const response = await fastify.inject({
|
|
170
|
+
url: '/',
|
|
171
|
+
method: 'GET',
|
|
172
|
+
headers: { 'accept-encoding': 'gzip' }
|
|
173
|
+
})
|
|
174
|
+
t.assert.equal(response.headers['content-encoding'], 'gzip')
|
|
175
|
+
t.assert.equal(response.headers['content-length'], response.rawPayload.length.toString())
|
|
176
|
+
t.assert.equal(zlib.gunzipSync(response.rawPayload).toString('utf-8'), smallPayload)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test('it should honour `zlibOptions`', async (t) => {
|
|
180
|
+
t.plan(2)
|
|
181
|
+
|
|
182
|
+
const fastify = Fastify()
|
|
183
|
+
await fastify.register(compressPlugin, { global: true, syncThreshold, zlibOptions: { level: 9 } })
|
|
184
|
+
|
|
185
|
+
fastify.get('/', (_request, reply) => {
|
|
186
|
+
reply.type('text/plain').send(smallPayload)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
const response = await fastify.inject({
|
|
190
|
+
url: '/',
|
|
191
|
+
method: 'GET',
|
|
192
|
+
headers: { 'accept-encoding': 'gzip' }
|
|
193
|
+
})
|
|
194
|
+
t.assert.equal(zlib.gunzipSync(response.rawPayload).toString('utf-8'), smallPayload)
|
|
195
|
+
t.assert.equal(response.rawPayload.length, zlib.gzipSync(smallPayload, { level: 9 }).length)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
test('it should honour `brotliOptions`', async (t) => {
|
|
199
|
+
t.plan(2)
|
|
200
|
+
|
|
201
|
+
const brotliOptions = { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 11 } }
|
|
202
|
+
|
|
203
|
+
const fastify = Fastify()
|
|
204
|
+
await fastify.register(compressPlugin, { global: true, syncThreshold, brotliOptions })
|
|
205
|
+
|
|
206
|
+
fastify.get('/', (_request, reply) => {
|
|
207
|
+
reply.type('text/plain').send(smallPayload)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
const response = await fastify.inject({
|
|
211
|
+
url: '/',
|
|
212
|
+
method: 'GET',
|
|
213
|
+
headers: { 'accept-encoding': 'br' }
|
|
214
|
+
})
|
|
215
|
+
t.assert.equal(zlib.brotliDecompressSync(response.rawPayload).toString('utf-8'), smallPayload)
|
|
216
|
+
t.assert.equal(response.rawPayload.length, zlib.brotliCompressSync(smallPayload, brotliOptions).length)
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
describe('An explicit `syncThreshold` should always win over the computed default :', async () => {
|
|
221
|
+
// the computed default is clamped to [4096, 65536], so these two values sit on either
|
|
222
|
+
// side of every default the formula can produce, whatever the host reports
|
|
223
|
+
test('a value below the computed default should push a payload onto the stream pipeline', async (t) => {
|
|
224
|
+
t.plan(3)
|
|
225
|
+
|
|
226
|
+
const fastify = Fastify()
|
|
227
|
+
await fastify.register(compressPlugin, { global: true, syncThreshold: 2048 })
|
|
228
|
+
|
|
229
|
+
fastify.get('/', (_request, reply) => {
|
|
230
|
+
reply.type('text/plain').send(smallPayload)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
const response = await fastify.inject({
|
|
234
|
+
url: '/',
|
|
235
|
+
method: 'GET',
|
|
236
|
+
headers: { 'accept-encoding': 'gzip' }
|
|
237
|
+
})
|
|
238
|
+
t.assert.equal(response.headers['content-encoding'], 'gzip')
|
|
239
|
+
t.assert.ok(!response.headers['content-length'], 'no content length')
|
|
240
|
+
t.assert.equal(zlib.gunzipSync(response.rawPayload).toString('utf-8'), smallPayload)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
test('a value above the computed default should keep a payload on the synchronous path', async (t) => {
|
|
244
|
+
t.plan(3)
|
|
245
|
+
|
|
246
|
+
const hugePayload = buildPayload(128 * 1024)
|
|
247
|
+
|
|
248
|
+
const fastify = Fastify()
|
|
249
|
+
await fastify.register(compressPlugin, { global: true, syncThreshold: 256 * 1024 })
|
|
250
|
+
|
|
251
|
+
fastify.get('/', (_request, reply) => {
|
|
252
|
+
reply.type('text/plain').send(hugePayload)
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
const response = await fastify.inject({
|
|
256
|
+
url: '/',
|
|
257
|
+
method: 'GET',
|
|
258
|
+
headers: { 'accept-encoding': 'gzip' }
|
|
259
|
+
})
|
|
260
|
+
t.assert.equal(response.headers['content-encoding'], 'gzip')
|
|
261
|
+
t.assert.equal(response.headers['content-length'], response.rawPayload.length.toString())
|
|
262
|
+
t.assert.equal(zlib.gunzipSync(response.rawPayload).toString('utf-8'), hugePayload)
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
test('the computed default should compress a payload it covers on every host', async (t) => {
|
|
266
|
+
t.plan(3)
|
|
267
|
+
|
|
268
|
+
// 2 KiB is above `threshold` and below the lowest default the formula can produce
|
|
269
|
+
const payload = buildPayload(2048)
|
|
270
|
+
|
|
271
|
+
const fastify = Fastify()
|
|
272
|
+
await fastify.register(compressPlugin, { global: true })
|
|
273
|
+
|
|
274
|
+
fastify.get('/', (_request, reply) => {
|
|
275
|
+
reply.type('text/plain').send(payload)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
const response = await fastify.inject({
|
|
279
|
+
url: '/',
|
|
280
|
+
method: 'GET',
|
|
281
|
+
headers: { 'accept-encoding': 'gzip' }
|
|
282
|
+
})
|
|
283
|
+
t.assert.equal(response.headers['content-encoding'], 'gzip')
|
|
284
|
+
t.assert.equal(response.headers['content-length'], response.rawPayload.length.toString())
|
|
285
|
+
t.assert.equal(zlib.gunzipSync(response.rawPayload).toString('utf-8'), payload)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
test('the computed default should stream a payload above the highest value it can produce', async (t) => {
|
|
289
|
+
t.plan(3)
|
|
290
|
+
|
|
291
|
+
// 256 KiB is above the upper clamp of the formula, so it streams on every host
|
|
292
|
+
const payload = buildPayload(256 * 1024)
|
|
293
|
+
|
|
294
|
+
const fastify = Fastify()
|
|
295
|
+
await fastify.register(compressPlugin, { global: true })
|
|
296
|
+
|
|
297
|
+
fastify.get('/', (_request, reply) => {
|
|
298
|
+
reply.type('text/plain').send(payload)
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
const response = await fastify.inject({
|
|
302
|
+
url: '/',
|
|
303
|
+
method: 'GET',
|
|
304
|
+
headers: { 'accept-encoding': 'gzip' }
|
|
305
|
+
})
|
|
306
|
+
t.assert.equal(response.headers['content-encoding'], 'gzip')
|
|
307
|
+
t.assert.ok(!response.headers['content-length'], 'no content length')
|
|
308
|
+
t.assert.equal(zlib.gunzipSync(response.rawPayload).toString('utf-8'), payload)
|
|
309
|
+
})
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
describe('It should fall back to the stream pipeline when the payload is not eligible :', async () => {
|
|
313
|
+
test('when the payload is smaller than `threshold`', async (t) => {
|
|
314
|
+
t.plan(3)
|
|
315
|
+
|
|
316
|
+
const fastify = Fastify()
|
|
317
|
+
await fastify.register(compressPlugin, { global: true, syncThreshold })
|
|
318
|
+
|
|
319
|
+
fastify.get('/', (_request, reply) => {
|
|
320
|
+
reply.type('text/plain').send('hello world')
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
const response = await fastify.inject({
|
|
324
|
+
url: '/',
|
|
325
|
+
method: 'GET',
|
|
326
|
+
headers: { 'accept-encoding': 'gzip' }
|
|
327
|
+
})
|
|
328
|
+
t.assert.ok(!response.headers.vary)
|
|
329
|
+
t.assert.ok(!response.headers['content-encoding'])
|
|
330
|
+
t.assert.equal(response.payload, 'hello world')
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
test('when the payload is bigger than `syncThreshold`', async (t) => {
|
|
334
|
+
t.plan(4)
|
|
335
|
+
|
|
336
|
+
const fastify = Fastify()
|
|
337
|
+
await fastify.register(compressPlugin, { global: true, syncThreshold })
|
|
338
|
+
|
|
339
|
+
fastify.get('/', (_request, reply) => {
|
|
340
|
+
reply.type('text/plain').send(largePayload)
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
const response = await fastify.inject({
|
|
344
|
+
url: '/',
|
|
345
|
+
method: 'GET',
|
|
346
|
+
headers: { 'accept-encoding': 'gzip' }
|
|
347
|
+
})
|
|
348
|
+
t.assert.equal(response.headers.vary, 'accept-encoding')
|
|
349
|
+
t.assert.equal(response.headers['content-encoding'], 'gzip')
|
|
350
|
+
// the streamed path cannot know the compressed size upfront
|
|
351
|
+
t.assert.ok(!response.headers['content-length'], 'no content length')
|
|
352
|
+
t.assert.equal(zlib.gunzipSync(response.rawPayload).toString('utf-8'), largePayload)
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
test('when `syncThreshold` is `0`', async (t) => {
|
|
356
|
+
t.plan(4)
|
|
357
|
+
|
|
358
|
+
const fastify = Fastify()
|
|
359
|
+
await fastify.register(compressPlugin, { global: true, syncThreshold: 0 })
|
|
360
|
+
|
|
361
|
+
fastify.get('/', (_request, reply) => {
|
|
362
|
+
reply.type('text/plain').send(smallPayload)
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
const response = await fastify.inject({
|
|
366
|
+
url: '/',
|
|
367
|
+
method: 'GET',
|
|
368
|
+
headers: { 'accept-encoding': 'gzip' }
|
|
369
|
+
})
|
|
370
|
+
t.assert.equal(response.headers.vary, 'accept-encoding')
|
|
371
|
+
t.assert.equal(response.headers['content-encoding'], 'gzip')
|
|
372
|
+
t.assert.ok(!response.headers['content-length'], 'no content length')
|
|
373
|
+
t.assert.equal(zlib.gunzipSync(response.rawPayload).toString('utf-8'), smallPayload)
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
test('when `syncThreshold` is `0` using `reply.compress()`', async (t) => {
|
|
377
|
+
t.plan(3)
|
|
378
|
+
|
|
379
|
+
const fastify = Fastify()
|
|
380
|
+
await fastify.register(compressPlugin, { global: false, syncThreshold: 0 })
|
|
381
|
+
|
|
382
|
+
fastify.get('/', (_request, reply) => {
|
|
383
|
+
reply.type('text/plain').compress(smallPayload)
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
const response = await fastify.inject({
|
|
387
|
+
url: '/',
|
|
388
|
+
method: 'GET',
|
|
389
|
+
headers: { 'accept-encoding': 'gzip' }
|
|
390
|
+
})
|
|
391
|
+
t.assert.equal(response.headers['content-encoding'], 'gzip')
|
|
392
|
+
t.assert.ok(!response.headers['content-length'], 'no content length')
|
|
393
|
+
t.assert.equal(zlib.gunzipSync(response.rawPayload).toString('utf-8'), smallPayload)
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
test('when the payload is a Stream', async (t) => {
|
|
397
|
+
t.plan(3)
|
|
398
|
+
|
|
399
|
+
const fastify = Fastify()
|
|
400
|
+
await fastify.register(compressPlugin, { global: true, syncThreshold })
|
|
401
|
+
|
|
402
|
+
fastify.get('/', (_request, reply) => {
|
|
403
|
+
reply.type('text/plain').send(createReadStream('./package.json'))
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
const response = await fastify.inject({
|
|
407
|
+
url: '/',
|
|
408
|
+
method: 'GET',
|
|
409
|
+
headers: { 'accept-encoding': 'gzip' }
|
|
410
|
+
})
|
|
411
|
+
t.assert.equal(response.headers['content-encoding'], 'gzip')
|
|
412
|
+
t.assert.ok(!response.headers['content-length'], 'no content length')
|
|
413
|
+
t.assert.ok(zlib.gunzipSync(response.rawPayload).length > 0)
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
test('when the payload is a Web ReadableStream', async (t) => {
|
|
417
|
+
t.plan(3)
|
|
418
|
+
|
|
419
|
+
const fastify = Fastify()
|
|
420
|
+
await fastify.register(compressPlugin, { global: true, syncThreshold })
|
|
421
|
+
|
|
422
|
+
fastify.get('/', (_request, reply) => {
|
|
423
|
+
const stream = new ReadableStream({
|
|
424
|
+
start (controller) {
|
|
425
|
+
controller.enqueue(new TextEncoder().encode(smallPayload))
|
|
426
|
+
controller.close()
|
|
427
|
+
}
|
|
428
|
+
})
|
|
429
|
+
reply.type('text/plain').send(stream)
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
const response = await fastify.inject({
|
|
433
|
+
url: '/',
|
|
434
|
+
method: 'GET',
|
|
435
|
+
headers: { 'accept-encoding': 'gzip' }
|
|
436
|
+
})
|
|
437
|
+
t.assert.equal(response.headers['content-encoding'], 'gzip')
|
|
438
|
+
t.assert.ok(!response.headers['content-length'], 'no content length')
|
|
439
|
+
t.assert.equal(zlib.gunzipSync(response.rawPayload).toString('utf-8'), smallPayload)
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
test('when a custom `zlib` provides no synchronous method', async (t) => {
|
|
443
|
+
t.plan(4)
|
|
444
|
+
|
|
445
|
+
let usedCustom = false
|
|
446
|
+
const customZlib = { createGzip: () => (usedCustom = true) && zlib.createGzip() }
|
|
447
|
+
|
|
448
|
+
const fastify = Fastify()
|
|
449
|
+
await fastify.register(compressPlugin, { global: true, syncThreshold, zlib: customZlib })
|
|
450
|
+
|
|
451
|
+
fastify.get('/', (_request, reply) => {
|
|
452
|
+
reply.type('text/plain').send(smallPayload)
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
const response = await fastify.inject({
|
|
456
|
+
url: '/',
|
|
457
|
+
method: 'GET',
|
|
458
|
+
headers: { 'accept-encoding': 'gzip' }
|
|
459
|
+
})
|
|
460
|
+
t.assert.equal(usedCustom, true)
|
|
461
|
+
t.assert.equal(response.headers['content-encoding'], 'gzip')
|
|
462
|
+
t.assert.ok(!response.headers['content-length'], 'no content length')
|
|
463
|
+
t.assert.equal(zlib.gunzipSync(response.rawPayload).toString('utf-8'), smallPayload)
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
test('when a custom `zlib` provides no synchronous method using `reply.compress()`', async (t) => {
|
|
467
|
+
t.plan(3)
|
|
468
|
+
|
|
469
|
+
let usedCustom = false
|
|
470
|
+
const customZlib = { createDeflate: () => (usedCustom = true) && zlib.createDeflate() }
|
|
471
|
+
|
|
472
|
+
const fastify = Fastify()
|
|
473
|
+
await fastify.register(compressPlugin, { global: false, syncThreshold, zlib: customZlib })
|
|
474
|
+
|
|
475
|
+
fastify.get('/', (_request, reply) => {
|
|
476
|
+
reply.type('text/plain').compress(smallPayload)
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
const response = await fastify.inject({
|
|
480
|
+
url: '/',
|
|
481
|
+
method: 'GET',
|
|
482
|
+
headers: { 'accept-encoding': 'deflate' }
|
|
483
|
+
})
|
|
484
|
+
t.assert.equal(usedCustom, true)
|
|
485
|
+
t.assert.ok(!response.headers['content-length'], 'no content length')
|
|
486
|
+
t.assert.equal(zlib.inflateSync(response.rawPayload).toString('utf-8'), smallPayload)
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
test('when `removeContentLengthHeader` is `false` and the reply already carries a `Content-Length`', async (t) => {
|
|
490
|
+
t.plan(3)
|
|
491
|
+
|
|
492
|
+
const fastify = Fastify()
|
|
493
|
+
await fastify.register(compressPlugin, { global: true, syncThreshold, removeContentLengthHeader: false })
|
|
494
|
+
|
|
495
|
+
fastify.get('/', (_request, reply) => {
|
|
496
|
+
reply
|
|
497
|
+
.type('text/plain')
|
|
498
|
+
.header('content-length', '' + Buffer.byteLength(smallPayload))
|
|
499
|
+
.send(smallPayload)
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
const response = await fastify.inject({
|
|
503
|
+
url: '/',
|
|
504
|
+
method: 'GET',
|
|
505
|
+
headers: { 'accept-encoding': 'gzip' }
|
|
506
|
+
})
|
|
507
|
+
t.assert.equal(response.headers['content-encoding'], 'gzip')
|
|
508
|
+
// the caller provided value is preserved, as it is on the streamed path
|
|
509
|
+
t.assert.equal(response.headers['content-length'], Buffer.byteLength(smallPayload).toString())
|
|
510
|
+
t.assert.equal(zlib.gunzipSync(response.rawPayload).toString('utf-8'), smallPayload)
|
|
511
|
+
})
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
describe('When `syncThreshold` is `0`, every encoding should keep using the stream pipeline :', async () => {
|
|
515
|
+
// a custom `zlib` exposing an unrelated method only: every encoding falls back
|
|
516
|
+
// to the native stream compressor, exactly as it did before the synchronous path
|
|
517
|
+
const customZlib = { createGunzip: zlib.createGunzip }
|
|
518
|
+
|
|
519
|
+
for (const [encoding, decompress] of [
|
|
520
|
+
['br', zlib.brotliDecompressSync],
|
|
521
|
+
['gzip', zlib.gunzipSync],
|
|
522
|
+
['deflate', zlib.inflateSync]
|
|
523
|
+
]) {
|
|
524
|
+
test(`using ${encoding} with a custom \`zlib\``, async (t) => {
|
|
525
|
+
t.plan(3)
|
|
526
|
+
|
|
527
|
+
const fastify = Fastify()
|
|
528
|
+
await fastify.register(compressPlugin, { global: true, zlib: customZlib, syncThreshold: 0 })
|
|
529
|
+
|
|
530
|
+
fastify.get('/', (_request, reply) => {
|
|
531
|
+
reply.type('text/plain').send(smallPayload)
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
const response = await fastify.inject({
|
|
535
|
+
url: '/',
|
|
536
|
+
method: 'GET',
|
|
537
|
+
headers: { 'accept-encoding': encoding }
|
|
538
|
+
})
|
|
539
|
+
t.assert.equal(response.headers['content-encoding'], encoding)
|
|
540
|
+
t.assert.ok(!response.headers['content-length'], 'no content length')
|
|
541
|
+
t.assert.equal(decompress(response.rawPayload).toString('utf-8'), smallPayload)
|
|
542
|
+
})
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
test('using zstd with a custom `zlib`', async (t) => {
|
|
546
|
+
if (typeof zlib.createZstdCompress !== 'function') {
|
|
547
|
+
t.skip('zstd not supported in this Node.js version')
|
|
548
|
+
return
|
|
549
|
+
}
|
|
550
|
+
t.plan(3)
|
|
551
|
+
|
|
552
|
+
const fastify = Fastify()
|
|
553
|
+
await fastify.register(compressPlugin, { global: true, zlib: customZlib, syncThreshold: 0 })
|
|
554
|
+
|
|
555
|
+
fastify.get('/', (_request, reply) => {
|
|
556
|
+
reply.type('text/plain').send(smallPayload)
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
const response = await fastify.inject({
|
|
560
|
+
url: '/',
|
|
561
|
+
method: 'GET',
|
|
562
|
+
headers: { 'accept-encoding': 'zstd' }
|
|
563
|
+
})
|
|
564
|
+
t.assert.equal(response.headers['content-encoding'], 'zstd')
|
|
565
|
+
t.assert.ok(!response.headers['content-length'], 'no content length')
|
|
566
|
+
t.assert.equal(zlib.zstdDecompressSync(response.rawPayload).toString('utf-8'), smallPayload)
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
test('using zstd with the native `zlib`', async (t) => {
|
|
570
|
+
if (typeof zlib.createZstdCompress !== 'function') {
|
|
571
|
+
t.skip('zstd not supported in this Node.js version')
|
|
572
|
+
return
|
|
573
|
+
}
|
|
574
|
+
t.plan(3)
|
|
575
|
+
|
|
576
|
+
const fastify = Fastify()
|
|
577
|
+
await fastify.register(compressPlugin, { global: true, syncThreshold: 0 })
|
|
578
|
+
|
|
579
|
+
fastify.get('/', (_request, reply) => {
|
|
580
|
+
reply.type('text/plain').send(smallPayload)
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
const response = await fastify.inject({
|
|
584
|
+
url: '/',
|
|
585
|
+
method: 'GET',
|
|
586
|
+
headers: { 'accept-encoding': 'zstd' }
|
|
587
|
+
})
|
|
588
|
+
t.assert.equal(response.headers['content-encoding'], 'zstd')
|
|
589
|
+
t.assert.ok(!response.headers['content-length'], 'no content length')
|
|
590
|
+
t.assert.equal(zlib.zstdDecompressSync(response.rawPayload).toString('utf-8'), smallPayload)
|
|
591
|
+
})
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
describe('It should use a synchronous method provided by a custom `zlib` :', async () => {
|
|
595
|
+
test('using `gzipSync`', async (t) => {
|
|
596
|
+
t.plan(3)
|
|
597
|
+
|
|
598
|
+
let usedCustom = false
|
|
599
|
+
const customZlib = {
|
|
600
|
+
gzipSync: (payload, options) => {
|
|
601
|
+
usedCustom = true
|
|
602
|
+
return zlib.gzipSync(payload, options)
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const fastify = Fastify()
|
|
607
|
+
await fastify.register(compressPlugin, { global: true, syncThreshold, zlib: customZlib })
|
|
608
|
+
|
|
609
|
+
fastify.get('/', (_request, reply) => {
|
|
610
|
+
reply.type('text/plain').send(smallPayload)
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
const response = await fastify.inject({
|
|
614
|
+
url: '/',
|
|
615
|
+
method: 'GET',
|
|
616
|
+
headers: { 'accept-encoding': 'gzip' }
|
|
617
|
+
})
|
|
618
|
+
t.assert.equal(usedCustom, true)
|
|
619
|
+
t.assert.equal(response.headers['content-encoding'], 'gzip')
|
|
620
|
+
t.assert.equal(zlib.gunzipSync(response.rawPayload).toString('utf-8'), smallPayload)
|
|
621
|
+
})
|
|
622
|
+
})
|
|
623
|
+
|
|
624
|
+
describe('It should pass already compressed payloads through untouched :', async () => {
|
|
625
|
+
test('when the payload is already gzipped', async (t) => {
|
|
626
|
+
t.plan(3)
|
|
627
|
+
|
|
628
|
+
const gzipped = zlib.gzipSync(incompressiblePayload)
|
|
629
|
+
|
|
630
|
+
const fastify = Fastify()
|
|
631
|
+
await fastify.register(compressPlugin, { global: true, syncThreshold })
|
|
632
|
+
|
|
633
|
+
fastify.get('/', (_request, reply) => {
|
|
634
|
+
reply.type('application/octet-stream').send(gzipped)
|
|
635
|
+
})
|
|
636
|
+
|
|
637
|
+
const response = await fastify.inject({
|
|
638
|
+
url: '/',
|
|
639
|
+
method: 'GET',
|
|
640
|
+
headers: { 'accept-encoding': 'gzip' }
|
|
641
|
+
})
|
|
642
|
+
t.assert.equal(response.headers['content-encoding'], 'gzip')
|
|
643
|
+
t.assert.deepEqual(response.rawPayload, gzipped)
|
|
644
|
+
t.assert.deepEqual(zlib.gunzipSync(response.rawPayload), incompressiblePayload)
|
|
645
|
+
})
|
|
646
|
+
|
|
647
|
+
test('when the payload is already deflated', async (t) => {
|
|
648
|
+
t.plan(3)
|
|
649
|
+
|
|
650
|
+
const deflated = zlib.deflateSync(incompressiblePayload)
|
|
651
|
+
|
|
652
|
+
const fastify = Fastify()
|
|
653
|
+
await fastify.register(compressPlugin, { global: true, syncThreshold })
|
|
654
|
+
|
|
655
|
+
fastify.get('/', (_request, reply) => {
|
|
656
|
+
reply.type('application/octet-stream').send(deflated)
|
|
657
|
+
})
|
|
658
|
+
|
|
659
|
+
const response = await fastify.inject({
|
|
660
|
+
url: '/',
|
|
661
|
+
method: 'GET',
|
|
662
|
+
headers: { 'accept-encoding': 'deflate' }
|
|
663
|
+
})
|
|
664
|
+
t.assert.equal(response.headers['content-encoding'], 'deflate')
|
|
665
|
+
t.assert.deepEqual(response.rawPayload, deflated)
|
|
666
|
+
t.assert.deepEqual(zlib.inflateSync(response.rawPayload), incompressiblePayload)
|
|
667
|
+
})
|
|
668
|
+
|
|
669
|
+
test('when the payload is already gzipped using `reply.compress()`', async (t) => {
|
|
670
|
+
t.plan(2)
|
|
671
|
+
|
|
672
|
+
const gzipped = zlib.gzipSync(incompressiblePayload)
|
|
673
|
+
|
|
674
|
+
const fastify = Fastify()
|
|
675
|
+
await fastify.register(compressPlugin, { global: false, syncThreshold })
|
|
676
|
+
|
|
677
|
+
fastify.get('/', (_request, reply) => {
|
|
678
|
+
reply.type('application/octet-stream').compress(gzipped)
|
|
679
|
+
})
|
|
680
|
+
|
|
681
|
+
const response = await fastify.inject({
|
|
682
|
+
url: '/',
|
|
683
|
+
method: 'GET',
|
|
684
|
+
headers: { 'accept-encoding': 'gzip' }
|
|
685
|
+
})
|
|
686
|
+
t.assert.equal(response.headers['content-encoding'], 'gzip')
|
|
687
|
+
t.assert.deepEqual(response.rawPayload, gzipped)
|
|
688
|
+
})
|
|
689
|
+
})
|
|
690
|
+
|
|
691
|
+
describe('It should handle synchronous compression errors :', async () => {
|
|
692
|
+
test('using `onSend` hook', async (t) => {
|
|
693
|
+
t.plan(2)
|
|
694
|
+
|
|
695
|
+
const customZlib = {
|
|
696
|
+
gzipSync: () => {
|
|
697
|
+
throw new Error('sync compression failed')
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const fastify = Fastify()
|
|
702
|
+
await fastify.register(compressPlugin, { global: true, syncThreshold, zlib: customZlib })
|
|
703
|
+
|
|
704
|
+
fastify.get('/', (_request, reply) => {
|
|
705
|
+
reply.type('text/plain').send(smallPayload)
|
|
706
|
+
})
|
|
707
|
+
|
|
708
|
+
const response = await fastify.inject({
|
|
709
|
+
url: '/',
|
|
710
|
+
method: 'GET',
|
|
711
|
+
headers: { 'accept-encoding': 'gzip' }
|
|
712
|
+
})
|
|
713
|
+
t.assert.equal(response.statusCode, 500)
|
|
714
|
+
t.assert.equal(JSON.parse(response.payload).message, 'sync compression failed')
|
|
715
|
+
})
|
|
716
|
+
|
|
717
|
+
test('using `reply.compress()`', async (t) => {
|
|
718
|
+
t.plan(2)
|
|
719
|
+
|
|
720
|
+
const customZlib = {
|
|
721
|
+
gzipSync: () => {
|
|
722
|
+
throw new Error('sync compression failed')
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const fastify = Fastify()
|
|
727
|
+
await fastify.register(compressPlugin, { global: false, syncThreshold, zlib: customZlib })
|
|
728
|
+
|
|
729
|
+
fastify.get('/', (_request, reply) => {
|
|
730
|
+
reply.type('text/plain').compress(smallPayload)
|
|
731
|
+
})
|
|
732
|
+
|
|
733
|
+
const response = await fastify.inject({
|
|
734
|
+
url: '/',
|
|
735
|
+
method: 'GET',
|
|
736
|
+
headers: { 'accept-encoding': 'gzip' }
|
|
737
|
+
})
|
|
738
|
+
t.assert.equal(response.statusCode, 500)
|
|
739
|
+
t.assert.equal(JSON.parse(response.payload).message, 'sync compression failed')
|
|
740
|
+
})
|
|
741
|
+
})
|