@fastify/compress 8.0.3 → 8.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 +5 -0
- package/LICENSE +3 -1
- package/README.md +68 -9
- package/index.js +37 -5
- package/lib/utils.js +29 -1
- package/package.json +7 -7
- package/test/global-compress.test.js +101 -0
- package/test/global-decompress.test.js +27 -0
- package/test/routes-compress.test.js +30 -0
- package/test/utils.test.js +18 -1
- package/types/index.d.ts +2 -2
- package/types/index.test-d.ts +30 -0
package/.github/workflows/ci.yml
CHANGED
|
@@ -14,6 +14,11 @@ on:
|
|
|
14
14
|
- 'docs/**'
|
|
15
15
|
- '*.md'
|
|
16
16
|
|
|
17
|
+
# This allows a subsequently queued workflow run to interrupt previous runs
|
|
18
|
+
concurrency:
|
|
19
|
+
group: "${{ github.workflow }}-${{ github.event.pull_request.head.label || github.head_ref || github.ref }}"
|
|
20
|
+
cancel-in-progress: true
|
|
21
|
+
|
|
17
22
|
permissions:
|
|
18
23
|
contents: read
|
|
19
24
|
|
package/LICENSE
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2017 Fastify
|
|
3
|
+
Copyright (c) 2017-present The Fastify team
|
|
4
|
+
|
|
5
|
+
The Fastify team members are listed at https://github.com/fastify/fastify#team.
|
|
4
6
|
|
|
5
7
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
8
|
of this software and associated documentation files (the "Software"), to deal
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://github.com/neostandard/neostandard)
|
|
6
6
|
|
|
7
7
|
Adds compression utils to [the Fastify `reply` object](https://fastify.dev/docs/latest/Reference/Reply/#reply) and a hook to decompress requests payloads.
|
|
8
|
-
Supports `gzip`, `deflate`, and `
|
|
8
|
+
Supports `gzip`, `deflate`, `brotli`, and `zstd` (Node.js 22.15+/23.8+).
|
|
9
9
|
|
|
10
10
|
> ℹ️ Note: In large-scale scenarios, use a proxy like Nginx to handle response compression.
|
|
11
11
|
|
|
@@ -37,11 +37,12 @@ This plugin adds two functionalities to Fastify: a compress utility and a global
|
|
|
37
37
|
|
|
38
38
|
Currently, the following encoding tokens are supported, using the first acceptable token in this order:
|
|
39
39
|
|
|
40
|
-
1. `
|
|
41
|
-
2. `
|
|
42
|
-
3. `
|
|
43
|
-
4.
|
|
44
|
-
5.
|
|
40
|
+
1. `zstd` (Node.js 22.15+/23.8+)
|
|
41
|
+
2. `br`
|
|
42
|
+
3. `gzip`
|
|
43
|
+
4. `deflate`
|
|
44
|
+
5. `*` (no preference — `@fastify/compress` will use `gzip`)
|
|
45
|
+
6. `identity` (no compression)
|
|
45
46
|
|
|
46
47
|
If an unsupported encoding is received or the `'accept-encoding'` header is missing, the payload will not be compressed.
|
|
47
48
|
To return an error for unsupported encoding, use the `onUnsupportedEncoding` option.
|
|
@@ -105,6 +106,40 @@ app.get('/', (req, reply) => {
|
|
|
105
106
|
await app.listen({ port: 3000 })
|
|
106
107
|
```
|
|
107
108
|
|
|
109
|
+
It's also possible to pass a Fetch API `Response` object or a Web `ReadableStream`. The plugin will automatically extract the body stream from the `Response` or convert the Web stream to a Node.js `Readable` behind the scenes.
|
|
110
|
+
|
|
111
|
+
```js
|
|
112
|
+
import fastify from 'fastify'
|
|
113
|
+
|
|
114
|
+
const app = fastify()
|
|
115
|
+
await app.register(import('@fastify/compress'), { global: true })
|
|
116
|
+
|
|
117
|
+
app.get('/', async (req, reply) => {
|
|
118
|
+
const resp = new Response('Hello from Fetch Response')
|
|
119
|
+
reply.compress(resp)
|
|
120
|
+
})
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
app.get('/', async (req, reply) => {
|
|
125
|
+
return new Response('Hello from Fetch Response')
|
|
126
|
+
})
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
app.get('/', (req, reply) => {
|
|
131
|
+
const stream = new ReadableStream({
|
|
132
|
+
start (controller) {
|
|
133
|
+
controller.enqueue(new TextEncoder().encode('Hello from Web ReadableStream'))
|
|
134
|
+
controller.close()
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
reply.header('content-type', 'text/plain')
|
|
139
|
+
reply.compress(stream)
|
|
140
|
+
})
|
|
141
|
+
```
|
|
142
|
+
|
|
108
143
|
## Compress Options
|
|
109
144
|
|
|
110
145
|
### threshold
|
|
@@ -175,6 +210,13 @@ await fastify.register(
|
|
|
175
210
|
// Only support gzip and deflate, and prefer deflate to gzip
|
|
176
211
|
{ encodings: ['deflate', 'gzip'] }
|
|
177
212
|
)
|
|
213
|
+
|
|
214
|
+
// Example with zstd support (Node.js 22.15+/23.8+)
|
|
215
|
+
await fastify.register(
|
|
216
|
+
import('@fastify/compress'),
|
|
217
|
+
// Prefer zstd, fallback to brotli, then gzip
|
|
218
|
+
{ encodings: ['zstd', 'br', 'gzip'] }
|
|
219
|
+
)
|
|
178
220
|
```
|
|
179
221
|
|
|
180
222
|
### brotliOptions and zlibOptions
|
|
@@ -214,14 +256,24 @@ This plugin adds a `preParsing` hook to decompress the request payload based on
|
|
|
214
256
|
|
|
215
257
|
Currently, the following encoding tokens are supported:
|
|
216
258
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
259
|
+
- `zstd` (Node.js 22.15+/23.8+)
|
|
260
|
+
- `br`
|
|
261
|
+
- `gzip`
|
|
262
|
+
- `deflate`
|
|
220
263
|
|
|
221
264
|
If an unsupported encoding or invalid payload is received, the plugin throws an error.
|
|
222
265
|
|
|
223
266
|
If the request header is missing, the plugin yields to the next hook.
|
|
224
267
|
|
|
268
|
+
### Supported payload types
|
|
269
|
+
|
|
270
|
+
The plugin supports compressing the following payload types:
|
|
271
|
+
|
|
272
|
+
- Strings and Buffers
|
|
273
|
+
- Node.js streams
|
|
274
|
+
- Response objects (from the Fetch API)
|
|
275
|
+
- ReadableStream objects (from the Web Streams API)
|
|
276
|
+
|
|
225
277
|
### Global hook
|
|
226
278
|
|
|
227
279
|
The global request decompression hook is enabled by default. To disable it, pass `{ global: false }`:
|
|
@@ -268,6 +320,13 @@ await fastify.register(
|
|
|
268
320
|
// Only support gzip
|
|
269
321
|
{ requestEncodings: ['gzip'] }
|
|
270
322
|
)
|
|
323
|
+
|
|
324
|
+
// Example with zstd support for request decompression (Node.js 22.15+/23.8+)
|
|
325
|
+
await fastify.register(
|
|
326
|
+
import('@fastify/compress'),
|
|
327
|
+
// Support zstd, brotli and gzip for request decompression
|
|
328
|
+
{ requestEncodings: ['zstd', 'br', 'gzip'] }
|
|
329
|
+
)
|
|
271
330
|
```
|
|
272
331
|
|
|
273
332
|
### forceRequestEncoding
|
package/index.js
CHANGED
|
@@ -12,7 +12,7 @@ const { Minipass } = require('minipass')
|
|
|
12
12
|
const pumpify = require('pumpify')
|
|
13
13
|
const { Readable } = require('readable-stream')
|
|
14
14
|
|
|
15
|
-
const { isStream, isGzip, isDeflate, intoAsyncIterator } = require('./lib/utils')
|
|
15
|
+
const { isStream, isGzip, isDeflate, intoAsyncIterator, isWebReadableStream, isFetchResponse, webStreamToNodeReadable } = require('./lib/utils')
|
|
16
16
|
|
|
17
17
|
const InvalidRequestEncodingError = createError('FST_CP_ERR_INVALID_CONTENT_ENCODING', 'Unsupported Content-Encoding: %s', 415)
|
|
18
18
|
const InvalidRequestCompressedPayloadError = createError('FST_CP_ERR_INVALID_CONTENT', 'Could not decompress the request payload using the provided encoding', 400)
|
|
@@ -145,6 +145,9 @@ function processCompressParams (opts) {
|
|
|
145
145
|
gzip: () => ((opts.zlib || zlib).createGzip || zlib.createGzip)(params.zlibOptions),
|
|
146
146
|
deflate: () => ((opts.zlib || zlib).createDeflate || zlib.createDeflate)(params.zlibOptions)
|
|
147
147
|
}
|
|
148
|
+
if (typeof ((opts.zlib || zlib).createZstdCompress || zlib.createZstdCompress) === 'function') {
|
|
149
|
+
params.compressStream.zstd = () => ((opts.zlib || zlib).createZstdCompress || zlib.createZstdCompress)(params.zlibOptions)
|
|
150
|
+
}
|
|
148
151
|
params.uncompressStream = {
|
|
149
152
|
// Currently params.uncompressStream.br() is never called as we do not have any way to autodetect brotli compression in `fastify-compress`
|
|
150
153
|
// Brotli documentation reference: [RFC 7932](https://www.rfc-editor.org/rfc/rfc7932)
|
|
@@ -152,8 +155,16 @@ function processCompressParams (opts) {
|
|
|
152
155
|
gzip: () => ((opts.zlib || zlib).createGunzip || zlib.createGunzip)(params.zlibOptions),
|
|
153
156
|
deflate: () => ((opts.zlib || zlib).createInflate || zlib.createInflate)(params.zlibOptions)
|
|
154
157
|
}
|
|
158
|
+
if (typeof ((opts.zlib || zlib).createZstdDecompress || zlib.createZstdDecompress) === 'function') {
|
|
159
|
+
// Currently params.uncompressStream.zstd() is never called as we do not have any way to autodetect zstd compression in `fastify-compress`
|
|
160
|
+
// Zstd documentation reference: [RFC 8878](https://www.rfc-editor.org/rfc/rfc8878)
|
|
161
|
+
params.uncompressStream.zstd = /* c8 ignore next */ () => ((opts.zlib || zlib).createZstdDecompress || zlib.createZstdDecompress)(params.zlibOptions)
|
|
162
|
+
}
|
|
155
163
|
|
|
156
164
|
const supportedEncodings = ['br', 'gzip', 'deflate', 'identity']
|
|
165
|
+
if (typeof zlib.createZstdCompress === 'function') {
|
|
166
|
+
supportedEncodings.unshift('zstd')
|
|
167
|
+
}
|
|
157
168
|
|
|
158
169
|
params.encodings = Array.isArray(opts.encodings)
|
|
159
170
|
? supportedEncodings
|
|
@@ -184,8 +195,14 @@ function processDecompressParams (opts) {
|
|
|
184
195
|
encodings: [],
|
|
185
196
|
forceEncoding: null
|
|
186
197
|
}
|
|
198
|
+
if (typeof (customZlib.createZstdDecompress || zlib.createZstdDecompress) === 'function') {
|
|
199
|
+
params.decompressStream.zstd = customZlib.createZstdDecompress || zlib.createZstdDecompress
|
|
200
|
+
}
|
|
187
201
|
|
|
188
202
|
const supportedEncodings = ['br', 'gzip', 'deflate', 'identity']
|
|
203
|
+
if (typeof zlib.createZstdCompress === 'function') {
|
|
204
|
+
supportedEncodings.unshift('zstd')
|
|
205
|
+
}
|
|
189
206
|
|
|
190
207
|
params.encodings = Array.isArray(opts.requestEncodings)
|
|
191
208
|
? supportedEncodings
|
|
@@ -237,6 +254,7 @@ function buildRouteCompress (_fastify, params, routeOptions, decorateOnly) {
|
|
|
237
254
|
if (payload == null) {
|
|
238
255
|
return next()
|
|
239
256
|
}
|
|
257
|
+
|
|
240
258
|
const responseEncoding = reply.getHeader('Content-Encoding')
|
|
241
259
|
if (responseEncoding && responseEncoding !== 'identity') {
|
|
242
260
|
// response is already compressed
|
|
@@ -273,10 +291,18 @@ function buildRouteCompress (_fastify, params, routeOptions, decorateOnly) {
|
|
|
273
291
|
}
|
|
274
292
|
|
|
275
293
|
if (typeof payload.pipe !== 'function') {
|
|
276
|
-
if (
|
|
277
|
-
|
|
294
|
+
if (isFetchResponse(payload)) {
|
|
295
|
+
payload = payload.body
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (isWebReadableStream(payload)) {
|
|
299
|
+
payload = webStreamToNodeReadable(payload)
|
|
300
|
+
} else {
|
|
301
|
+
if (Buffer.byteLength(payload) < params.threshold) {
|
|
302
|
+
return next()
|
|
303
|
+
}
|
|
304
|
+
payload = Readable.from(intoAsyncIterator(payload))
|
|
278
305
|
}
|
|
279
|
-
payload = Readable.from(intoAsyncIterator(payload))
|
|
280
306
|
}
|
|
281
307
|
|
|
282
308
|
setVaryHeader(reply)
|
|
@@ -391,7 +417,13 @@ function compress (params) {
|
|
|
391
417
|
}
|
|
392
418
|
|
|
393
419
|
if (typeof payload.pipe !== 'function') {
|
|
394
|
-
if (
|
|
420
|
+
if (isFetchResponse(payload)) {
|
|
421
|
+
payload = payload.body
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (isWebReadableStream(payload)) {
|
|
425
|
+
payload = webStreamToNodeReadable(payload)
|
|
426
|
+
} else if (!Buffer.isBuffer(payload) && typeof payload !== 'string') {
|
|
395
427
|
payload = this.serialize(payload)
|
|
396
428
|
}
|
|
397
429
|
}
|
package/lib/utils.js
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { Readable: NodeReadable } = require('node:stream')
|
|
4
|
+
|
|
5
|
+
// https://datatracker.ietf.org/doc/html/rfc8878#section-3.1.1
|
|
6
|
+
function isZstd (buffer) {
|
|
7
|
+
return (
|
|
8
|
+
typeof buffer === 'object' &&
|
|
9
|
+
buffer !== null &&
|
|
10
|
+
buffer.length > 3 &&
|
|
11
|
+
// Zstd magic number: 0xFD2FB528 (little-endian)
|
|
12
|
+
buffer[0] === 0x28 &&
|
|
13
|
+
buffer[1] === 0xb5 &&
|
|
14
|
+
buffer[2] === 0x2f &&
|
|
15
|
+
buffer[3] === 0xfd
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
3
19
|
// https://datatracker.ietf.org/doc/html/rfc1950#section-2
|
|
4
20
|
function isDeflate (buffer) {
|
|
5
21
|
return (
|
|
@@ -35,6 +51,18 @@ function isStream (stream) {
|
|
|
35
51
|
return stream !== null && typeof stream === 'object' && typeof stream.pipe === 'function'
|
|
36
52
|
}
|
|
37
53
|
|
|
54
|
+
function isWebReadableStream (obj) {
|
|
55
|
+
return obj instanceof ReadableStream
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isFetchResponse (obj) {
|
|
59
|
+
return obj instanceof Response
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function webStreamToNodeReadable (webStream) {
|
|
63
|
+
return NodeReadable.fromWeb(webStream)
|
|
64
|
+
}
|
|
65
|
+
|
|
38
66
|
/**
|
|
39
67
|
* Provide a async iteratable for Readable.from
|
|
40
68
|
*/
|
|
@@ -76,4 +104,4 @@ async function * intoAsyncIterator (payload) {
|
|
|
76
104
|
yield payload
|
|
77
105
|
}
|
|
78
106
|
|
|
79
|
-
module.exports = { isGzip, isDeflate, isStream, intoAsyncIterator }
|
|
107
|
+
module.exports = { isZstd, isGzip, isDeflate, isStream, intoAsyncIterator, isWebReadableStream, isFetchResponse, webStreamToNodeReadable }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fastify/compress",
|
|
3
|
-
"version": "8.0
|
|
3
|
+
"version": "8.2.0",
|
|
4
4
|
"description": "Fastify compression utils",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -16,16 +16,15 @@
|
|
|
16
16
|
"readable-stream": "^4.5.2"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"@
|
|
20
|
-
"@types/node": "^22.0.0",
|
|
19
|
+
"@types/node": "^24.0.8",
|
|
21
20
|
"adm-zip": "^0.5.12",
|
|
22
21
|
"c8": "^10.1.2",
|
|
23
22
|
"eslint": "^9.17.0",
|
|
24
23
|
"fastify": "^5.0.0",
|
|
25
24
|
"jsonstream": "^1.0.3",
|
|
26
25
|
"neostandard": "^0.12.0",
|
|
27
|
-
"tsd": "^0.
|
|
28
|
-
"typescript": "~5.
|
|
26
|
+
"tsd": "^0.33.0",
|
|
27
|
+
"typescript": "~5.9.2"
|
|
29
28
|
},
|
|
30
29
|
"scripts": {
|
|
31
30
|
"lint": "eslint",
|
|
@@ -41,7 +40,8 @@
|
|
|
41
40
|
"compression",
|
|
42
41
|
"deflate",
|
|
43
42
|
"gzip",
|
|
44
|
-
"brotli"
|
|
43
|
+
"brotli",
|
|
44
|
+
"zstd"
|
|
45
45
|
],
|
|
46
46
|
"author": "Tomas Della Vedova - @delvedor (http://delved.org)",
|
|
47
47
|
"contributors": [
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"url": "git+https://github.com/fastify/fastify-compress.git"
|
|
84
84
|
},
|
|
85
85
|
"tsd": {
|
|
86
|
-
"directory": "
|
|
86
|
+
"directory": "types"
|
|
87
87
|
},
|
|
88
88
|
"publishConfig": {
|
|
89
89
|
"access": "public"
|
|
@@ -9,6 +9,7 @@ const JSONStream = require('jsonstream')
|
|
|
9
9
|
const Fastify = require('fastify')
|
|
10
10
|
const compressPlugin = require('../index')
|
|
11
11
|
const { once } = require('node:events')
|
|
12
|
+
const { ReadableStream: WebReadableStream, Response } = globalThis
|
|
12
13
|
|
|
13
14
|
describe('When `global` is not set, it is `true` by default :', async () => {
|
|
14
15
|
test('it should compress Buffer data using brotli when `Accept-Encoding` request header is `br`', async (t) => {
|
|
@@ -33,6 +34,32 @@ describe('When `global` is not set, it is `true` by default :', async () => {
|
|
|
33
34
|
t.assert.equal(payload.toString('utf-8'), buf.toString())
|
|
34
35
|
})
|
|
35
36
|
|
|
37
|
+
test('it should compress Buffer data using zstd when `Accept-Encoding` request header is `zstd`', async (t) => {
|
|
38
|
+
if (typeof zlib.createZstdCompress !== 'function') {
|
|
39
|
+
t.skip('zstd not supported in this Node.js version')
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
t.plan(1)
|
|
43
|
+
|
|
44
|
+
const fastify = Fastify()
|
|
45
|
+
await fastify.register(compressPlugin, { threshold: 0 })
|
|
46
|
+
|
|
47
|
+
const buf = Buffer.from('hello world')
|
|
48
|
+
fastify.get('/', (_request, reply) => {
|
|
49
|
+
reply.send(buf)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const response = await fastify.inject({
|
|
53
|
+
url: '/',
|
|
54
|
+
method: 'GET',
|
|
55
|
+
headers: {
|
|
56
|
+
'accept-encoding': 'zstd'
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
const payload = zlib.zstdDecompressSync(response.rawPayload)
|
|
60
|
+
t.assert.equal(payload.toString('utf-8'), buf.toString())
|
|
61
|
+
})
|
|
62
|
+
|
|
36
63
|
test('it should compress Buffer data using deflate when `Accept-Encoding` request header is `deflate`', async (t) => {
|
|
37
64
|
t.plan(1)
|
|
38
65
|
|
|
@@ -100,6 +127,32 @@ describe('When `global` is not set, it is `true` by default :', async () => {
|
|
|
100
127
|
t.assert.equal(payload.toString('utf-8'), JSON.stringify(json))
|
|
101
128
|
})
|
|
102
129
|
|
|
130
|
+
test('it should compress JSON data using zstd when `Accept-Encoding` request header is `zstd`', async (t) => {
|
|
131
|
+
if (typeof zlib.createZstdCompress !== 'function') {
|
|
132
|
+
t.skip('zstd not supported in this Node.js version')
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
t.plan(1)
|
|
136
|
+
|
|
137
|
+
const fastify = Fastify()
|
|
138
|
+
await fastify.register(compressPlugin, { threshold: 0 })
|
|
139
|
+
|
|
140
|
+
const json = { hello: 'world' }
|
|
141
|
+
fastify.get('/', (_request, reply) => {
|
|
142
|
+
reply.send(json)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
const response = await fastify.inject({
|
|
146
|
+
url: '/',
|
|
147
|
+
method: 'GET',
|
|
148
|
+
headers: {
|
|
149
|
+
'accept-encoding': 'zstd'
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
const payload = zlib.zstdDecompressSync(response.rawPayload)
|
|
153
|
+
t.assert.equal(payload.toString('utf-8'), JSON.stringify(json))
|
|
154
|
+
})
|
|
155
|
+
|
|
103
156
|
test('it should compress JSON data using deflate when `Accept-Encoding` request header is `deflate`', async (t) => {
|
|
104
157
|
t.plan(1)
|
|
105
158
|
|
|
@@ -213,6 +266,54 @@ describe('When `global` is not set, it is `true` by default :', async () => {
|
|
|
213
266
|
const payload = zlib.gunzipSync(response.rawPayload)
|
|
214
267
|
t.assert.equal(payload.toString('utf-8'), 'hello')
|
|
215
268
|
})
|
|
269
|
+
|
|
270
|
+
test('it should compress a Fetch API Response body', async (t) => {
|
|
271
|
+
t.plan(1)
|
|
272
|
+
|
|
273
|
+
const fastify = Fastify()
|
|
274
|
+
await fastify.register(compressPlugin, { threshold: 0 })
|
|
275
|
+
|
|
276
|
+
const body = 'hello from fetch response'
|
|
277
|
+
fastify.get('/fetch-resp', (_request, reply) => {
|
|
278
|
+
const resp = new Response(body, { headers: { 'content-type': 'text/plain' } })
|
|
279
|
+
reply.send(resp)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
const response = await fastify.inject({
|
|
283
|
+
url: '/fetch-resp',
|
|
284
|
+
method: 'GET',
|
|
285
|
+
headers: { 'accept-encoding': 'gzip' }
|
|
286
|
+
})
|
|
287
|
+
const payload = zlib.gunzipSync(response.rawPayload)
|
|
288
|
+
t.assert.equal(payload.toString('utf-8'), body)
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
test('it should compress a Web ReadableStream body', async (t) => {
|
|
292
|
+
t.plan(1)
|
|
293
|
+
|
|
294
|
+
const fastify = Fastify()
|
|
295
|
+
await fastify.register(compressPlugin, { threshold: 0 })
|
|
296
|
+
|
|
297
|
+
const body = 'hello from web stream'
|
|
298
|
+
fastify.get('/web-stream', (_request, reply) => {
|
|
299
|
+
const stream = new WebReadableStream({
|
|
300
|
+
start (controller) {
|
|
301
|
+
controller.enqueue(Buffer.from(body))
|
|
302
|
+
controller.close()
|
|
303
|
+
}
|
|
304
|
+
})
|
|
305
|
+
reply.header('content-type', 'text/plain')
|
|
306
|
+
reply.send(stream)
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
const response = await fastify.inject({
|
|
310
|
+
url: '/web-stream',
|
|
311
|
+
method: 'GET',
|
|
312
|
+
headers: { 'accept-encoding': 'gzip' }
|
|
313
|
+
})
|
|
314
|
+
const payload = zlib.gunzipSync(response.rawPayload)
|
|
315
|
+
t.assert.equal(payload.toString('utf-8'), body)
|
|
316
|
+
})
|
|
216
317
|
})
|
|
217
318
|
|
|
218
319
|
describe('It should send compressed Stream data when `global` is `true` :', async () => {
|
|
@@ -42,6 +42,33 @@ describe('It should decompress the request payload :', async () => {
|
|
|
42
42
|
t.assert.equal(response.body, '@fastify/compress')
|
|
43
43
|
})
|
|
44
44
|
|
|
45
|
+
test('using zstd algorithm when `Content-Encoding` request header value is set to `zstd`', async (t) => {
|
|
46
|
+
if (typeof zlib.createZstdCompress !== 'function') {
|
|
47
|
+
t.skip('zstd not supported in this Node.js version')
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
t.plan(2)
|
|
51
|
+
|
|
52
|
+
const fastify = Fastify()
|
|
53
|
+
await fastify.register(compressPlugin)
|
|
54
|
+
|
|
55
|
+
fastify.post('/', (request, reply) => {
|
|
56
|
+
reply.send(request.body.name)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const response = await fastify.inject({
|
|
60
|
+
url: '/',
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: {
|
|
63
|
+
'content-type': 'application/json',
|
|
64
|
+
'content-encoding': 'zstd'
|
|
65
|
+
},
|
|
66
|
+
payload: createPayload(zlib.createZstdCompress)
|
|
67
|
+
})
|
|
68
|
+
t.assert.equal(response.statusCode, 200)
|
|
69
|
+
t.assert.equal(response.body, '@fastify/compress')
|
|
70
|
+
})
|
|
71
|
+
|
|
45
72
|
test('using deflate algorithm when `Content-Encoding` request header value is set to `deflate`', async (t) => {
|
|
46
73
|
t.plan(2)
|
|
47
74
|
|
|
@@ -440,3 +440,33 @@ test('It should avoid to trigger `onSend` hook twice', async (t) => {
|
|
|
440
440
|
})
|
|
441
441
|
t.assert.deepEqual(JSON.parse(zlib.brotliDecompressSync(response.rawPayload)), { hi: true })
|
|
442
442
|
})
|
|
443
|
+
|
|
444
|
+
test('reply.compress should handle Fetch Response', async (t) => {
|
|
445
|
+
t.plan(1)
|
|
446
|
+
const fastify = Fastify()
|
|
447
|
+
await fastify.register(compressPlugin, { global: true, threshold: 0 })
|
|
448
|
+
fastify.get('/', (_req, reply) => {
|
|
449
|
+
const r = new Response('from reply.compress', { headers: { 'content-type': 'text/plain' } })
|
|
450
|
+
reply.compress(r)
|
|
451
|
+
})
|
|
452
|
+
const res = await fastify.inject({ url: '/', method: 'GET', headers: { 'accept-encoding': 'gzip' } })
|
|
453
|
+
t.assert.equal(zlib.gunzipSync(res.rawPayload).toString('utf8'), 'from reply.compress')
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
test('reply.compress should handle Web ReadableStream', async (t) => {
|
|
457
|
+
t.plan(1)
|
|
458
|
+
const fastify = Fastify()
|
|
459
|
+
await fastify.register(compressPlugin, { global: true, threshold: 0 })
|
|
460
|
+
fastify.get('/', (_req, reply) => {
|
|
461
|
+
const stream = new ReadableStream({
|
|
462
|
+
start (controller) {
|
|
463
|
+
controller.enqueue(Buffer.from('from webstream'))
|
|
464
|
+
controller.close()
|
|
465
|
+
}
|
|
466
|
+
})
|
|
467
|
+
reply.header('content-type', 'text/plain')
|
|
468
|
+
reply.compress(stream)
|
|
469
|
+
})
|
|
470
|
+
const res = await fastify.inject({ url: '/', method: 'GET', headers: { 'accept-encoding': 'gzip' } })
|
|
471
|
+
t.assert.equal(zlib.gunzipSync(res.rawPayload).toString('utf8'), 'from webstream')
|
|
472
|
+
})
|
package/test/utils.test.js
CHANGED
|
@@ -4,7 +4,7 @@ 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
6
|
const { test } = require('node:test')
|
|
7
|
-
const { isStream, isDeflate, isGzip, intoAsyncIterator } = require('../lib/utils')
|
|
7
|
+
const { isStream, isZstd, isDeflate, isGzip, intoAsyncIterator } = require('../lib/utils')
|
|
8
8
|
|
|
9
9
|
test('isStream() utility should be able to detect Streams', async (t) => {
|
|
10
10
|
t.plan(12)
|
|
@@ -48,6 +48,23 @@ test('isDeflate() utility should be able to detect deflate compressed Buffer', a
|
|
|
48
48
|
equal(isDeflate(''), false)
|
|
49
49
|
})
|
|
50
50
|
|
|
51
|
+
test('isZstd() utility should be able to detect zstd compressed Buffer', async (t) => {
|
|
52
|
+
t.plan(10)
|
|
53
|
+
const equal = t.assert.equal
|
|
54
|
+
|
|
55
|
+
equal(isZstd(Buffer.alloc(0)), false)
|
|
56
|
+
equal(isZstd(Buffer.alloc(1)), false)
|
|
57
|
+
equal(isZstd(Buffer.alloc(2)), false)
|
|
58
|
+
equal(isZstd(Buffer.alloc(3)), false)
|
|
59
|
+
equal(isZstd(Buffer.from([0x28, 0xb5, 0x2f])), false)
|
|
60
|
+
equal(isZstd(Buffer.from([0x28, 0xb5, 0x2f, 0xfd])), true)
|
|
61
|
+
|
|
62
|
+
equal(isZstd({}), false)
|
|
63
|
+
equal(isZstd(null), false)
|
|
64
|
+
equal(isZstd(undefined), false)
|
|
65
|
+
equal(isZstd(''), false)
|
|
66
|
+
})
|
|
67
|
+
|
|
51
68
|
test('isGzip() utility should be able to detect gzip compressed Buffer', async (t) => {
|
|
52
69
|
t.plan(10)
|
|
53
70
|
const equal = t.assert.equal
|
package/types/index.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ declare module 'fastify' {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
interface FastifyReply {
|
|
29
|
-
compress(input: Stream | Input): void;
|
|
29
|
+
compress(input: Stream | Input | Response | ReadableStream): void;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export interface RouteOptions {
|
|
@@ -57,7 +57,7 @@ type RouteDecompressOptions = Pick<fastifyCompress.FastifyCompressOptions,
|
|
|
57
57
|
| 'zlib'
|
|
58
58
|
>
|
|
59
59
|
|
|
60
|
-
type EncodingToken = 'br' | 'deflate' | 'gzip' | 'identity'
|
|
60
|
+
type EncodingToken = 'zstd' | 'br' | 'deflate' | 'gzip' | 'identity'
|
|
61
61
|
|
|
62
62
|
type CompressibleContentTypeFunction = (contentType: string) => boolean
|
|
63
63
|
|
package/types/index.test-d.ts
CHANGED
|
@@ -25,8 +25,14 @@ const withGlobalOptions: FastifyCompressOptions = {
|
|
|
25
25
|
removeContentLengthHeader: true
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
const withZstdOptions: FastifyCompressOptions = {
|
|
29
|
+
encodings: ['zstd', 'br', 'gzip', 'deflate', 'identity'],
|
|
30
|
+
requestEncodings: ['zstd', 'br', 'gzip', 'deflate', 'identity']
|
|
31
|
+
}
|
|
32
|
+
|
|
28
33
|
const app: FastifyInstance = fastify()
|
|
29
34
|
app.register(fastifyCompress, withGlobalOptions)
|
|
35
|
+
app.register(fastifyCompress, withZstdOptions)
|
|
30
36
|
|
|
31
37
|
app.register(fastifyCompress, {
|
|
32
38
|
customTypes: value => value === 'application/json'
|
|
@@ -111,9 +117,33 @@ appWithoutGlobal.inject(
|
|
|
111
117
|
}
|
|
112
118
|
)
|
|
113
119
|
|
|
120
|
+
// Test that invalid encoding values trigger TypeScript errors
|
|
121
|
+
expectError(fastify().register(fastifyCompress, {
|
|
122
|
+
encodings: ['invalid-encoding']
|
|
123
|
+
}))
|
|
124
|
+
|
|
125
|
+
expectError(fastify().register(fastifyCompress, {
|
|
126
|
+
requestEncodings: ['another-invalid-encoding']
|
|
127
|
+
}))
|
|
128
|
+
|
|
114
129
|
// Instantiation of an app that should trigger a typescript error
|
|
115
130
|
const appThatTriggerAnError = fastify()
|
|
116
131
|
expectError(appThatTriggerAnError.register(fastifyCompress, {
|
|
117
132
|
global: true,
|
|
118
133
|
thisOptionDoesNotExist: 'trigger a typescript error'
|
|
119
134
|
}))
|
|
135
|
+
|
|
136
|
+
app.get('/ts-fetch-response', async (_request, reply) => {
|
|
137
|
+
const resp = new Response('ok', { headers: { 'content-type': 'text/plain' } })
|
|
138
|
+
expectType<void>(reply.compress(resp))
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
app.get('/ts-web-readable-stream', async (_request, reply) => {
|
|
142
|
+
const stream = new ReadableStream({
|
|
143
|
+
start (controller) {
|
|
144
|
+
controller.enqueue(new Uint8Array([1, 2, 3]))
|
|
145
|
+
controller.close()
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
expectType<void>(reply.compress(stream))
|
|
149
|
+
})
|