@fastify/compress 8.3.1 → 9.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.c8rc +2 -6
- package/.github/dependabot.yml +42 -2
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/lock-threads.yml +19 -0
- package/LICENSE +1 -3
- package/README.md +2 -3
- package/index.js +42 -29
- package/lib/utils.js +90 -1
- package/package.json +9 -15
- package/test/global-compress.test.js +119 -0
- package/test/global-decompress.test.js +4 -2
- package/test/regression/issue-382.test.js +193 -0
- package/test/regression/issue-410.test.js +163 -0
- package/test/routes-compress.test.js +0 -89
- package/test/routes-decompress.test.js +4 -87
- package/test/utils.test.js +138 -1
- package/types/index.d.ts +0 -14
- package/types/{index.test-d.ts → index.tst.ts} +31 -31
- package/.github/stale.yml +0 -21
package/.c8rc
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"all": true,
|
|
3
|
-
"exclude": [
|
|
4
|
-
"test/",
|
|
5
|
-
"coverage/",
|
|
6
|
-
"types/"
|
|
7
|
-
],
|
|
3
|
+
"exclude": ["test/", "coverage/", "types/", "eslint.config.js"],
|
|
8
4
|
"clean": true,
|
|
9
5
|
"check-coverage": true,
|
|
10
6
|
"branches": 100,
|
|
11
7
|
"lines": 100,
|
|
12
8
|
"functions": 100,
|
|
13
9
|
"statements": 100
|
|
14
|
-
}
|
|
10
|
+
}
|
package/.github/dependabot.yml
CHANGED
|
@@ -2,12 +2,52 @@ version: 2
|
|
|
2
2
|
updates:
|
|
3
3
|
- package-ecosystem: "github-actions"
|
|
4
4
|
directory: "/"
|
|
5
|
+
commit-message:
|
|
6
|
+
# Prefix all commit messages with "chore: "
|
|
7
|
+
prefix: "chore"
|
|
5
8
|
schedule:
|
|
6
|
-
interval: "
|
|
9
|
+
interval: "weekly"
|
|
10
|
+
cooldown:
|
|
11
|
+
default-days: 7
|
|
12
|
+
allow:
|
|
13
|
+
- dependency-name: "*"
|
|
14
|
+
update-types:
|
|
15
|
+
- "version-update:semver-major"
|
|
7
16
|
open-pull-requests-limit: 10
|
|
8
17
|
|
|
9
18
|
- package-ecosystem: "npm"
|
|
10
19
|
directory: "/"
|
|
20
|
+
commit-message:
|
|
21
|
+
# Prefix all commit messages with "chore: "
|
|
22
|
+
prefix: "chore"
|
|
11
23
|
schedule:
|
|
12
|
-
interval: "
|
|
24
|
+
interval: "weekly"
|
|
25
|
+
cooldown:
|
|
26
|
+
default-days: 7
|
|
27
|
+
versioning-strategy: "increase-if-necessary"
|
|
28
|
+
allow:
|
|
29
|
+
- dependency-name: "*"
|
|
30
|
+
update-types:
|
|
31
|
+
- "version-update:semver-major"
|
|
32
|
+
ignore:
|
|
33
|
+
# TODO: remove ignore until neostandard support ESLint 10
|
|
34
|
+
- dependency-name: "eslint"
|
|
35
|
+
- dependency-name: "neostandard"
|
|
36
|
+
- dependency-name: "@stylistic/*"
|
|
13
37
|
open-pull-requests-limit: 10
|
|
38
|
+
groups:
|
|
39
|
+
# Production dependencies with breaking changes
|
|
40
|
+
dependencies:
|
|
41
|
+
dependency-type: "production"
|
|
42
|
+
# ESLint related dependencies
|
|
43
|
+
dev-dependencies-eslint:
|
|
44
|
+
patterns:
|
|
45
|
+
- "eslint"
|
|
46
|
+
- "neostandard"
|
|
47
|
+
- "@stylistic/*"
|
|
48
|
+
# TypeScript related dependencies
|
|
49
|
+
dev-dependencies-typescript:
|
|
50
|
+
patterns:
|
|
51
|
+
- "@types/*"
|
|
52
|
+
- "tstyche"
|
|
53
|
+
- "typescript"
|
package/.github/workflows/ci.yml
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: Lock Threads
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
schedule:
|
|
5
|
+
- cron: '0 0 1 * *'
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
group: lock
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
lock-threads:
|
|
16
|
+
permissions:
|
|
17
|
+
issues: write
|
|
18
|
+
pull-requests: write
|
|
19
|
+
uses: fastify/workflows/.github/workflows/lock-threads.yml@v6
|
package/LICENSE
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2017-present The Fastify team
|
|
4
|
-
|
|
5
|
-
The Fastify team members are listed at https://github.com/fastify/fastify#team.
|
|
3
|
+
Copyright (c) 2017-present The Fastify team <https://github.com/fastify/fastify#team>
|
|
6
4
|
|
|
7
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
6
|
of this software and associated documentation files (the "Software"), to deal
|
package/README.md
CHANGED
package/index.js
CHANGED
|
@@ -2,17 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
const zlib = require('node:zlib')
|
|
4
4
|
const { inherits, format } = require('node:util')
|
|
5
|
+
const { pipeline, compose } = require('node:stream')
|
|
5
6
|
|
|
6
7
|
const fp = require('fastify-plugin')
|
|
7
8
|
const encodingNegotiator = require('@fastify/accept-negotiator')
|
|
8
|
-
const pump = require('pump')
|
|
9
9
|
const mimedb = require('mime-db')
|
|
10
|
-
const peek = require('peek-stream')
|
|
11
10
|
const { Minipass } = require('minipass')
|
|
12
|
-
const pumpify = require('pumpify')
|
|
13
11
|
const { Readable } = require('readable-stream')
|
|
14
12
|
|
|
15
|
-
const {
|
|
13
|
+
const {
|
|
14
|
+
isStream,
|
|
15
|
+
isGzip,
|
|
16
|
+
isDeflate,
|
|
17
|
+
intoAsyncIterator,
|
|
18
|
+
isWebReadableStream,
|
|
19
|
+
isFetchResponse,
|
|
20
|
+
webStreamToNodeReadable,
|
|
21
|
+
createPeekTransform
|
|
22
|
+
} = require('./lib/utils')
|
|
16
23
|
|
|
17
24
|
const InvalidRequestEncodingError = createError('FST_CP_ERR_INVALID_CONTENT_ENCODING', 'Unsupported Content-Encoding: %s', 415)
|
|
18
25
|
const InvalidRequestCompressedPayloadError = createError('FST_CP_ERR_INVALID_CONTENT', 'Could not decompress the request payload using the provided encoding', 400)
|
|
@@ -50,11 +57,6 @@ function fastifyCompress (fastify, opts, next) {
|
|
|
50
57
|
|
|
51
58
|
// add onSend hook onto each route as needed
|
|
52
59
|
fastify.addHook('onRoute', (routeOptions) => {
|
|
53
|
-
// If route config.compress has been set it takes precedence over compress
|
|
54
|
-
if (routeOptions.config?.compress !== undefined) {
|
|
55
|
-
routeOptions.compress = routeOptions.config.compress
|
|
56
|
-
}
|
|
57
|
-
|
|
58
60
|
// Manage compression options
|
|
59
61
|
if (routeOptions.compress !== undefined) {
|
|
60
62
|
if (typeof routeOptions.compress === 'object') {
|
|
@@ -79,11 +81,6 @@ function fastifyCompress (fastify, opts, next) {
|
|
|
79
81
|
buildRouteCompress(fastify, globalCompressParams, routeOptions, true)
|
|
80
82
|
}
|
|
81
83
|
|
|
82
|
-
// If route config.decompress has been set it takes precedence over compress
|
|
83
|
-
if (routeOptions.config?.decompress !== undefined) {
|
|
84
|
-
routeOptions.decompress = routeOptions.config.decompress
|
|
85
|
-
}
|
|
86
|
-
|
|
87
84
|
// Manage decompression options
|
|
88
85
|
if (routeOptions.decompress !== undefined) {
|
|
89
86
|
if (typeof routeOptions.decompress === 'object') {
|
|
@@ -269,6 +266,9 @@ function buildRouteCompress (_fastify, params, routeOptions, decorateOnly) {
|
|
|
269
266
|
const noCompress =
|
|
270
267
|
// don't compress on x-no-compression header
|
|
271
268
|
(req.headers['x-no-compression'] !== undefined) ||
|
|
269
|
+
// don't compress partial content: Content-Range describes unencoded byte offsets
|
|
270
|
+
(reply.statusCode === 206) ||
|
|
271
|
+
(reply.getHeader('Content-Range') !== undefined) ||
|
|
272
272
|
// don't compress if not one of the indicated compressible types
|
|
273
273
|
(shouldCompress(reply.getHeader('Content-Type') || 'application/json', params.compressibleTypes) === false) ||
|
|
274
274
|
// don't compress on missing or identity `accept-encoding` header
|
|
@@ -289,7 +289,7 @@ function buildRouteCompress (_fastify, params, routeOptions, decorateOnly) {
|
|
|
289
289
|
encoding === undefined
|
|
290
290
|
? reply.removeHeader('Content-Encoding')
|
|
291
291
|
: reply.header('Content-Encoding', 'identity')
|
|
292
|
-
|
|
292
|
+
pipeline(stream, payload = unzipStream(params.uncompressStream), onEnd.bind(reply))
|
|
293
293
|
}
|
|
294
294
|
return next(null, payload)
|
|
295
295
|
}
|
|
@@ -316,7 +316,7 @@ function buildRouteCompress (_fastify, params, routeOptions, decorateOnly) {
|
|
|
316
316
|
}
|
|
317
317
|
|
|
318
318
|
stream = zipStream(params.compressStream, encoding)
|
|
319
|
-
|
|
319
|
+
pipeline(payload, stream, onEnd.bind(reply))
|
|
320
320
|
next(null, stream)
|
|
321
321
|
}
|
|
322
322
|
}
|
|
@@ -378,7 +378,10 @@ function buildRouteDecompress (_fastify, params, routeOptions) {
|
|
|
378
378
|
raw.on('data', trackEncodedLength.bind(decompresser))
|
|
379
379
|
raw.on('end', removeEncodedLengthTracking)
|
|
380
380
|
|
|
381
|
-
|
|
381
|
+
pipeline(raw, decompresser, () => {
|
|
382
|
+
// Cleanup callback - decompression errors are handled by decompresser's error handler
|
|
383
|
+
})
|
|
384
|
+
next(null, decompresser)
|
|
382
385
|
}
|
|
383
386
|
}
|
|
384
387
|
|
|
@@ -415,7 +418,7 @@ function compress (params) {
|
|
|
415
418
|
encoding === undefined
|
|
416
419
|
? this.removeHeader('Content-Encoding')
|
|
417
420
|
: this.header('Content-Encoding', 'identity')
|
|
418
|
-
|
|
421
|
+
pipeline(stream, payload = unzipStream(params.uncompressStream), onEnd.bind(this))
|
|
419
422
|
}
|
|
420
423
|
return this.send(payload)
|
|
421
424
|
}
|
|
@@ -446,7 +449,7 @@ function compress (params) {
|
|
|
446
449
|
}
|
|
447
450
|
|
|
448
451
|
stream = zipStream(params.compressStream, encoding)
|
|
449
|
-
|
|
452
|
+
pipeline(payload, stream, onEnd.bind(this))
|
|
450
453
|
return this.send(stream)
|
|
451
454
|
}
|
|
452
455
|
}
|
|
@@ -464,7 +467,17 @@ function setVaryHeader (reply) {
|
|
|
464
467
|
}
|
|
465
468
|
|
|
466
469
|
function onEnd (err) {
|
|
467
|
-
|
|
470
|
+
// Client disconnection during streaming is expected and handled by Fastify.
|
|
471
|
+
// Do not log "premature close" errors at error level since they are not
|
|
472
|
+
// actual errors - they occur when clients disconnect mid-response.
|
|
473
|
+
// Node's native stream.pipeline emits ERR_STREAM_PREMATURE_CLOSE with the
|
|
474
|
+
// message 'Premature close', while the legacy pump/end-of-stream packages
|
|
475
|
+
// emit 'premature close' without a code, so both variants are checked.
|
|
476
|
+
// See: https://github.com/fastify/fastify-compress/issues/382
|
|
477
|
+
// See: https://github.com/fastify/fastify-compress/issues/410
|
|
478
|
+
if (err && err.code !== 'ERR_STREAM_PREMATURE_CLOSE' && err.message !== 'premature close') {
|
|
479
|
+
this.log.error(err)
|
|
480
|
+
}
|
|
468
481
|
}
|
|
469
482
|
|
|
470
483
|
function trackEncodedLength (chunk) {
|
|
@@ -549,26 +562,26 @@ function maybeUnzip (payload, serialize) {
|
|
|
549
562
|
}
|
|
550
563
|
|
|
551
564
|
function zipStream (deflate, encoding) {
|
|
552
|
-
return
|
|
565
|
+
return createPeekTransform(function (data) {
|
|
553
566
|
switch (isCompressed(data)) {
|
|
554
|
-
case 1: return
|
|
555
|
-
case 2: return
|
|
567
|
+
case 1: return new Minipass()
|
|
568
|
+
case 2: return new Minipass()
|
|
556
569
|
}
|
|
557
|
-
return
|
|
570
|
+
return deflate[encoding]()
|
|
558
571
|
})
|
|
559
572
|
}
|
|
560
573
|
|
|
561
574
|
function unzipStream (inflate, maxRecursion) {
|
|
562
575
|
if (!(maxRecursion >= 0)) maxRecursion = 3
|
|
563
|
-
return
|
|
576
|
+
return createPeekTransform(function (data) {
|
|
564
577
|
// This path is never taken, when `maxRecursion` < 0 it is automatically set back to 3
|
|
565
578
|
/* c8 ignore next */
|
|
566
|
-
if (maxRecursion < 0)
|
|
579
|
+
if (maxRecursion < 0) throw new Error('Maximum recursion reached')
|
|
567
580
|
switch (isCompressed(data)) {
|
|
568
|
-
case 1: return
|
|
569
|
-
case 2: return
|
|
581
|
+
case 1: return compose(inflate.gzip(), unzipStream(inflate, maxRecursion - 1))
|
|
582
|
+
case 2: return compose(inflate.deflate(), unzipStream(inflate, maxRecursion - 1))
|
|
570
583
|
}
|
|
571
|
-
return
|
|
584
|
+
return new Minipass()
|
|
572
585
|
})
|
|
573
586
|
}
|
|
574
587
|
|
package/lib/utils.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { Readable: NodeReadable } = require('node:stream')
|
|
4
|
+
const { Transform } = require('readable-stream')
|
|
4
5
|
|
|
5
6
|
// https://datatracker.ietf.org/doc/html/rfc8878#section-3.1.1
|
|
6
7
|
function isZstd (buffer) {
|
|
@@ -104,4 +105,92 @@ async function * intoAsyncIterator (payload) {
|
|
|
104
105
|
yield payload
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
|
|
108
|
+
function createPeekTransform (swapStream, peekSize = 10) {
|
|
109
|
+
const buffers = []
|
|
110
|
+
let bufferSize = 0
|
|
111
|
+
let downstream = null
|
|
112
|
+
let flushCallback = null
|
|
113
|
+
|
|
114
|
+
const _transform = new Transform({
|
|
115
|
+
transform (chunk, encoding, callback) {
|
|
116
|
+
// when downstream already selected, directly passthrough
|
|
117
|
+
if (downstream !== null) return downstream.write(chunk, encoding, callback)
|
|
118
|
+
|
|
119
|
+
bufferSize += chunk.length
|
|
120
|
+
buffers.push(chunk)
|
|
121
|
+
|
|
122
|
+
// read more data when buffer size smaller than peeksize
|
|
123
|
+
if (bufferSize < peekSize) return callback()
|
|
124
|
+
|
|
125
|
+
wireDownStream(callback)
|
|
126
|
+
},
|
|
127
|
+
flush (callback) {
|
|
128
|
+
// used in "end" event to notify the transform finished flush
|
|
129
|
+
flushCallback = callback
|
|
130
|
+
if (downstream !== null) {
|
|
131
|
+
// all data should be flushed to downstream already
|
|
132
|
+
// we end the stream
|
|
133
|
+
downstream.end()
|
|
134
|
+
} else {
|
|
135
|
+
// flush internal buffers and end the downstream
|
|
136
|
+
wireDownStream((error) => {
|
|
137
|
+
if (error) {
|
|
138
|
+
flushCallback = null
|
|
139
|
+
callback(error)
|
|
140
|
+
} else {
|
|
141
|
+
downstream.end()
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
function onData (chunk) {
|
|
149
|
+
_transform.push(chunk)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function onError (error) {
|
|
153
|
+
_transform.destroy(error)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function onEnd () {
|
|
157
|
+
flushCallback && flushCallback()
|
|
158
|
+
// cleanup
|
|
159
|
+
downstream.off('data', onData)
|
|
160
|
+
downstream.off('error', onError)
|
|
161
|
+
downstream.off('end', onEnd)
|
|
162
|
+
downstream = null
|
|
163
|
+
flushCallback = null
|
|
164
|
+
|
|
165
|
+
_transform.push(null)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function wireDownStream (callback) {
|
|
169
|
+
const data = Buffer.concat(buffers)
|
|
170
|
+
try {
|
|
171
|
+
downstream = swapStream(data.subarray(0, peekSize))
|
|
172
|
+
downstream.on('data', onData)
|
|
173
|
+
downstream.on('error', onError)
|
|
174
|
+
downstream.on('end', onEnd)
|
|
175
|
+
downstream.write(data, callback)
|
|
176
|
+
// cleanup
|
|
177
|
+
buffers.length = 0
|
|
178
|
+
} catch (error) {
|
|
179
|
+
callback(error)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return _transform
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = {
|
|
187
|
+
isZstd,
|
|
188
|
+
isGzip,
|
|
189
|
+
isDeflate,
|
|
190
|
+
isStream,
|
|
191
|
+
intoAsyncIterator,
|
|
192
|
+
isWebReadableStream,
|
|
193
|
+
isFetchResponse,
|
|
194
|
+
webStreamToNodeReadable,
|
|
195
|
+
createPeekTransform
|
|
196
|
+
}
|
package/package.json
CHANGED
|
@@ -1,36 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fastify/compress",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.1.0",
|
|
4
4
|
"description": "Fastify compression utils",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "commonjs",
|
|
7
7
|
"types": "types/index.d.ts",
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@fastify/accept-negotiator": "^2.0.0",
|
|
10
|
-
"fastify-plugin": "^
|
|
10
|
+
"fastify-plugin": "^6.0.0",
|
|
11
11
|
"mime-db": "^1.52.0",
|
|
12
12
|
"minipass": "^7.0.4",
|
|
13
|
-
"peek-stream": "^1.1.3",
|
|
14
|
-
"pump": "^3.0.0",
|
|
15
|
-
"pumpify": "^2.0.1",
|
|
16
13
|
"readable-stream": "^4.5.2"
|
|
17
14
|
},
|
|
18
15
|
"devDependencies": {
|
|
19
|
-
"@types/node": "^
|
|
16
|
+
"@types/node": "^26.0.1",
|
|
20
17
|
"adm-zip": "^0.5.12",
|
|
21
|
-
"c8": "^
|
|
18
|
+
"c8": "^11.0.0",
|
|
22
19
|
"eslint": "^9.17.0",
|
|
23
20
|
"fastify": "^5.0.0",
|
|
24
21
|
"jsonstream": "^1.0.3",
|
|
25
|
-
"neostandard": "^0.
|
|
26
|
-
"
|
|
27
|
-
"typescript": "~
|
|
22
|
+
"neostandard": "^0.13.0",
|
|
23
|
+
"tstyche": "^7.1.0",
|
|
24
|
+
"typescript": "~6.0.2"
|
|
28
25
|
},
|
|
29
26
|
"scripts": {
|
|
30
27
|
"lint": "eslint",
|
|
31
28
|
"lint:fix": "eslint --fix",
|
|
32
29
|
"test": "npm run test:unit && npm run test:typescript",
|
|
33
|
-
"test:typescript": "
|
|
30
|
+
"test:typescript": "tstyche",
|
|
34
31
|
"test:unit": "node --test",
|
|
35
32
|
"test:coverage": "c8 node --test && c8 report --reporter=html",
|
|
36
33
|
"test:unit:verbose": "npm run test:unit -- -Rspec"
|
|
@@ -43,7 +40,7 @@
|
|
|
43
40
|
"brotli",
|
|
44
41
|
"zstd"
|
|
45
42
|
],
|
|
46
|
-
"author": "Tomas Della Vedova - @delvedor (
|
|
43
|
+
"author": "Tomas Della Vedova - @delvedor (https://delvedor.dev)",
|
|
47
44
|
"contributors": [
|
|
48
45
|
{
|
|
49
46
|
"name": "Matteo Collina",
|
|
@@ -82,9 +79,6 @@
|
|
|
82
79
|
"type": "git",
|
|
83
80
|
"url": "git+https://github.com/fastify/fastify-compress.git"
|
|
84
81
|
},
|
|
85
|
-
"tsd": {
|
|
86
|
-
"directory": "types"
|
|
87
|
-
},
|
|
88
82
|
"publishConfig": {
|
|
89
83
|
"access": "public"
|
|
90
84
|
}
|
|
@@ -682,6 +682,41 @@ describe('When a custom `zlib` option is provided, it should compress data :`',
|
|
|
682
682
|
t.assert.equal(response.headers['content-encoding'], 'gzip')
|
|
683
683
|
t.assert.equal(payload.toString('utf-8'), file)
|
|
684
684
|
})
|
|
685
|
+
|
|
686
|
+
test('using the custom `createZstdCompress()` method', async (t) => {
|
|
687
|
+
if (typeof zlib.createZstdCompress !== 'function') {
|
|
688
|
+
t.skip('zstd not supported in this Node.js version')
|
|
689
|
+
return
|
|
690
|
+
}
|
|
691
|
+
t.plan(4)
|
|
692
|
+
|
|
693
|
+
let usedCustom = false
|
|
694
|
+
const customZlib = { createZstdCompress: () => (usedCustom = true) && zlib.createZstdCompress() }
|
|
695
|
+
|
|
696
|
+
const fastify = Fastify()
|
|
697
|
+
await fastify.register(compressPlugin, { global: true, zlib: customZlib })
|
|
698
|
+
|
|
699
|
+
fastify.get('/', (_request, reply) => {
|
|
700
|
+
reply
|
|
701
|
+
.type('text/plain')
|
|
702
|
+
.compress(createReadStream('./package.json'))
|
|
703
|
+
})
|
|
704
|
+
|
|
705
|
+
const response = await fastify.inject({
|
|
706
|
+
url: '/',
|
|
707
|
+
method: 'GET',
|
|
708
|
+
headers: {
|
|
709
|
+
'accept-encoding': 'zstd'
|
|
710
|
+
}
|
|
711
|
+
})
|
|
712
|
+
t.assert.equal(usedCustom, true)
|
|
713
|
+
|
|
714
|
+
const file = readFileSync('./package.json', 'utf8')
|
|
715
|
+
const payload = zlib.zstdDecompressSync(response.rawPayload)
|
|
716
|
+
t.assert.equal(response.headers.vary, 'accept-encoding')
|
|
717
|
+
t.assert.equal(response.headers['content-encoding'], 'zstd')
|
|
718
|
+
t.assert.equal(payload.toString('utf-8'), file)
|
|
719
|
+
})
|
|
685
720
|
})
|
|
686
721
|
|
|
687
722
|
describe('When a malformed custom `zlib` option is provided, it should compress data :', async () => {
|
|
@@ -765,6 +800,37 @@ describe('When a malformed custom `zlib` option is provided, it should compress
|
|
|
765
800
|
const payload = zlib.gunzipSync(response.rawPayload)
|
|
766
801
|
t.assert.equal(payload.toString('utf-8'), 'hello')
|
|
767
802
|
})
|
|
803
|
+
|
|
804
|
+
test('using the fallback default Node.js core `zlib.createZstdCompress()` method', async (t) => {
|
|
805
|
+
if (typeof zlib.createZstdCompress !== 'function') {
|
|
806
|
+
t.skip('zstd not supported in this Node.js version')
|
|
807
|
+
return
|
|
808
|
+
}
|
|
809
|
+
t.plan(1)
|
|
810
|
+
|
|
811
|
+
const fastify = Fastify()
|
|
812
|
+
await fastify.register(compressPlugin, {
|
|
813
|
+
global: true,
|
|
814
|
+
threshold: 0,
|
|
815
|
+
zlib: true // will trigger a fallback on the default zlib.createZstdCompress
|
|
816
|
+
})
|
|
817
|
+
|
|
818
|
+
fastify.get('/', (_request, reply) => {
|
|
819
|
+
reply
|
|
820
|
+
.type('text/plain')
|
|
821
|
+
.compress('hello')
|
|
822
|
+
})
|
|
823
|
+
|
|
824
|
+
const response = await fastify.inject({
|
|
825
|
+
url: '/',
|
|
826
|
+
method: 'GET',
|
|
827
|
+
headers: {
|
|
828
|
+
'accept-encoding': 'zstd'
|
|
829
|
+
}
|
|
830
|
+
})
|
|
831
|
+
const payload = zlib.zstdDecompressSync(response.rawPayload)
|
|
832
|
+
t.assert.equal(payload.toString('utf-8'), 'hello')
|
|
833
|
+
})
|
|
768
834
|
})
|
|
769
835
|
|
|
770
836
|
describe('When `inflateIfDeflated` is `true` and `X-No-Compression` request header is `true` :', async () => {
|
|
@@ -3548,3 +3614,56 @@ test('It should demonstrate globalDecompression controls decompression independe
|
|
|
3548
3614
|
t.assert.equal(response.statusCode, 400)
|
|
3549
3615
|
t.assert.ok(response.body.includes('Content-Length') || response.body.includes('Bad Request'))
|
|
3550
3616
|
})
|
|
3617
|
+
|
|
3618
|
+
describe('Range Responses', () => {
|
|
3619
|
+
test('it should not compress 206 Partial Content responses', async (t) => {
|
|
3620
|
+
t.plan(4)
|
|
3621
|
+
const fastify = Fastify()
|
|
3622
|
+
await fastify.register(compressPlugin, { threshold: 0 })
|
|
3623
|
+
|
|
3624
|
+
fastify.get('/partial', (request, reply) => {
|
|
3625
|
+
reply
|
|
3626
|
+
.status(206)
|
|
3627
|
+
.header('Content-Type', 'text/plain')
|
|
3628
|
+
.header('Content-Range', 'bytes 0-4/12')
|
|
3629
|
+
.header('Content-Length', '5')
|
|
3630
|
+
.send('hello')
|
|
3631
|
+
})
|
|
3632
|
+
|
|
3633
|
+
const response = await fastify.inject({
|
|
3634
|
+
url: '/partial',
|
|
3635
|
+
method: 'GET',
|
|
3636
|
+
headers: { 'accept-encoding': 'gzip' }
|
|
3637
|
+
})
|
|
3638
|
+
|
|
3639
|
+
t.assert.equal(response.statusCode, 206)
|
|
3640
|
+
t.assert.equal(response.headers['content-encoding'], undefined)
|
|
3641
|
+
t.assert.equal(response.headers['content-range'], 'bytes 0-4/12')
|
|
3642
|
+
t.assert.equal(response.body, 'hello')
|
|
3643
|
+
})
|
|
3644
|
+
|
|
3645
|
+
test('it should not compress responses carrying a Content-Range header', async (t) => {
|
|
3646
|
+
t.plan(4)
|
|
3647
|
+
const fastify = Fastify()
|
|
3648
|
+
await fastify.register(compressPlugin, { threshold: 0 })
|
|
3649
|
+
|
|
3650
|
+
fastify.get('/content-range', (request, reply) => {
|
|
3651
|
+
reply
|
|
3652
|
+
.header('Content-Type', 'text/plain')
|
|
3653
|
+
.header('Content-Range', 'bytes 0-4/12')
|
|
3654
|
+
.header('Content-Length', '5')
|
|
3655
|
+
.send('hello')
|
|
3656
|
+
})
|
|
3657
|
+
|
|
3658
|
+
const response = await fastify.inject({
|
|
3659
|
+
url: '/content-range',
|
|
3660
|
+
method: 'GET',
|
|
3661
|
+
headers: { 'accept-encoding': 'gzip' }
|
|
3662
|
+
})
|
|
3663
|
+
|
|
3664
|
+
t.assert.equal(response.statusCode, 200)
|
|
3665
|
+
t.assert.equal(response.headers['content-encoding'], undefined)
|
|
3666
|
+
t.assert.equal(response.headers['content-range'], 'bytes 0-4/12')
|
|
3667
|
+
t.assert.equal(response.body, 'hello')
|
|
3668
|
+
})
|
|
3669
|
+
})
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
const { test, describe } = require('node:test')
|
|
4
4
|
const { createReadStream } = require('node:fs')
|
|
5
|
+
const { pipeline } = require('node:stream')
|
|
5
6
|
const path = require('node:path')
|
|
6
7
|
const zlib = require('node:zlib')
|
|
7
|
-
const pump = require('pump')
|
|
8
8
|
const Fastify = require('fastify')
|
|
9
9
|
const compressPlugin = require('../index')
|
|
10
10
|
|
|
@@ -12,7 +12,9 @@ function createPayload (compressor) {
|
|
|
12
12
|
let payload = createReadStream(path.resolve(__dirname, '../package.json'))
|
|
13
13
|
|
|
14
14
|
if (compressor) {
|
|
15
|
-
|
|
15
|
+
const compressed = compressor()
|
|
16
|
+
pipeline(payload, compressed, () => {})
|
|
17
|
+
payload = compressed
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
return payload
|