Skip to content

Commit e399f43

Browse files
committed
fix: prevents fastify-compress from trying to work with something it cannot
Sending something other than buffers of strings in the `payload` happens, but this would break the later `Buffer.byteLength(payload)` Signed-off-by: Bart Riepe <[email protected]>
1 parent 331457b commit e399f43

File tree

3 files changed

+102
-1
lines changed

3 files changed

+102
-1
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ yarn.lock
147147
# editor files
148148
.vscode
149149
.idea
150+
.zed
150151

151152
#tap files
152153
.tap/

index.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,10 @@ function processCompressParams (opts) {
161161
.sort((a, b) => opts.encodings.indexOf(a) - opts.encodings.indexOf(b))
162162
: supportedEncodings
163163

164+
params.isCompressiblePayload = typeof opts.isCompressiblePayload === 'function'
165+
? opts.isCompressiblePayload
166+
: isCompressiblePayload
167+
164168
return params
165169
}
166170

@@ -273,6 +277,11 @@ function buildRouteCompress (_fastify, params, routeOptions, decorateOnly) {
273277
}
274278

275279
if (typeof payload.pipe !== 'function') {
280+
// Payload is not a stream, ensure we don't try to compress something we cannot get the length of.
281+
if (!params.isCompressiblePayload(payload)) {
282+
return next(null, payload)
283+
}
284+
276285
if (Buffer.byteLength(payload) < params.threshold) {
277286
return next()
278287
}
@@ -391,7 +400,7 @@ function compress (params) {
391400
}
392401

393402
if (typeof payload.pipe !== 'function') {
394-
if (!Buffer.isBuffer(payload) && typeof payload !== 'string') {
403+
if (!params.isCompressiblePayload(payload)) {
395404
payload = this.serialize(payload)
396405
}
397406
}
@@ -477,6 +486,13 @@ function getEncodingHeader (encodings, request) {
477486
}
478487
}
479488

489+
function isCompressiblePayload (payload) {
490+
// By the time payloads reach this point, Fastify has already serialized
491+
// objects/arrays/etc to strings, so we only need to check for the actual
492+
// types that make it through: Buffer and string
493+
return Buffer.isBuffer(payload) || typeof payload === 'string'
494+
}
495+
480496
function shouldCompress (type, compressibleTypes) {
481497
if (compressibleTypes(type)) return true
482498
const data = mimedb[type.split(';', 1)[0].trim().toLowerCase()]

test/global-compress.test.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3298,3 +3298,87 @@ for (const contentType of notByDefaultSupportedContentTypes) {
32983298
t.assert.equal(response.rawPayload.toString('utf-8'), file)
32993299
})
33003300
}
3301+
3302+
test('It should not compress non-buffer/non-string payloads', async (t) => {
3303+
t.plan(4)
3304+
3305+
let payloadTypeChecked = null
3306+
let payloadReceived = null
3307+
const testIsCompressiblePayload = (payload) => {
3308+
payloadTypeChecked = typeof payload
3309+
payloadReceived = payload
3310+
// Return false for objects, true for strings/buffers like the original
3311+
return Buffer.isBuffer(payload) || typeof payload === 'string'
3312+
}
3313+
3314+
const fastify = Fastify()
3315+
await fastify.register(compressPlugin, {
3316+
isCompressiblePayload: testIsCompressiblePayload
3317+
})
3318+
3319+
// Create a Response-like object that might come from another plugin
3320+
const responseObject = new Response('{"message": "test"}', {
3321+
status: 200,
3322+
headers: { 'content-type': 'application/json' }
3323+
})
3324+
3325+
fastify.get('/', (_request, reply) => {
3326+
// Simulate a scenario where another plugin sets a Response object as the payload
3327+
// We use an onSend hook to intercept and replace the payload before compression to simulate that behavior
3328+
reply.header('content-type', 'application/json')
3329+
reply.send('{"message": "test"}')
3330+
})
3331+
3332+
// Add the onSend hook that replaces the payload with a Response object
3333+
fastify.addHook('onSend', async () => {
3334+
return responseObject
3335+
})
3336+
3337+
const response = await fastify.inject({
3338+
url: '/',
3339+
method: 'GET',
3340+
headers: {
3341+
'accept-encoding': 'gzip, deflate, br'
3342+
}
3343+
})
3344+
3345+
t.assert.equal(response.statusCode, 200)
3346+
// The response should not be compressed since the payload is a Response object
3347+
t.assert.equal(response.headers['content-encoding'], undefined)
3348+
// Verify that the payload was a Response object when isCompressiblePayload was called
3349+
t.assert.equal(payloadTypeChecked, 'object')
3350+
t.assert.equal(payloadReceived instanceof Response, true)
3351+
})
3352+
3353+
test('It should serialize and compress objects when reply.compress() receives non-compressible objects', async (t) => {
3354+
t.plan(2)
3355+
3356+
const fastify = Fastify()
3357+
await fastify.register(compressPlugin, {
3358+
threshold: 0 // Ensure even small payloads get compressed
3359+
})
3360+
3361+
// Create a larger object to ensure it exceeds any default threshold
3362+
const objectPayload = {
3363+
message: 'test data'.repeat(100),
3364+
value: 42,
3365+
description: 'A test object that should be large enough to trigger compression after serialization'.repeat(10)
3366+
}
3367+
3368+
fastify.get('/', (_request, reply) => {
3369+
reply.header('content-type', 'application/json')
3370+
// The compress function should now serialize the object and then compress it
3371+
reply.compress(objectPayload)
3372+
})
3373+
3374+
const response = await fastify.inject({
3375+
url: '/',
3376+
method: 'GET',
3377+
headers: {
3378+
'accept-encoding': 'gzip, deflate, br'
3379+
}
3380+
})
3381+
3382+
t.assert.equal(response.statusCode, 200)
3383+
t.assert.ok(['gzip', 'deflate', 'br'].includes(response.headers['content-encoding']))
3384+
})

0 commit comments

Comments
 (0)