@fastify/compress 8.0.3 → 8.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.
@@ -0,0 +1,8 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "mcp__ripgrep__search"
5
+ ],
6
+ "deny": []
7
+ }
8
+ }
package/CLAUDE.md ADDED
@@ -0,0 +1,64 @@
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`.
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
  [![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](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 `brotli`.
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. `br`
41
- 2. `gzip`
42
- 3. `deflate`
43
- 4. `*` (no preference — `@fastify/compress` will use `gzip`)
44
- 5. `identity` (no compression)
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.
@@ -175,6 +176,13 @@ await fastify.register(
175
176
  // Only support gzip and deflate, and prefer deflate to gzip
176
177
  { encodings: ['deflate', 'gzip'] }
177
178
  )
179
+
180
+ // Example with zstd support (Node.js 22.15+/23.8+)
181
+ await fastify.register(
182
+ import('@fastify/compress'),
183
+ // Prefer zstd, fallback to brotli, then gzip
184
+ { encodings: ['zstd', 'br', 'gzip'] }
185
+ )
178
186
  ```
179
187
 
180
188
  ### brotliOptions and zlibOptions
@@ -214,9 +222,10 @@ This plugin adds a `preParsing` hook to decompress the request payload based on
214
222
 
215
223
  Currently, the following encoding tokens are supported:
216
224
 
217
- 1. `br`
218
- 2. `gzip`
219
- 3. `deflate`
225
+ 1. `zstd` (Node.js 22.15+/23.8+)
226
+ 2. `br`
227
+ 3. `gzip`
228
+ 4. `deflate`
220
229
 
221
230
  If an unsupported encoding or invalid payload is received, the plugin throws an error.
222
231
 
@@ -268,6 +277,13 @@ await fastify.register(
268
277
  // Only support gzip
269
278
  { requestEncodings: ['gzip'] }
270
279
  )
280
+
281
+ // Example with zstd support for request decompression (Node.js 22.15+/23.8+)
282
+ await fastify.register(
283
+ import('@fastify/compress'),
284
+ // Support zstd, brotli and gzip for request decompression
285
+ { requestEncodings: ['zstd', 'br', 'gzip'] }
286
+ )
271
287
  ```
272
288
 
273
289
  ### forceRequestEncoding
package/index.js CHANGED
@@ -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
package/lib/utils.js CHANGED
@@ -1,5 +1,19 @@
1
1
  'use strict'
2
2
 
3
+ // https://datatracker.ietf.org/doc/html/rfc8878#section-3.1.1
4
+ function isZstd (buffer) {
5
+ return (
6
+ typeof buffer === 'object' &&
7
+ buffer !== null &&
8
+ buffer.length > 3 &&
9
+ // Zstd magic number: 0xFD2FB528 (little-endian)
10
+ buffer[0] === 0x28 &&
11
+ buffer[1] === 0xb5 &&
12
+ buffer[2] === 0x2f &&
13
+ buffer[3] === 0xfd
14
+ )
15
+ }
16
+
3
17
  // https://datatracker.ietf.org/doc/html/rfc1950#section-2
4
18
  function isDeflate (buffer) {
5
19
  return (
@@ -76,4 +90,4 @@ async function * intoAsyncIterator (payload) {
76
90
  yield payload
77
91
  }
78
92
 
79
- module.exports = { isGzip, isDeflate, isStream, intoAsyncIterator }
93
+ module.exports = { isZstd, isGzip, isDeflate, isStream, intoAsyncIterator }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fastify/compress",
3
- "version": "8.0.3",
3
+ "version": "8.1.0",
4
4
  "description": "Fastify compression utils",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
@@ -24,8 +24,8 @@
24
24
  "fastify": "^5.0.0",
25
25
  "jsonstream": "^1.0.3",
26
26
  "neostandard": "^0.12.0",
27
- "tsd": "^0.31.0",
28
- "typescript": "~5.7.2"
27
+ "tsd": "^0.32.0",
28
+ "typescript": "~5.8.3"
29
29
  },
30
30
  "scripts": {
31
31
  "lint": "eslint",
@@ -41,7 +41,8 @@
41
41
  "compression",
42
42
  "deflate",
43
43
  "gzip",
44
- "brotli"
44
+ "brotli",
45
+ "zstd"
45
46
  ],
46
47
  "author": "Tomas Della Vedova - @delvedor (http://delved.org)",
47
48
  "contributors": [
@@ -33,6 +33,32 @@ describe('When `global` is not set, it is `true` by default :', async () => {
33
33
  t.assert.equal(payload.toString('utf-8'), buf.toString())
34
34
  })
35
35
 
36
+ test('it should compress Buffer data using zstd when `Accept-Encoding` request header is `zstd`', async (t) => {
37
+ if (typeof zlib.createZstdCompress !== 'function') {
38
+ t.skip('zstd not supported in this Node.js version')
39
+ return
40
+ }
41
+ t.plan(1)
42
+
43
+ const fastify = Fastify()
44
+ await fastify.register(compressPlugin, { threshold: 0 })
45
+
46
+ const buf = Buffer.from('hello world')
47
+ fastify.get('/', (_request, reply) => {
48
+ reply.send(buf)
49
+ })
50
+
51
+ const response = await fastify.inject({
52
+ url: '/',
53
+ method: 'GET',
54
+ headers: {
55
+ 'accept-encoding': 'zstd'
56
+ }
57
+ })
58
+ const payload = zlib.zstdDecompressSync(response.rawPayload)
59
+ t.assert.equal(payload.toString('utf-8'), buf.toString())
60
+ })
61
+
36
62
  test('it should compress Buffer data using deflate when `Accept-Encoding` request header is `deflate`', async (t) => {
37
63
  t.plan(1)
38
64
 
@@ -100,6 +126,32 @@ describe('When `global` is not set, it is `true` by default :', async () => {
100
126
  t.assert.equal(payload.toString('utf-8'), JSON.stringify(json))
101
127
  })
102
128
 
129
+ test('it should compress JSON data using zstd when `Accept-Encoding` request header is `zstd`', async (t) => {
130
+ if (typeof zlib.createZstdCompress !== 'function') {
131
+ t.skip('zstd not supported in this Node.js version')
132
+ return
133
+ }
134
+ t.plan(1)
135
+
136
+ const fastify = Fastify()
137
+ await fastify.register(compressPlugin, { threshold: 0 })
138
+
139
+ const json = { hello: 'world' }
140
+ fastify.get('/', (_request, reply) => {
141
+ reply.send(json)
142
+ })
143
+
144
+ const response = await fastify.inject({
145
+ url: '/',
146
+ method: 'GET',
147
+ headers: {
148
+ 'accept-encoding': 'zstd'
149
+ }
150
+ })
151
+ const payload = zlib.zstdDecompressSync(response.rawPayload)
152
+ t.assert.equal(payload.toString('utf-8'), JSON.stringify(json))
153
+ })
154
+
103
155
  test('it should compress JSON data using deflate when `Accept-Encoding` request header is `deflate`', async (t) => {
104
156
  t.plan(1)
105
157
 
@@ -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
 
@@ -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
@@ -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
 
@@ -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,6 +117,15 @@ 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, {