Skip to content

Commit 64e54e5

Browse files
committed
fix: merge partial route options with global options
1 parent 0aacfec commit 64e54e5

3 files changed

Lines changed: 214 additions & 86 deletions

File tree

index.js

Lines changed: 129 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,7 @@ function fastifyCompress (fastify, opts, next) {
5757
// Manage compression options
5858
if (routeOptions.compress !== undefined) {
5959
if (typeof routeOptions.compress === 'object') {
60-
const mergedCompressParams = Object.assign(
61-
{}, globalCompressParams, processCompressParams(routeOptions.compress)
62-
)
60+
const mergedCompressParams = processCompressParams(routeOptions.compress, globalCompressParams)
6361

6462
// if the current endpoint has a custom compress configuration ...
6563
buildRouteCompress(fastify, mergedCompressParams, routeOptions)
@@ -87,9 +85,7 @@ function fastifyCompress (fastify, opts, next) {
8785
if (routeOptions.decompress !== undefined) {
8886
if (typeof routeOptions.decompress === 'object') {
8987
// if the current endpoint has a custom compress configuration ...
90-
const mergedDecompressParams = Object.assign(
91-
{}, globalDecompressParams, processDecompressParams(routeOptions.decompress)
92-
)
88+
const mergedDecompressParams = processDecompressParams(routeOptions.decompress, globalDecompressParams)
9389

9490
buildRouteDecompress(fastify, mergedDecompressParams, routeOptions)
9591
} else if (routeOptions.decompress === false) {
@@ -116,109 +112,156 @@ const recommendedDefaultBrotliOptions = {
116112
}
117113
}
118114

119-
function processCompressParams (opts) {
120-
/* c8 ignore next 3 */
121-
if (!opts) {
122-
return
115+
const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key)
116+
const baseOrDefault = (baseParams, key, defaultValue) => baseParams !== undefined ? baseParams[key] : defaultValue
117+
const optionOrBase = (opts, key, baseParams, defaultValue) => hasOwn(opts, key) ? opts[key] : baseOrDefault(baseParams, key, defaultValue)
118+
const booleanOptionOrBase = (opts, key, baseParams, defaultValue) => typeof opts[key] === 'boolean' ? opts[key] : baseOrDefault(baseParams, key, defaultValue)
119+
const numberOptionOrBase = (opts, key, baseParams, defaultValue) => typeof opts[key] === 'number' ? opts[key] : baseOrDefault(baseParams, key, defaultValue)
120+
const zlibOrBase = (opts, baseParams) => opts.zlib || baseOrDefault(baseParams, 'zlib', zlib)
121+
122+
function globalOptionOrBase (opts, key, baseParams) {
123+
if (typeof opts[key] === 'boolean') return opts[key]
124+
125+
if (typeof opts.global === 'boolean') return opts.global
126+
127+
return baseOrDefault(baseParams, 'global', true)
128+
}
129+
130+
function getBrotliOptions (opts, isGlobal, baseParams) {
131+
// Route-level compress objects are active compression configs, even when global compression is disabled.
132+
const useDefaults = isGlobal || baseParams !== undefined
133+
134+
if (hasOwn(opts, 'brotliOptions')) {
135+
return useDefaults
136+
? { ...recommendedDefaultBrotliOptions, ...opts.brotliOptions }
137+
: opts.brotliOptions
123138
}
124139

125-
const params = {
126-
global: (typeof opts.globalCompression === 'boolean')
127-
? opts.globalCompression
128-
: (typeof opts.global === 'boolean') ? opts.global : true
129-
}
130-
131-
params.removeContentLengthHeader = typeof opts.removeContentLengthHeader === 'boolean' ? opts.removeContentLengthHeader : true
132-
params.brotliOptions = params.global
133-
? { ...recommendedDefaultBrotliOptions, ...opts.brotliOptions }
134-
: opts.brotliOptions
135-
params.zlibOptions = opts.zlibOptions
136-
params.onUnsupportedEncoding = opts.onUnsupportedEncoding
137-
params.inflateIfDeflated = opts.inflateIfDeflated === true
138-
params.threshold = typeof opts.threshold === 'number' ? opts.threshold : 1024
139-
params.compressibleTypes = opts.customTypes instanceof RegExp
140-
? opts.customTypes.test.bind(opts.customTypes)
141-
: typeof opts.customTypes === 'function'
142-
? opts.customTypes
143-
: defaultCompressibleTypes.test.bind(defaultCompressibleTypes)
144-
params.compressStream = {
145-
br: () => ((opts.zlib || zlib).createBrotliCompress || zlib.createBrotliCompress)(params.brotliOptions),
146-
gzip: () => ((opts.zlib || zlib).createGzip || zlib.createGzip)(params.zlibOptions),
147-
deflate: () => ((opts.zlib || zlib).createDeflate || zlib.createDeflate)(params.zlibOptions)
148-
}
149-
if (typeof ((opts.zlib || zlib).createZstdCompress || zlib.createZstdCompress) === 'function') {
150-
params.compressStream.zstd = () => ((opts.zlib || zlib).createZstdCompress || zlib.createZstdCompress)(params.zlibOptions)
151-
}
152-
params.uncompressStream = {
153-
// Currently params.uncompressStream.br() is never called as we do not have any way to autodetect brotli compression in `fastify-compress`
140+
if (baseParams !== undefined && baseParams.brotliOptions !== undefined) return baseParams.brotliOptions
141+
142+
return useDefaults ? { ...recommendedDefaultBrotliOptions } : undefined
143+
}
144+
145+
function getCompressibleTypes (opts, baseParams) {
146+
if (opts.customTypes instanceof RegExp) return opts.customTypes.test.bind(opts.customTypes)
147+
148+
if (typeof opts.customTypes === 'function') return opts.customTypes
149+
150+
if (!hasOwn(opts, 'customTypes') && baseParams !== undefined) return baseParams.compressibleTypes
151+
152+
return defaultCompressibleTypes.test.bind(defaultCompressibleTypes)
153+
}
154+
155+
function getSupportedEncodings () {
156+
const supportedEncodings = ['br', 'gzip', 'deflate', 'identity']
157+
if (typeof zlib.createZstdCompress === 'function') supportedEncodings.unshift('zstd')
158+
return supportedEncodings
159+
}
160+
161+
function encodingsOrBase (opts, key, baseParams) {
162+
const supportedEncodings = getSupportedEncodings()
163+
164+
if (Array.isArray(opts[key])) {
165+
return supportedEncodings
166+
.filter(encoding => opts[key].includes(encoding))
167+
.sort((a, b) => opts[key].indexOf(a) - opts[key].indexOf(b))
168+
}
169+
170+
return hasOwn(opts, key) || baseParams === undefined ? supportedEncodings : baseParams.encodings
171+
}
172+
173+
function getCompressStream (customZlib, params) {
174+
const compressStream = {
175+
br: () => (customZlib.createBrotliCompress || zlib.createBrotliCompress)(params.brotliOptions),
176+
gzip: () => (customZlib.createGzip || zlib.createGzip)(params.zlibOptions),
177+
deflate: () => (customZlib.createDeflate || zlib.createDeflate)(params.zlibOptions)
178+
}
179+
180+
if (typeof (customZlib.createZstdCompress || zlib.createZstdCompress) === 'function') compressStream.zstd = () => (customZlib.createZstdCompress || zlib.createZstdCompress)(params.zlibOptions)
181+
182+
return compressStream
183+
}
184+
185+
function getUncompressStream (customZlib, params) {
186+
const uncompressStream = {
187+
// Currently uncompressStream.br() is never called as we do not have any way to autodetect brotli compression in `fastify-compress`
154188
// Brotli documentation reference: [RFC 7932](https://www.rfc-editor.org/rfc/rfc7932)
155-
br: /* c8 ignore next */ () => ((opts.zlib || zlib).createBrotliDecompress || zlib.createBrotliDecompress)(params.brotliOptions),
156-
gzip: () => ((opts.zlib || zlib).createGunzip || zlib.createGunzip)(params.zlibOptions),
157-
deflate: () => ((opts.zlib || zlib).createInflate || zlib.createInflate)(params.zlibOptions)
189+
br: /* c8 ignore next */ () => (customZlib.createBrotliDecompress || zlib.createBrotliDecompress)(params.brotliOptions),
190+
gzip: () => (customZlib.createGunzip || zlib.createGunzip)(params.zlibOptions),
191+
deflate: () => (customZlib.createInflate || zlib.createInflate)(params.zlibOptions)
158192
}
159-
if (typeof ((opts.zlib || zlib).createZstdDecompress || zlib.createZstdDecompress) === 'function') {
160-
// Currently params.uncompressStream.zstd() is never called as we do not have any way to autodetect zstd compression in `fastify-compress`
193+
194+
if (typeof (customZlib.createZstdDecompress || zlib.createZstdDecompress) === 'function') {
195+
// Currently uncompressStream.zstd() is never called as we do not have any way to autodetect zstd compression in `fastify-compress`
161196
// Zstd documentation reference: [RFC 8878](https://www.rfc-editor.org/rfc/rfc8878)
162-
params.uncompressStream.zstd = /* c8 ignore next */ () => ((opts.zlib || zlib).createZstdDecompress || zlib.createZstdDecompress)(params.zlibOptions)
197+
uncompressStream.zstd = /* c8 ignore next */ () => (customZlib.createZstdDecompress || zlib.createZstdDecompress)(params.zlibOptions)
163198
}
164199

165-
const supportedEncodings = ['br', 'gzip', 'deflate', 'identity']
166-
if (typeof zlib.createZstdCompress === 'function') {
167-
supportedEncodings.unshift('zstd')
200+
return uncompressStream
201+
}
202+
203+
function getDecompressStream (customZlib) {
204+
const decompressStream = {
205+
br: customZlib.createBrotliDecompress || zlib.createBrotliDecompress,
206+
gzip: customZlib.createGunzip || zlib.createGunzip,
207+
deflate: customZlib.createInflate || zlib.createInflate
168208
}
169209

170-
params.encodings = Array.isArray(opts.encodings)
171-
? supportedEncodings
172-
.filter(encoding => opts.encodings.includes(encoding))
173-
.sort((a, b) => opts.encodings.indexOf(a) - opts.encodings.indexOf(b))
174-
: supportedEncodings
210+
if (typeof (customZlib.createZstdDecompress || zlib.createZstdDecompress) === 'function') decompressStream.zstd = customZlib.createZstdDecompress || zlib.createZstdDecompress
175211

176-
return params
212+
return decompressStream
177213
}
178214

179-
function processDecompressParams (opts) {
215+
const forceEncodingOrBase = (opts, baseParams) => hasOwn(opts, 'forceRequestEncoding')
216+
? opts.forceRequestEncoding || null
217+
: baseOrDefault(baseParams, 'forceEncoding', null)
218+
219+
function processCompressParams (opts, baseParams) {
180220
/* c8 ignore next 3 */
181221
if (!opts) {
182-
return
222+
return baseParams
183223
}
184224

185-
const customZlib = opts.zlib || zlib
186-
225+
const customZlib = zlibOrBase(opts, baseParams)
187226
const params = {
188-
global: (typeof opts.globalDecompression === 'boolean')
189-
? opts.globalDecompression
190-
: (typeof opts.global === 'boolean') ? opts.global : true,
191-
onUnsupportedRequestEncoding: opts.onUnsupportedRequestEncoding,
192-
onInvalidRequestPayload: opts.onInvalidRequestPayload,
193-
decompressStream: {
194-
br: customZlib.createBrotliDecompress || zlib.createBrotliDecompress,
195-
gzip: customZlib.createGunzip || zlib.createGunzip,
196-
deflate: customZlib.createInflate || zlib.createInflate
197-
},
198-
encodings: [],
199-
forceEncoding: null
200-
}
201-
if (typeof (customZlib.createZstdDecompress || zlib.createZstdDecompress) === 'function') {
202-
params.decompressStream.zstd = customZlib.createZstdDecompress || zlib.createZstdDecompress
227+
zlib: customZlib,
228+
global: globalOptionOrBase(opts, 'globalCompression', baseParams),
229+
removeContentLengthHeader: booleanOptionOrBase(opts, 'removeContentLengthHeader', baseParams, true),
230+
zlibOptions: optionOrBase(opts, 'zlibOptions', baseParams),
231+
onUnsupportedEncoding: optionOrBase(opts, 'onUnsupportedEncoding', baseParams),
232+
inflateIfDeflated: booleanOptionOrBase(opts, 'inflateIfDeflated', baseParams, false),
233+
threshold: numberOptionOrBase(opts, 'threshold', baseParams, 1024),
234+
compressibleTypes: getCompressibleTypes(opts, baseParams)
203235
}
204236

205-
const supportedEncodings = ['br', 'gzip', 'deflate', 'identity']
206-
if (typeof zlib.createZstdCompress === 'function') {
207-
supportedEncodings.unshift('zstd')
237+
params.brotliOptions = getBrotliOptions(opts, params.global, baseParams)
238+
params.compressStream = getCompressStream(customZlib, params)
239+
params.uncompressStream = getUncompressStream(customZlib, params)
240+
params.encodings = encodingsOrBase(opts, 'encodings', baseParams)
241+
242+
return params
243+
}
244+
245+
function processDecompressParams (opts, baseParams) {
246+
/* c8 ignore next 3 */
247+
if (!opts) {
248+
return baseParams
208249
}
209250

210-
params.encodings = Array.isArray(opts.requestEncodings)
211-
? supportedEncodings
212-
.filter(encoding => opts.requestEncodings.includes(encoding))
213-
.sort((a, b) => opts.requestEncodings.indexOf(a) - opts.requestEncodings.indexOf(b))
214-
: supportedEncodings
251+
const customZlib = zlibOrBase(opts, baseParams)
215252

216-
if (opts.forceRequestEncoding) {
217-
params.forceEncoding = opts.forceRequestEncoding
253+
const params = {
254+
zlib: customZlib,
255+
global: globalOptionOrBase(opts, 'globalDecompression', baseParams),
256+
onUnsupportedRequestEncoding: optionOrBase(opts, 'onUnsupportedRequestEncoding', baseParams),
257+
onInvalidRequestPayload: optionOrBase(opts, 'onInvalidRequestPayload', baseParams),
258+
decompressStream: getDecompressStream(customZlib),
259+
encodings: encodingsOrBase(opts, 'requestEncodings', baseParams),
260+
forceEncoding: forceEncodingOrBase(opts, baseParams)
261+
}
218262

219-
if (params.encodings.includes(opts.forceRequestEncoding)) {
220-
params.encodings = [opts.forceRequestEncoding]
221-
}
263+
if (params.forceEncoding && params.encodings.includes(params.forceEncoding)) {
264+
params.encodings = [params.forceEncoding]
222265
}
223266

224267
return params

test/routes-compress.test.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,43 @@ describe('When `compress.removeContentLengthHeader` is `false`, it should not re
328328
equal(response.headers['content-length'], payload.length.toString())
329329
equal(payload.toString('utf-8'), file)
330330
})
331+
332+
test('using global setting with partial route options', async (t) => {
333+
t.plan(4)
334+
const equal = t.assert.equal
335+
336+
const fastify = Fastify()
337+
await fastify.register(compressPlugin, { global: true, removeContentLengthHeader: false })
338+
339+
fastify.get('/', {
340+
compress: { threshold: 1 }
341+
}, (_request, reply) => {
342+
readFile('./package.json', 'utf8', (err, data) => {
343+
if (err) {
344+
return reply.send(err)
345+
}
346+
347+
reply
348+
.type('text/plain')
349+
.header('content-length', '' + data.length)
350+
.send(data)
351+
})
352+
})
353+
354+
const response = await fastify.inject({
355+
url: '/',
356+
method: 'GET',
357+
headers: {
358+
'accept-encoding': 'deflate'
359+
}
360+
})
361+
const file = readFileSync('./package.json', 'utf8')
362+
const payload = zlib.inflateSync(response.rawPayload)
363+
equal(response.headers.vary, 'accept-encoding')
364+
equal(response.headers['content-encoding'], 'deflate')
365+
equal(response.headers['content-length'], payload.length.toString())
366+
equal(payload.toString('utf-8'), file)
367+
})
331368
})
332369

333370
describe('When using the old routes `{ config: compress }` option :', async () => {

test/routes-decompress.test.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,54 @@ describe('When using routes `decompress` settings :', async () => {
138138
equal(response.body, '@fastify/compress')
139139
})
140140

141+
test('it should keep global `onInvalidRequestPayload` when route options are partial', async (t) => {
142+
t.plan(2)
143+
144+
const fastify = Fastify()
145+
await fastify.register(compressPlugin, {
146+
onInvalidRequestPayload (encoding, _request, error) {
147+
return {
148+
statusCode: 422,
149+
code: 'INVALID_PAYLOAD',
150+
error: 'Unprocessable Entity',
151+
message: `Global handler used for ${encoding}: ${error.message}.`
152+
}
153+
}
154+
})
155+
156+
fastify.post('/', {
157+
decompress: {
158+
onUnsupportedRequestEncoding (encoding) {
159+
return {
160+
statusCode: 415,
161+
code: 'UNSUPPORTED_ENCODING',
162+
error: 'Unsupported Media Type',
163+
message: `${encoding} is not supported.`
164+
}
165+
}
166+
}
167+
}, (request, reply) => {
168+
reply.send(request.body.name)
169+
})
170+
171+
const response = await fastify.inject({
172+
url: '/',
173+
method: 'POST',
174+
headers: {
175+
'content-type': 'application/json',
176+
'content-encoding': 'deflate'
177+
},
178+
payload: createPayload(zlib.createGzip)
179+
})
180+
t.assert.equal(response.statusCode, 422)
181+
t.assert.deepEqual(response.json(), {
182+
statusCode: 422,
183+
code: 'INVALID_PAYLOAD',
184+
error: 'Unprocessable Entity',
185+
message: 'Global handler used for deflate: incorrect header check.'
186+
})
187+
})
188+
141189
test('it should not decompress data when route `decompress` option is set to `false`', async (t) => {
142190
t.plan(6)
143191
const equal = t.assert.equal

0 commit comments

Comments
 (0)