From 95fd1243280b5aadc73b46ec81bb6227d12d697b Mon Sep 17 00:00:00 2001 From: Michael Scholz Date: Thu, 23 Apr 2026 15:46:13 +0000 Subject: [PATCH] test: exercise async handler + compress onSend interaction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document and regression-test the two async-handler patterns from the Fastify Routes reference § Promise resolution against the compress stream path. Existing `onSend` hook test uses a sync callback-style handler so it never enters wrap-thenable; neither does any other current test in this file. The interaction only surfaces with async handlers producing a response above the compression threshold. Both documented-correct patterns are covered: - `return ` (let Fastify serialize + send) - `return reply.send(...)` (when you need reply.* side-effects) Ref: https://fastify.dev/docs/latest/Reference/Routes/#promise-resolution Ref: fastify/fastify#6682 (closed — WAD per the docs above) --- test/routes-compress.test.js | 58 ++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/test/routes-compress.test.js b/test/routes-compress.test.js index fc5e5bf..7c25d65 100644 --- a/test/routes-compress.test.js +++ b/test/routes-compress.test.js @@ -470,3 +470,61 @@ test('reply.compress should handle Web ReadableStream', async (t) => { const res = await fastify.inject({ url: '/', method: 'GET', headers: { 'accept-encoding': 'gzip' } }) t.assert.equal(zlib.gunzipSync(res.rawPayload).toString('utf8'), 'from webstream') }) + +// Regression: async route handlers that produce a compressed body must follow one of the +// patterns documented in the Fastify Routes reference — otherwise `wrap-thenable` fires a second +// `reply.send(undefined)` while the gzip stream is still piping, silently emptying the response +// to `Content-Length: 0`. See: +// https://fastify.dev/docs/latest/Reference/Routes/#promise-resolution +// +// These tests exercise both documented-correct patterns against the compress onSend stream path +// so future regressions in the interaction surface here rather than as empty 200s in production. +describe('When an async handler produces a compressed response :', async () => { + const bigPayload = JSON.stringify( + Array.from({ length: 500 }, (_, i) => ({ + id: `id-${i}`, name: `item ${i}`, createdAt: new Date().toISOString() + })) + ) + + test('it should deliver the full compressed body when the handler returns the payload', async (t) => { + t.plan(3) + const fastify = Fastify() + await fastify.register(compressPlugin, { global: true, threshold: 5000 }) + + fastify.get('/', async (_req, reply) => { + reply.header('content-type', 'application/json') + return bigPayload + }) + + const res = await fastify.inject({ + url: '/', + method: 'GET', + headers: { 'accept-encoding': 'gzip' } + }) + + t.assert.equal(res.statusCode, 200) + t.assert.equal(res.headers['content-encoding'], 'gzip') + t.assert.equal(zlib.gunzipSync(res.rawPayload).toString('utf8'), bigPayload) + }) + + test('it should deliver the full compressed body when the handler does `return reply.send(...)`', async (t) => { + t.plan(3) + const fastify = Fastify() + await fastify.register(compressPlugin, { global: true, threshold: 5000 }) + + fastify.get('/', async (_req, reply) => { + reply.header('content-type', 'application/json') + return reply.send(bigPayload) + }) + + const res = await fastify.inject({ + url: '/', + method: 'GET', + headers: { 'accept-encoding': 'gzip' } + }) + + t.assert.equal(res.statusCode, 200) + t.assert.equal(res.headers['content-encoding'], 'gzip') + t.assert.equal(zlib.gunzipSync(res.rawPayload).toString('utf8'), bigPayload) + }) +})