@fastify/compress 5.0.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/dependabot.yml +13 -0
- package/.github/stale.yml +21 -0
- package/.github/workflows/ci.yml +47 -0
- package/.taprc +6 -0
- package/LICENSE +21 -0
- package/README.md +310 -0
- package/index.d.ts +81 -0
- package/index.js +556 -0
- package/lib/utils.js +7 -0
- package/package.json +73 -0
- package/test/global-compress.test.js +3048 -0
- package/test/global-decompress.test.js +423 -0
- package/test/routes-compress.test.js +437 -0
- package/test/routes-decompress.test.js +305 -0
- package/test/types/index.test-d.ts +115 -0
- package/test/utils.test.js +27 -0
package/index.js
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fp = require('fastify-plugin')
|
|
4
|
+
const zlib = require('zlib')
|
|
5
|
+
const pump = require('pump')
|
|
6
|
+
const mimedb = require('mime-db')
|
|
7
|
+
const intoStream = require('into-stream')
|
|
8
|
+
const peek = require('peek-stream')
|
|
9
|
+
const Minipass = require('minipass')
|
|
10
|
+
const pumpify = require('pumpify')
|
|
11
|
+
const isGzip = require('is-gzip')
|
|
12
|
+
const isDeflate = require('is-deflate')
|
|
13
|
+
const encodingNegotiator = require('encoding-negotiator')
|
|
14
|
+
const { inherits, format } = require('util')
|
|
15
|
+
|
|
16
|
+
const { isStream } = require('./lib/utils')
|
|
17
|
+
|
|
18
|
+
const InvalidRequestEncodingError = createError('FST_CP_ERR_INVALID_CONTENT_ENCODING', 'Unsupported Content-Encoding: %s', 415)
|
|
19
|
+
const InvalidRequestCompressedPayloadError = createError('FST_CP_ERR_INVALID_CONTENT', 'Could not decompress the request payload using the provided encoding', 400)
|
|
20
|
+
|
|
21
|
+
function compressPlugin (fastify, opts, next) {
|
|
22
|
+
const globalCompressParams = processCompressParams(opts)
|
|
23
|
+
const globalDecompressParams = processDecompressParams(opts)
|
|
24
|
+
|
|
25
|
+
if (opts.encodings && opts.encodings.length < 1) {
|
|
26
|
+
next(new Error('The `encodings` option array must have at least 1 item.'))
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (opts.requestEncodings && opts.requestEncodings.length < 1) {
|
|
31
|
+
next(new Error('The `requestEncodings` option array must have at least 1 item.'))
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (globalCompressParams.encodings.length < 1) {
|
|
36
|
+
next(new Error('None of the passed `encodings` were supported — compression not possible.'))
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (globalDecompressParams.encodings.length < 1) {
|
|
41
|
+
next(new Error('None of the passed `requestEncodings` were supported — request decompression not possible.'))
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (globalDecompressParams.forceEncoding && !globalDecompressParams.encodings.includes(globalDecompressParams.forceEncoding)) {
|
|
46
|
+
next(new Error(`Unsupported decompression encoding ${opts.forceRequestEncoding}.`))
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fastify.decorateReply('compress', null)
|
|
51
|
+
|
|
52
|
+
// add onSend hook onto each route as needed
|
|
53
|
+
fastify.addHook('onRoute', (routeOptions) => {
|
|
54
|
+
// If route config.compress has been set it takes precedence over compress
|
|
55
|
+
if (routeOptions.config && typeof routeOptions.config.compress !== 'undefined') {
|
|
56
|
+
routeOptions.compress = routeOptions.config.compress
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Manage compression options
|
|
60
|
+
if (typeof routeOptions.compress !== 'undefined') {
|
|
61
|
+
if (typeof routeOptions.compress === 'object') {
|
|
62
|
+
const mergedCompressParams = Object.assign(
|
|
63
|
+
{}, globalCompressParams, processCompressParams(routeOptions.compress)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
// if the current endpoint has a custom compress configuration ...
|
|
67
|
+
buildRouteCompress(fastify, mergedCompressParams, routeOptions)
|
|
68
|
+
} else if (routeOptions.compress === false) {
|
|
69
|
+
// don't apply any compress settings
|
|
70
|
+
} else {
|
|
71
|
+
throw new Error('Unknown value for route compress configuration')
|
|
72
|
+
}
|
|
73
|
+
} else if (globalCompressParams.global) {
|
|
74
|
+
// if the plugin is set globally (meaning that all the routes will be compressed)
|
|
75
|
+
// As the endpoint, does not have a custom rateLimit configuration, use the global one.
|
|
76
|
+
buildRouteCompress(fastify, globalCompressParams, routeOptions)
|
|
77
|
+
} else {
|
|
78
|
+
// if no options are specified and the plugin is not global, then we still want to decorate
|
|
79
|
+
// the reply in this case
|
|
80
|
+
buildRouteCompress(fastify, globalCompressParams, routeOptions, true)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// If route config.decompress has been set it takes precedence over compress
|
|
84
|
+
if (routeOptions.config && typeof routeOptions.config.decompress !== 'undefined') {
|
|
85
|
+
routeOptions.decompress = routeOptions.config.decompress
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Manage decompression options
|
|
89
|
+
if (typeof routeOptions.decompress !== 'undefined') {
|
|
90
|
+
if (typeof routeOptions.decompress === 'object') {
|
|
91
|
+
// if the current endpoint has a custom compress configuration ...
|
|
92
|
+
const mergedDecompressParams = Object.assign(
|
|
93
|
+
{}, globalDecompressParams, processDecompressParams(routeOptions.decompress)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
buildRouteDecompress(fastify, mergedDecompressParams, routeOptions)
|
|
97
|
+
} else if (routeOptions.decompress === false) {
|
|
98
|
+
// don't apply any decompress settings
|
|
99
|
+
} else {
|
|
100
|
+
throw new Error('Unknown value for route decompress configuration')
|
|
101
|
+
}
|
|
102
|
+
} else if (globalDecompressParams.global) {
|
|
103
|
+
// if the plugin is set globally (meaning that all the routes will be decompressed)
|
|
104
|
+
// As the endpoint, does not have a custom rateLimit configuration, use the global one.
|
|
105
|
+
buildRouteDecompress(fastify, globalDecompressParams, routeOptions)
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
next()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function processCompressParams (opts) {
|
|
113
|
+
/* istanbul ignore next */
|
|
114
|
+
if (!opts) {
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const params = {
|
|
119
|
+
global: (typeof opts.global === 'boolean') ? opts.global : true
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
params.removeContentLengthHeader = typeof opts.removeContentLengthHeader === 'boolean' ? opts.removeContentLengthHeader : true
|
|
123
|
+
params.brotliOptions = opts.brotliOptions
|
|
124
|
+
params.zlibOptions = opts.zlibOptions
|
|
125
|
+
params.onUnsupportedEncoding = opts.onUnsupportedEncoding
|
|
126
|
+
params.inflateIfDeflated = opts.inflateIfDeflated === true
|
|
127
|
+
params.threshold = typeof opts.threshold === 'number' ? opts.threshold : 1024
|
|
128
|
+
params.compressibleTypes = opts.customTypes instanceof RegExp ? opts.customTypes : /^text\/(?!event-stream)|\+json$|\+text$|\+xml$|octet-stream$/
|
|
129
|
+
params.compressStream = {
|
|
130
|
+
br: () => ((opts.zlib || zlib).createBrotliCompress || zlib.createBrotliCompress)(params.brotliOptions),
|
|
131
|
+
gzip: () => ((opts.zlib || zlib).createGzip || zlib.createGzip)(params.zlibOptions),
|
|
132
|
+
deflate: () => ((opts.zlib || zlib).createDeflate || zlib.createDeflate)(params.zlibOptions)
|
|
133
|
+
}
|
|
134
|
+
params.uncompressStream = {
|
|
135
|
+
// Currently params.uncompressStream.br() is never called as we do not have any way to autodetect brotli compression in `fastify-compress`
|
|
136
|
+
// Brotli documentation reference: [RFC 7932](https://www.rfc-editor.org/rfc/rfc7932)
|
|
137
|
+
br: /* istanbul ignore next */ () => ((opts.zlib || zlib).createBrotliDecompress || zlib.createBrotliDecompress)(params.brotliOptions),
|
|
138
|
+
gzip: () => ((opts.zlib || zlib).createGunzip || zlib.createGunzip)(params.zlibOptions),
|
|
139
|
+
deflate: () => ((opts.zlib || zlib).createInflate || zlib.createInflate)(params.zlibOptions)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const supportedEncodings = ['br', 'gzip', 'deflate', 'identity']
|
|
143
|
+
|
|
144
|
+
params.encodings = Array.isArray(opts.encodings)
|
|
145
|
+
? supportedEncodings
|
|
146
|
+
.filter(encoding => opts.encodings.includes(encoding))
|
|
147
|
+
.sort((a, b) => opts.encodings.indexOf(a) - supportedEncodings.indexOf(b))
|
|
148
|
+
: supportedEncodings
|
|
149
|
+
|
|
150
|
+
return params
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function processDecompressParams (opts) {
|
|
154
|
+
/* istanbul ignore next */
|
|
155
|
+
if (!opts) {
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const customZlib = opts.zlib || zlib
|
|
160
|
+
|
|
161
|
+
const params = {
|
|
162
|
+
global: (typeof opts.global === 'boolean') ? opts.global : true,
|
|
163
|
+
onUnsupportedRequestEncoding: opts.onUnsupportedRequestEncoding,
|
|
164
|
+
onInvalidRequestPayload: opts.onInvalidRequestPayload,
|
|
165
|
+
decompressStream: {
|
|
166
|
+
br: customZlib.createBrotliDecompress || zlib.createBrotliDecompress,
|
|
167
|
+
gzip: customZlib.createGunzip || zlib.createGunzip,
|
|
168
|
+
deflate: customZlib.createInflate || zlib.createInflate
|
|
169
|
+
},
|
|
170
|
+
encodings: [],
|
|
171
|
+
forceEncoding: null
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const supportedEncodings = ['br', 'gzip', 'deflate', 'identity']
|
|
175
|
+
|
|
176
|
+
params.encodings = Array.isArray(opts.requestEncodings)
|
|
177
|
+
? supportedEncodings
|
|
178
|
+
.filter(encoding => opts.requestEncodings.includes(encoding))
|
|
179
|
+
.sort((a, b) => opts.requestEncodings.indexOf(a) - supportedEncodings.indexOf(b))
|
|
180
|
+
: supportedEncodings
|
|
181
|
+
|
|
182
|
+
if (opts.forceRequestEncoding) {
|
|
183
|
+
params.forceEncoding = opts.forceRequestEncoding
|
|
184
|
+
|
|
185
|
+
if (params.encodings.includes(opts.forceRequestEncoding)) {
|
|
186
|
+
params.encodings = [opts.forceRequestEncoding]
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return params
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function buildRouteCompress (fastify, params, routeOptions, decorateOnly) {
|
|
194
|
+
// In order to provide a compress method with the same parameter set as the route itself,
|
|
195
|
+
// we decorate the reply at the start of the request
|
|
196
|
+
if (Array.isArray(routeOptions.onRequest)) {
|
|
197
|
+
routeOptions.onRequest.push(onRequest)
|
|
198
|
+
} else if (typeof routeOptions.onRequest === 'function') {
|
|
199
|
+
routeOptions.onRequest = [routeOptions.onRequest, onRequest]
|
|
200
|
+
} else {
|
|
201
|
+
routeOptions.onRequest = [onRequest]
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const compressFn = compress(params)
|
|
205
|
+
function onRequest (req, reply, next) {
|
|
206
|
+
reply.compress = compressFn
|
|
207
|
+
next()
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (decorateOnly) {
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (Array.isArray(routeOptions.onSend)) {
|
|
215
|
+
routeOptions.onSend.push(onSend)
|
|
216
|
+
} else if (typeof routeOptions.onSend === 'function') {
|
|
217
|
+
routeOptions.onSend = [routeOptions.onSend, onSend]
|
|
218
|
+
} else {
|
|
219
|
+
routeOptions.onSend = [onSend]
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function onSend (req, reply, payload, next) {
|
|
223
|
+
if (payload == null) {
|
|
224
|
+
return next()
|
|
225
|
+
}
|
|
226
|
+
const responseEncoding = reply.getHeader('Content-Encoding')
|
|
227
|
+
if (responseEncoding && responseEncoding !== 'identity') {
|
|
228
|
+
// response is already compressed
|
|
229
|
+
return next()
|
|
230
|
+
}
|
|
231
|
+
setVaryHeader(reply)
|
|
232
|
+
|
|
233
|
+
let stream, encoding
|
|
234
|
+
const noCompress =
|
|
235
|
+
// don't compress on x-no-compression header
|
|
236
|
+
(req.headers['x-no-compression'] !== undefined) ||
|
|
237
|
+
// don't compress if not one of the indicated compressible types
|
|
238
|
+
(shouldCompress(reply.getHeader('Content-Type') || 'application/json', params.compressibleTypes) === false) ||
|
|
239
|
+
// don't compress on missing or identity `accept-encoding` header
|
|
240
|
+
((encoding = getEncodingHeader(params.encodings, req)) == null || encoding === 'identity')
|
|
241
|
+
|
|
242
|
+
if (encoding == null && params.onUnsupportedEncoding != null) {
|
|
243
|
+
const encodingHeader = req.headers['accept-encoding']
|
|
244
|
+
try {
|
|
245
|
+
const errorPayload = params.onUnsupportedEncoding(encodingHeader, reply.request, reply)
|
|
246
|
+
return next(null, errorPayload)
|
|
247
|
+
} catch (err) {
|
|
248
|
+
return next(err)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (noCompress) {
|
|
253
|
+
if (params.inflateIfDeflated && isStream(stream = maybeUnzip(payload))) {
|
|
254
|
+
encoding === undefined
|
|
255
|
+
? reply.removeHeader('Content-Encoding')
|
|
256
|
+
: reply.header('Content-Encoding', 'identity')
|
|
257
|
+
pump(stream, payload = unzipStream(params.uncompressStream), onEnd.bind(reply))
|
|
258
|
+
}
|
|
259
|
+
return next(null, payload)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (typeof payload.pipe !== 'function') {
|
|
263
|
+
if (Buffer.byteLength(payload) < params.threshold) {
|
|
264
|
+
return next()
|
|
265
|
+
}
|
|
266
|
+
payload = intoStream(payload)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
params.removeContentLengthHeader
|
|
270
|
+
? reply
|
|
271
|
+
.header('Content-Encoding', encoding)
|
|
272
|
+
.removeHeader('content-length')
|
|
273
|
+
: reply.header('Content-Encoding', encoding)
|
|
274
|
+
|
|
275
|
+
stream = zipStream(params.compressStream, encoding)
|
|
276
|
+
pump(payload, stream, onEnd.bind(reply))
|
|
277
|
+
next(null, stream)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function buildRouteDecompress (fastify, params, routeOptions) {
|
|
282
|
+
// Add our decompress handler in the preParsing hook
|
|
283
|
+
if (Array.isArray(routeOptions.preParsing)) {
|
|
284
|
+
routeOptions.preParsing.unshift(preParsing)
|
|
285
|
+
} else if (typeof routeOptions.preParsing === 'function') {
|
|
286
|
+
routeOptions.preParsing = [preParsing, routeOptions.preParsing]
|
|
287
|
+
} else {
|
|
288
|
+
routeOptions.preParsing = [preParsing]
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function preParsing (request, reply, raw, next) {
|
|
292
|
+
// Get the encoding from the options or from the headers
|
|
293
|
+
let encoding = params.forceEncoding
|
|
294
|
+
|
|
295
|
+
if (!encoding) {
|
|
296
|
+
encoding = request.headers['content-encoding']
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// The request is not compressed, nothing to do here
|
|
300
|
+
if (!encoding) {
|
|
301
|
+
return next(null, raw)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Check that encoding is supported
|
|
305
|
+
if (!params.encodings.includes(encoding)) {
|
|
306
|
+
let errorPayload
|
|
307
|
+
|
|
308
|
+
if (params.onUnsupportedRequestEncoding) {
|
|
309
|
+
try {
|
|
310
|
+
errorPayload = params.onUnsupportedRequestEncoding(encoding, request)
|
|
311
|
+
} catch (ex) {
|
|
312
|
+
errorPayload = undefined
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (!errorPayload) {
|
|
317
|
+
errorPayload = new InvalidRequestEncodingError(encoding)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return next(errorPayload)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// No action on identity
|
|
324
|
+
if (encoding === 'identity') {
|
|
325
|
+
return next(null, raw)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Prepare decompression - If there is a decompress error, prepare the error for fastify handing
|
|
329
|
+
const decompresser = params.decompressStream[encoding]()
|
|
330
|
+
decompresser.receivedEncodedLength = 0
|
|
331
|
+
decompresser.on('error', onDecompressError.bind(this, request, params, encoding))
|
|
332
|
+
decompresser.pause()
|
|
333
|
+
|
|
334
|
+
// Track length of encoded length to handle receivedEncodedLength
|
|
335
|
+
raw.on('data', trackEncodedLength.bind(decompresser))
|
|
336
|
+
raw.on('end', removeEncodedLengthTracking)
|
|
337
|
+
|
|
338
|
+
next(null, pump(raw, decompresser))
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function compress (params) {
|
|
343
|
+
return function (payload) {
|
|
344
|
+
if (payload == null) {
|
|
345
|
+
this.send(new Error('Internal server error'))
|
|
346
|
+
return
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
setVaryHeader(this)
|
|
350
|
+
let stream, encoding
|
|
351
|
+
const noCompress =
|
|
352
|
+
// don't compress on x-no-compression header
|
|
353
|
+
(this.request.headers['x-no-compression'] !== undefined) ||
|
|
354
|
+
// don't compress if not one of the indicated compressible types
|
|
355
|
+
(shouldCompress(this.getHeader('Content-Type') || 'application/json', params.compressibleTypes) === false) ||
|
|
356
|
+
// don't compress on missing or identity `accept-encoding` header
|
|
357
|
+
((encoding = getEncodingHeader(params.encodings, this.request)) == null || encoding === 'identity')
|
|
358
|
+
|
|
359
|
+
if (encoding == null && params.onUnsupportedEncoding != null) {
|
|
360
|
+
const encodingHeader = this.request.headers['accept-encoding']
|
|
361
|
+
|
|
362
|
+
let errorPayload
|
|
363
|
+
try {
|
|
364
|
+
errorPayload = params.onUnsupportedEncoding(encodingHeader, this.request, this)
|
|
365
|
+
} catch (ex) {
|
|
366
|
+
errorPayload = ex
|
|
367
|
+
}
|
|
368
|
+
return this.send(errorPayload)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (noCompress) {
|
|
372
|
+
if (params.inflateIfDeflated && isStream(stream = maybeUnzip(payload, this.serialize.bind(this)))) {
|
|
373
|
+
encoding === undefined
|
|
374
|
+
? this.removeHeader('Content-Encoding')
|
|
375
|
+
: this.header('Content-Encoding', 'identity')
|
|
376
|
+
pump(stream, payload = unzipStream(params.uncompressStream), onEnd.bind(this))
|
|
377
|
+
}
|
|
378
|
+
return this.send(payload)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (typeof payload.pipe !== 'function') {
|
|
382
|
+
if (!Buffer.isBuffer(payload) && typeof payload !== 'string') {
|
|
383
|
+
payload = this.serialize(payload)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (typeof payload.pipe !== 'function') {
|
|
388
|
+
if (Buffer.byteLength(payload) < params.threshold) {
|
|
389
|
+
return this.send(payload)
|
|
390
|
+
}
|
|
391
|
+
payload = intoStream(payload)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
params.removeContentLengthHeader
|
|
395
|
+
? this
|
|
396
|
+
.header('Content-Encoding', encoding)
|
|
397
|
+
.removeHeader('content-length')
|
|
398
|
+
: this.header('Content-Encoding', encoding)
|
|
399
|
+
|
|
400
|
+
stream = zipStream(params.compressStream, encoding)
|
|
401
|
+
pump(payload, stream, onEnd.bind(this))
|
|
402
|
+
this.send(stream)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function setVaryHeader (reply) {
|
|
407
|
+
if (reply.hasHeader('Vary')) {
|
|
408
|
+
const varyHeader = Array.isArray(reply.getHeader('Vary')) ? reply.getHeader('Vary') : [reply.getHeader('Vary')]
|
|
409
|
+
if (!varyHeader.some((h) => h.includes('accept-encoding'))) {
|
|
410
|
+
reply.header('Vary', `${varyHeader.join(', ')}, accept-encoding`)
|
|
411
|
+
}
|
|
412
|
+
} else {
|
|
413
|
+
reply.header('Vary', 'accept-encoding')
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function onEnd (err) {
|
|
418
|
+
if (err) this.log.error(err)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function trackEncodedLength (chunk) {
|
|
422
|
+
this.receivedEncodedLength += chunk.length
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function removeEncodedLengthTracking () {
|
|
426
|
+
this.removeListener('data', trackEncodedLength)
|
|
427
|
+
this.removeListener('end', removeEncodedLengthTracking)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function onDecompressError (request, params, encoding, error) {
|
|
431
|
+
this.log.debug(`compress: invalid request payload - ${error}`)
|
|
432
|
+
|
|
433
|
+
let errorPayload
|
|
434
|
+
|
|
435
|
+
if (params.onInvalidRequestPayload) {
|
|
436
|
+
try {
|
|
437
|
+
errorPayload = params.onInvalidRequestPayload(encoding, request, error)
|
|
438
|
+
} catch (ex) {
|
|
439
|
+
errorPayload = undefined
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (!errorPayload) {
|
|
444
|
+
errorPayload = new InvalidRequestCompressedPayloadError()
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
error.decompressError = error
|
|
448
|
+
Object.assign(error, errorPayload)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function getEncodingHeader (encodings, request) {
|
|
452
|
+
let header = request.headers['accept-encoding']
|
|
453
|
+
if (header != null) {
|
|
454
|
+
header = header.toLowerCase()
|
|
455
|
+
// consider the no-preference token as gzip for downstream compat
|
|
456
|
+
// and x-gzip as an alias of gzip
|
|
457
|
+
// ref.: [HTTP/1.1 RFC 7230 section 4.2.3](https://datatracker.ietf.org/doc/html/rfc7230#section-4.2.3)
|
|
458
|
+
.replace(/\*|x-gzip/g, 'gzip')
|
|
459
|
+
return encodingNegotiator.negotiate(header, encodings)
|
|
460
|
+
} else {
|
|
461
|
+
return undefined
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function shouldCompress (type, compressibleTypes) {
|
|
466
|
+
if (compressibleTypes.test(type)) return true
|
|
467
|
+
const data = mimedb[type.split(';', 1)[0].trim().toLowerCase()]
|
|
468
|
+
if (data === undefined) return false
|
|
469
|
+
return data.compressible === true
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function isCompressed (data) {
|
|
473
|
+
if (isGzip(data)) return 1
|
|
474
|
+
if (isDeflate(data)) return 2
|
|
475
|
+
return 0
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function maybeUnzip (payload, serialize) {
|
|
479
|
+
if (isStream(payload)) return payload
|
|
480
|
+
|
|
481
|
+
let buf = payload; let result = payload
|
|
482
|
+
|
|
483
|
+
if (ArrayBuffer.isView(payload)) {
|
|
484
|
+
// Cast non-Buffer DataViews into a Buffer
|
|
485
|
+
buf = result = Buffer.from(
|
|
486
|
+
payload.buffer,
|
|
487
|
+
payload.byteOffset,
|
|
488
|
+
payload.byteLength
|
|
489
|
+
)
|
|
490
|
+
} else if (serialize && typeof payload !== 'string') {
|
|
491
|
+
buf = result = serialize(payload)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// handle case where serialize doesn't return a string or Buffer
|
|
495
|
+
if (!Buffer.isBuffer(buf)) return result
|
|
496
|
+
if (isCompressed(buf) === 0) return result
|
|
497
|
+
return intoStream(result)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function zipStream (deflate, encoding) {
|
|
501
|
+
return peek({ newline: false, maxBuffer: 10 }, function (data, swap) {
|
|
502
|
+
switch (isCompressed(data)) {
|
|
503
|
+
case 1: return swap(null, new Minipass())
|
|
504
|
+
case 2: return swap(null, new Minipass())
|
|
505
|
+
}
|
|
506
|
+
return swap(null, deflate[encoding]())
|
|
507
|
+
})
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function unzipStream (inflate, maxRecursion) {
|
|
511
|
+
if (!(maxRecursion >= 0)) maxRecursion = 3
|
|
512
|
+
return peek({ newline: false, maxBuffer: 10 }, function (data, swap) {
|
|
513
|
+
/* istanbul ignore if */
|
|
514
|
+
// This path is never taken, when `maxRecursion` < 0 it is automatically set back to 3
|
|
515
|
+
if (maxRecursion < 0) return swap(new Error('Maximum recursion reached'))
|
|
516
|
+
switch (isCompressed(data)) {
|
|
517
|
+
case 1: return swap(null, pumpify(inflate.gzip(), unzipStream(inflate, maxRecursion - 1)))
|
|
518
|
+
case 2: return swap(null, pumpify(inflate.deflate(), unzipStream(inflate, maxRecursion - 1)))
|
|
519
|
+
}
|
|
520
|
+
return swap(null, new Minipass())
|
|
521
|
+
})
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function createError (code, message, statusCode) {
|
|
525
|
+
code = code.toUpperCase()
|
|
526
|
+
|
|
527
|
+
function FastifyCompressError (a) {
|
|
528
|
+
Error.captureStackTrace(this, FastifyCompressError)
|
|
529
|
+
this.name = 'FastifyCompressError'
|
|
530
|
+
this.code = code
|
|
531
|
+
|
|
532
|
+
if (a) {
|
|
533
|
+
this.message = format(message, a)
|
|
534
|
+
} else {
|
|
535
|
+
this.message = message
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
this.statusCode = statusCode
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
FastifyCompressError.prototype[Symbol.toStringTag] = 'Error'
|
|
542
|
+
|
|
543
|
+
/* istanbul ignore next */
|
|
544
|
+
FastifyCompressError.prototype.toString = function () {
|
|
545
|
+
return `${this.name} [${this.code}]: ${this.message}`
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
inherits(FastifyCompressError, Error)
|
|
549
|
+
|
|
550
|
+
return FastifyCompressError
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
module.exports = fp(compressPlugin, {
|
|
554
|
+
fastify: '3.x',
|
|
555
|
+
name: 'fastify-compress'
|
|
556
|
+
})
|
package/lib/utils.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fastify/compress",
|
|
3
|
+
"version": "5.0.0",
|
|
4
|
+
"description": "Fastify compression utils",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"encoding-negotiator": "^2.0.1",
|
|
9
|
+
"fastify-plugin": "^3.0.0",
|
|
10
|
+
"into-stream": "^6.0.0",
|
|
11
|
+
"is-deflate": "^1.0.0",
|
|
12
|
+
"is-gzip": "^2.0.0",
|
|
13
|
+
"is-zip": "^1.0.0",
|
|
14
|
+
"mime-db": "^1.51.0",
|
|
15
|
+
"minipass": "^3.1.6",
|
|
16
|
+
"peek-stream": "^1.1.3",
|
|
17
|
+
"pump": "^3.0.0",
|
|
18
|
+
"pumpify": "^2.0.1",
|
|
19
|
+
"string-to-stream": "^3.0.1"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^17.0.8",
|
|
23
|
+
"@typescript-eslint/eslint-plugin": "^5.9.0",
|
|
24
|
+
"@typescript-eslint/parser": "^5.9.0",
|
|
25
|
+
"adm-zip": "^0.5.9",
|
|
26
|
+
"fastify": "^3.25.3",
|
|
27
|
+
"jsonstream": "^1.0.3",
|
|
28
|
+
"pre-commit": "^1.2.2",
|
|
29
|
+
"standard": "^16.0.4",
|
|
30
|
+
"tap": "^16.0.0",
|
|
31
|
+
"tsd": "^0.20.0",
|
|
32
|
+
"typescript": "^4.5.4"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"coverage": "npm run unit -- --cov",
|
|
36
|
+
"coverage-report": "npm run coverage -- --coverage-report=lcov",
|
|
37
|
+
"lint": "standard",
|
|
38
|
+
"lint:fix": "npm run lint -- --fix",
|
|
39
|
+
"lint:typescript": "npm run lint:fix -- --parser @typescript-eslint/parser --plugin typescript test/types/*.ts",
|
|
40
|
+
"lint:types": "npm run lint:fix -- --parser @typescript-eslint/parser --plugin typescript *.d.ts",
|
|
41
|
+
"test": "npm run lint && npm run unit && npm run test:typescript",
|
|
42
|
+
"test:typescript": "tsd",
|
|
43
|
+
"unit": "tap -J test/*.test.js",
|
|
44
|
+
"unit:report": "npm run unit -- --cov --coverage-report=html",
|
|
45
|
+
"unit:verbose": "npm run unit -- -Rspec"
|
|
46
|
+
},
|
|
47
|
+
"keywords": [
|
|
48
|
+
"fastify",
|
|
49
|
+
"compression",
|
|
50
|
+
"deflate",
|
|
51
|
+
"gzip",
|
|
52
|
+
"brotli"
|
|
53
|
+
],
|
|
54
|
+
"author": "Tomas Della Vedova - @delvedor (http://delved.org)",
|
|
55
|
+
"license": "MIT",
|
|
56
|
+
"bugs": {
|
|
57
|
+
"url": "https://github.com/fastify/fastify-compress/issues"
|
|
58
|
+
},
|
|
59
|
+
"homepage": "https://github.com/fastify/fastify-compress#readme",
|
|
60
|
+
"repository": {
|
|
61
|
+
"type": "git",
|
|
62
|
+
"url": "git+https://github.com/fastify/fastify-compress.git"
|
|
63
|
+
},
|
|
64
|
+
"engines": {
|
|
65
|
+
"node": ">=10.16"
|
|
66
|
+
},
|
|
67
|
+
"tsd": {
|
|
68
|
+
"directory": "test/types"
|
|
69
|
+
},
|
|
70
|
+
"publishConfig": {
|
|
71
|
+
"access": "public"
|
|
72
|
+
}
|
|
73
|
+
}
|