@fastify/compress 8.1.0 → 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/README.md +47 -4
- package/index.js +20 -5
- package/lib/utils.js +15 -1
- package/package.json +5 -6
- package/test/global-compress.test.js +49 -0
- package/test/routes-compress.test.js +30 -0
- package/types/index.d.ts +1 -1
- package/types/index.test-d.ts +15 -0
- package/.claude/settings.local.json +0 -8
- package/CLAUDE.md +0 -64
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/README.md
CHANGED
|
@@ -106,6 +106,40 @@ app.get('/', (req, reply) => {
|
|
|
106
106
|
await app.listen({ port: 3000 })
|
|
107
107
|
```
|
|
108
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
|
+
|
|
109
143
|
## Compress Options
|
|
110
144
|
|
|
111
145
|
### threshold
|
|
@@ -222,15 +256,24 @@ This plugin adds a `preParsing` hook to decompress the request payload based on
|
|
|
222
256
|
|
|
223
257
|
Currently, the following encoding tokens are supported:
|
|
224
258
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
259
|
+
- `zstd` (Node.js 22.15+/23.8+)
|
|
260
|
+
- `br`
|
|
261
|
+
- `gzip`
|
|
262
|
+
- `deflate`
|
|
229
263
|
|
|
230
264
|
If an unsupported encoding or invalid payload is received, the plugin throws an error.
|
|
231
265
|
|
|
232
266
|
If the request header is missing, the plugin yields to the next hook.
|
|
233
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
|
+
|
|
234
277
|
### Global hook
|
|
235
278
|
|
|
236
279
|
The global request decompression hook is enabled by default. To disable it, pass `{ global: false }`:
|
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)
|
|
@@ -254,6 +254,7 @@ function buildRouteCompress (_fastify, params, routeOptions, decorateOnly) {
|
|
|
254
254
|
if (payload == null) {
|
|
255
255
|
return next()
|
|
256
256
|
}
|
|
257
|
+
|
|
257
258
|
const responseEncoding = reply.getHeader('Content-Encoding')
|
|
258
259
|
if (responseEncoding && responseEncoding !== 'identity') {
|
|
259
260
|
// response is already compressed
|
|
@@ -290,10 +291,18 @@ function buildRouteCompress (_fastify, params, routeOptions, decorateOnly) {
|
|
|
290
291
|
}
|
|
291
292
|
|
|
292
293
|
if (typeof payload.pipe !== 'function') {
|
|
293
|
-
if (
|
|
294
|
-
|
|
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))
|
|
295
305
|
}
|
|
296
|
-
payload = Readable.from(intoAsyncIterator(payload))
|
|
297
306
|
}
|
|
298
307
|
|
|
299
308
|
setVaryHeader(reply)
|
|
@@ -408,7 +417,13 @@ function compress (params) {
|
|
|
408
417
|
}
|
|
409
418
|
|
|
410
419
|
if (typeof payload.pipe !== 'function') {
|
|
411
|
-
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') {
|
|
412
427
|
payload = this.serialize(payload)
|
|
413
428
|
}
|
|
414
429
|
}
|
package/lib/utils.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { Readable: NodeReadable } = require('node:stream')
|
|
4
|
+
|
|
3
5
|
// https://datatracker.ietf.org/doc/html/rfc8878#section-3.1.1
|
|
4
6
|
function isZstd (buffer) {
|
|
5
7
|
return (
|
|
@@ -49,6 +51,18 @@ function isStream (stream) {
|
|
|
49
51
|
return stream !== null && typeof stream === 'object' && typeof stream.pipe === 'function'
|
|
50
52
|
}
|
|
51
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
|
+
|
|
52
66
|
/**
|
|
53
67
|
* Provide a async iteratable for Readable.from
|
|
54
68
|
*/
|
|
@@ -90,4 +104,4 @@ async function * intoAsyncIterator (payload) {
|
|
|
90
104
|
yield payload
|
|
91
105
|
}
|
|
92
106
|
|
|
93
|
-
module.exports = { isZstd, 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.
|
|
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",
|
|
@@ -84,7 +83,7 @@
|
|
|
84
83
|
"url": "git+https://github.com/fastify/fastify-compress.git"
|
|
85
84
|
},
|
|
86
85
|
"tsd": {
|
|
87
|
-
"directory": "
|
|
86
|
+
"directory": "types"
|
|
88
87
|
},
|
|
89
88
|
"publishConfig": {
|
|
90
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) => {
|
|
@@ -265,6 +266,54 @@ describe('When `global` is not set, it is `true` by default :', async () => {
|
|
|
265
266
|
const payload = zlib.gunzipSync(response.rawPayload)
|
|
266
267
|
t.assert.equal(payload.toString('utf-8'), 'hello')
|
|
267
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
|
+
})
|
|
268
317
|
})
|
|
269
318
|
|
|
270
319
|
describe('It should send compressed Stream data when `global` is `true` :', async () => {
|
|
@@ -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/types/index.d.ts
CHANGED
package/types/index.test-d.ts
CHANGED
|
@@ -132,3 +132,18 @@ expectError(appThatTriggerAnError.register(fastifyCompress, {
|
|
|
132
132
|
global: true,
|
|
133
133
|
thisOptionDoesNotExist: 'trigger a typescript error'
|
|
134
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
|
+
})
|
package/CLAUDE.md
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
# CLAUDE.md
|
|
2
|
-
|
|
3
|
-
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
-
|
|
5
|
-
## Development Commands
|
|
6
|
-
|
|
7
|
-
### Testing
|
|
8
|
-
- `npm run test` - Run both unit and TypeScript tests
|
|
9
|
-
- `npm run test:unit` - Run unit tests with Node.js test runner
|
|
10
|
-
- `npm run test:coverage` - Run tests with coverage reporting
|
|
11
|
-
- `npm run lint` - Run ESLint for code style checking
|
|
12
|
-
- `npm run lint:fix` - Auto-fix linting issues
|
|
13
|
-
|
|
14
|
-
### Running Single Tests
|
|
15
|
-
Use Node.js test runner with file path:
|
|
16
|
-
```bash
|
|
17
|
-
node --test test/global-compress.test.js
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
## Architecture Overview
|
|
21
|
-
|
|
22
|
-
This is a Fastify plugin (`@fastify/compress`) that provides compression and decompression capabilities for HTTP requests and responses.
|
|
23
|
-
|
|
24
|
-
### Core Components
|
|
25
|
-
|
|
26
|
-
**Main Plugin (`index.js`)**: 574-line plugin implementation using Fastify's hook system:
|
|
27
|
-
- `onSend` hook for response compression
|
|
28
|
-
- `preParsing` hook for request decompression
|
|
29
|
-
- `onRoute` hook for route-level configuration
|
|
30
|
-
|
|
31
|
-
**Utilities (`lib/utils.js`)**: Core compression detection and stream handling:
|
|
32
|
-
- `isZstd()` / `isGzip()` / `isDeflate()` - Magic byte detection (RFC 8878/1952/1950)
|
|
33
|
-
- `isStream()` - Stream type checking
|
|
34
|
-
- `intoAsyncIterator()` - Payload conversion utilities
|
|
35
|
-
|
|
36
|
-
**TypeScript Support (`types/index.d.ts`)**: Full type definitions with Fastify module augmentation
|
|
37
|
-
|
|
38
|
-
### Compression System
|
|
39
|
-
|
|
40
|
-
**Supported Encodings**: zstd (Node.js 22.15+/23.8+), brotli (`br`), gzip, deflate, identity
|
|
41
|
-
**Priority Order**: zstd > br > gzip > deflate > * (defaults to gzip) > identity
|
|
42
|
-
**Threshold-based**: Default 1024 bytes minimum for compression
|
|
43
|
-
**Content-Type Aware**: Uses `mime-db` for compressible content detection
|
|
44
|
-
|
|
45
|
-
### Configuration Levels
|
|
46
|
-
1. **Global**: Applied to all routes via plugin options
|
|
47
|
-
2. **Route-level**: Override global settings in route config
|
|
48
|
-
3. **Runtime**: Manual compression via `reply.compress()`
|
|
49
|
-
|
|
50
|
-
### Key Dependencies
|
|
51
|
-
- `@fastify/accept-negotiator` - Content encoding negotiation
|
|
52
|
-
- `fastify-plugin` - Plugin registration
|
|
53
|
-
- `mime-db` - MIME type database
|
|
54
|
-
- `minipass`, `pump`, `pumpify` - Stream processing
|
|
55
|
-
|
|
56
|
-
### Testing Structure
|
|
57
|
-
Uses Node.js built-in test runner with describe/test pattern. Test files are organized by functionality:
|
|
58
|
-
- `global-compress.test.js` / `global-decompress.test.js` - Global functionality
|
|
59
|
-
- `routes-compress.test.js` / `routes-decompress.test.js` - Route-level functionality
|
|
60
|
-
- `utils.test.js` - Utility functions
|
|
61
|
-
- `regression/` - Regression tests
|
|
62
|
-
|
|
63
|
-
### Code Style
|
|
64
|
-
Uses `neostandard` (ESLint-based) with TypeScript support. Configuration in `eslint.config.js`.
|