Skip to content

Commit 599207e

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 <bart@serial-experiments.com>
1 parent 331457b commit 599207e

File tree

3 files changed

+110
-0
lines changed

3 files changed

+110
-0
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: 21 additions & 0 deletions
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
}
@@ -397,6 +406,11 @@ function compress (params) {
397406
}
398407

399408
if (typeof payload.pipe !== 'function') {
409+
// Ensure we don't try to compress something we cannot get the length of.
410+
if (!params.isCompressiblePayload(payload)) {
411+
return this.send(payload)
412+
}
413+
400414
if (Buffer.byteLength(payload) < params.threshold) {
401415
return this.send(payload)
402416
}
@@ -477,6 +491,13 @@ function getEncodingHeader (encodings, request) {
477491
}
478492
}
479493

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

test/global-compress.test.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3298,3 +3298,91 @@ 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'll use an onSend hook to intercept and replace the payload before compression
3328+
reply.header('content-type', 'application/json')
3329+
reply.send('{"message": "test"}')
3330+
})
3331+
3332+
// Add an onSend hook that replaces the payload with a Response object
3333+
fastify.addHook('onSend', async (request, reply, payload) => {
3334+
// Only do this for our test route
3335+
if (request.url === '/') {
3336+
return responseObject
3337+
}
3338+
return payload
3339+
})
3340+
3341+
const response = await fastify.inject({
3342+
url: '/',
3343+
method: 'GET',
3344+
headers: {
3345+
'accept-encoding': 'gzip, deflate, br'
3346+
}
3347+
})
3348+
3349+
t.assert.equal(response.statusCode, 200)
3350+
// The response should not be compressed since the payload is a Response object
3351+
t.assert.equal(response.headers['content-encoding'], undefined)
3352+
// Verify that the payload was a Response object when isCompressiblePayload was called
3353+
t.assert.equal(payloadTypeChecked, 'object')
3354+
t.assert.equal(payloadReceived instanceof Response, true)
3355+
})
3356+
3357+
test('It should not crash with Buffer.byteLength error when reply.compress() receives non-compressible objects', async (t) => {
3358+
t.plan(3)
3359+
3360+
const fastify = Fastify()
3361+
await fastify.register(compressPlugin)
3362+
3363+
// Create a Response object that would cause Buffer.byteLength to throw without isCompressiblePayload check
3364+
const responseObject = new Response('{"message": "test"}', {
3365+
status: 200,
3366+
headers: { 'content-type': 'application/json' }
3367+
})
3368+
3369+
fastify.get('/', (request, reply) => {
3370+
reply.header('content-type', 'application/json')
3371+
// This would previously cause: TypeError [ERR_INVALID_ARG_TYPE]: The "string" argument must be of type string or an instance of Buffer or ArrayBuffer. Received an instance of Response
3372+
reply.compress(responseObject)
3373+
})
3374+
3375+
const response = await fastify.inject({
3376+
url: '/',
3377+
method: 'GET',
3378+
headers: {
3379+
'accept-encoding': 'gzip, deflate, br'
3380+
}
3381+
})
3382+
3383+
t.assert.equal(response.statusCode, 200)
3384+
// The response should not be compressed since the payload is a Response object
3385+
t.assert.equal(response.headers['content-encoding'], undefined)
3386+
// Verify we get a response body (the Response object was handled gracefully)
3387+
t.assert.ok(response.body.length > 0)
3388+
})

0 commit comments

Comments
 (0)