From c5d3271a6f34c39b5698dce20445e4084741e170 Mon Sep 17 00:00:00 2001 From: barry <91018388+barry166@users.noreply.github.com> Date: Tue, 30 Jun 2026 23:17:30 +0800 Subject: [PATCH] Document async onResponse usage The runtime already allows an async onResponse callback to wait before sending a replacement body, but the docs and TypeScript hook type only described synchronous callbacks. This records the supported usage with runtime and type coverage instead of changing the proxy path. Constraint: fastify/fastify-reply-from#380 asks about async onResponse usage and maintainers requested PRs with tests Rejected: Change runtime control flow | async callbacks that call reply.send after await already work in the existing callback path Confidence: high Scope-risk: narrow Directive: Keep async callback errors user-handled unless the runtime explicitly adds async error routing Tested: npm run test:unit -- test/onResponse.test.js; npm run test:typescript; npm run lint; git diff --check Not-tested: Full npm test has pre-existing local failures for invalid-target expectations and Unix socket path length on this macOS checkout --- README.md | 14 ++++++++++++++ test/onResponse.test.js | 35 +++++++++++++++++++++++++++++++++++ types/index.d.ts | 2 +- types/index.tst.ts | 10 ++++++++++ 4 files changed, 60 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 930fc43c..2fd661a8 100644 --- a/README.md +++ b/README.md @@ -380,6 +380,20 @@ the `content-length` header. } ``` +`onResponse` may also be async when you need to read the upstream response +before sending a replacement body: + +```js +{ + async onResponse (request, reply, res) { + const data = await new Response(res.stream).json() + reply.send({ ...data, proxied: true }) + } +} +``` + +Handle errors inside async `onResponse` callbacks before replying. + **Note**: `onResponse` is called after headers have already been sent. If you want to modify response headers, use the `rewriteHeaders` hook. #### `onError(reply, error)` diff --git a/test/onResponse.test.js b/test/onResponse.test.js index b442a71d..f3213ea4 100644 --- a/test/onResponse.test.js +++ b/test/onResponse.test.js @@ -40,3 +40,38 @@ t.test('onResponse', async (t) => { t.assert.strictEqual(result.statusCode, 200) t.assert.strictEqual(await result.body.text(), 'hello world') }) + +t.test('async onResponse', async (t) => { + t.plan(6) + + const instance = Fastify() + instance.register(From) + t.after(() => instance.close()) + + const target = http.createServer((req, res) => { + t.assert.ok('request proxied') + t.assert.strictEqual(req.method, 'GET') + res.statusCode = 200 + res.end('hello world') + }) + + instance.get('/async', (request1, reply) => { + reply.from(`http://localhost:${target.address().port}`, { + async onResponse (request2, reply, res) { + await Promise.resolve() + t.assert.strictEqual(res.statusCode, 200) + t.assert.strictEqual(request1.raw, request2.raw) + reply.send(res.stream) + } + }) + }) + + t.after(() => target.close()) + + await instance.listen({ port: 0 }) + await new Promise(resolve => target.listen({ port: 0 }, resolve)) + + const result = await request(`http://localhost:${instance.server.address().port}/async`) + t.assert.strictEqual(result.statusCode, 200) + t.assert.strictEqual(await result.body.text(), 'hello world') +}) diff --git a/types/index.d.ts b/types/index.d.ts index 6a8fb272..c806ba28 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -75,7 +75,7 @@ declare namespace fastifyReplyFrom { request: FastifyRequest, reply: FastifyReply, res: RawServerResponse - ) => void; + ) => void | Promise; onError?: ( reply: FastifyReply, error: { error: Error } diff --git a/types/index.tst.ts b/types/index.tst.ts index 5e2bf6f8..d544bbd8 100644 --- a/types/index.tst.ts +++ b/types/index.tst.ts @@ -89,6 +89,16 @@ app.get('/v3', (_request, reply) => { }) }) +app.get('/async-on-response', (_request, reply) => { + reply.from('/async-on-response', { + async onResponse (request, reply, res) { + expect(request).type.toBe>() + expect(reply).type.toBe>() + expect(res).type.toBe>() + } + }) +}) + app.get('/http2', (_request, reply) => { reply.from('/', { method: 'POST',