From ff08637e123cf754937ce4cc3491a23aea7c8b1d Mon Sep 17 00:00:00 2001 From: Paolo Insogna Date: Fri, 29 May 2026 14:21:39 +0000 Subject: [PATCH] feat: Added handler option. Signed-off-by: Paolo Insogna --- README.md | 15 ++++++++++++ index.js | 6 ++++- src/options.js | 4 ++++ test/options.js | 3 +++ test/test.js | 58 ++++++++++++++++++++++++++++++++++++++++++++++ types/index.d.ts | 8 +++++++ types/index.tst.ts | 8 +++++++ 7 files changed, 101 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 20c34d9..bd65ede 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,21 @@ Rewrite the prefix to the specified string. Default: `''`. A `preHandler` to be applied on all routes. Useful for performing actions before the proxy is executed (e.g. check for authentication). +### `handler` + +A function to replace the default HTTP proxy handler. It receives the request, reply, rewritten destination, and reply options. + +By default, the handler calls `reply.from(dest, options)`. Use this option to customize the response handling while keeping the proxy URL rewriting behavior. + +```javascript +fastify.register(proxy, { + upstream: 'http://api-upstream.com', + async handler (request, reply, dest, options) { + return reply.from(dest, options) + } +}) +``` + ### `proxyPayloads` When this option is `false`, you will be able to access the body but it will also disable direct pass through of the payload. As a result, it is left up to the implementation to properly parse and proxy the payload correctly. diff --git a/index.js b/index.js index 12b5fcd..b36b6e7 100644 --- a/index.js +++ b/index.js @@ -522,6 +522,10 @@ function generateRewritePrefix (prefix, opts) { async function fastifyHttpProxy (fastify, opts) { opts = validateOptions(opts) + const replyHandler = opts.handler ?? function replyHandler (_, reply, dest, options) { + return reply.from(dest, options) + } + const preHandler = opts.preHandler || opts.beforeHandler const preRewrite = typeof opts.preRewrite === 'function' ? opts.preRewrite : noopPreRewrite const rewritePrefix = generateRewritePrefix(fastify.prefix, opts) @@ -631,7 +635,7 @@ async function fastifyHttpProxy (fastify, opts) { } /* c8 ignore stop */ return } - reply.from(dest, options) + return replyHandler(request, reply, dest, options) } fastify.decorateReply('fromParameters', fromParameters) diff --git a/src/options.js b/src/options.js index 658bdc5..af2425c 100644 --- a/src/options.js +++ b/src/options.js @@ -13,6 +13,10 @@ function validateOptions (options) { throw new Error('upstream must be specified') } + if (options.handler !== undefined && typeof options.handler !== 'function') { + throw new Error('handler must be a function') + } + if (options.wsReconnect) { const wsReconnect = options.wsReconnect diff --git a/test/options.js b/test/options.js index fea5bf5..db5b405 100644 --- a/test/options.js +++ b/test/options.js @@ -14,6 +14,9 @@ test('validateOptions', (t) => { assert.throws(() => validateOptions({}), /upstream must be specified/) + assert.throws(() => validateOptions({ ...requiredOptions, handler: '1' }), /handler must be a function/) + assert.doesNotThrow(() => validateOptions({ ...requiredOptions, handler: () => { } })) + assert.throws(() => validateOptions({ ...requiredOptions, wsReconnect: { pingInterval: -1 } }), /wsReconnect.pingInterval must be a non-negative number/) assert.throws(() => validateOptions({ ...requiredOptions, wsReconnect: { pingInterval: '1' } }), /wsReconnect.pingInterval must be a non-negative number/) assert.doesNotThrow(() => validateOptions({ ...requiredOptions, wsReconnect: { pingInterval: 1 } })) diff --git a/test/test.js b/test/test.js index da7bfac..674374f 100644 --- a/test/test.js +++ b/test/test.js @@ -437,6 +437,64 @@ async function run () { } }) + test('custom handler can handle response', async t => { + const proxyServer = Fastify() + + proxyServer.register(proxy, { + upstream: `http://localhost:${origin.server.address().port}`, + prefix: '/api', + rewritePrefix: '/api2', + replyOptions: { + contentType: 'text/plain' + }, + handler (request, reply, dest, options) { + t.assert.strictEqual(request.url, '/api/a') + t.assert.strictEqual(dest, '/api2/a') + t.assert.strictEqual(options.contentType, 'text/plain') + + return reply.send({ dest }) + } + }) + + await proxyServer.listen({ port: 0 }) + + t.after(() => { + proxyServer.close() + }) + + const response = await fetch(`http://localhost:${proxyServer.server.address().port}/api/a`) + const body = await response.json() + + t.assert.strictEqual(response.status, 200) + t.assert.deepStrictEqual(body, { dest: '/api2/a' }) + }) + + test('custom handler can delegate to reply.from()', async t => { + const proxyServer = Fastify() + + proxyServer.register(proxy, { + upstream: `http://localhost:${origin.server.address().port}`, + prefix: '/api', + rewritePrefix: '/api2', + handler (_request, reply, dest, options) { + t.assert.strictEqual(dest, '/api2/a') + return reply.from(dest, options) + } + }) + + await proxyServer.listen({ port: 0 }) + + t.after(() => { + proxyServer.close() + }) + + const response = await fetch(`http://localhost:${proxyServer.server.address().port}/api/a`) + const body = await response.text() + + t.assert.strictEqual(response.status, 200) + t.assert.strictEqual(body, 'this is /api2/a') + }) + test('rewritePrefix', async t => { const proxyServer = Fastify() diff --git a/types/index.d.ts b/types/index.d.ts index 7763d5c..3438a86 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -40,6 +40,13 @@ type ProxyPreValidationHookHandler = ( done: Parameters[2] ) => void +type ProxyHandler = ( + request: FastifyRequest, + reply: FastifyReplyWithFromParameters, + dest: string, + options: FastifyReplyFromHooks +) => unknown + interface WebSocketHooks { onConnect?: (context: { log: Logger }, source: WebSocket, target: WebSocket) => void; onDisconnect?: (context: { log: Logger }, source: WebSocket) => void; @@ -97,6 +104,7 @@ declare namespace fastifyHttpProxy { preHandler?: ProxyPreHandlerHookHandler; beforeHandler?: ProxyPreHandlerHookHandler; preValidation?: ProxyPreValidationHookHandler; + handler?: ProxyHandler; preRewrite?: ProxyPreRewriteHookHandler; config?: Object; replyOptions?: FastifyReplyFromHooks; diff --git a/types/index.tst.ts b/types/index.tst.ts index 685ad94..6203c71 100644 --- a/types/index.tst.ts +++ b/types/index.tst.ts @@ -6,6 +6,7 @@ import fastify, { type RequestGenericInterface, } from 'fastify' import { expect } from 'tstyche' +import { type FastifyReplyFromHooks } from '@fastify/reply-from' import fastifyHttpProxy from '..' const app = fastify() @@ -59,6 +60,13 @@ app.register(fastifyHttpProxy, { expect(result.options).type.toBe() expect(result.url).type.toBe() }, + handler: (request, reply, dest, options) => { + expect(request).type.toBe() + expect(reply.raw).type.toBe() + expect(dest).type.toBe() + expect(options).type.toBe() + return reply.from(dest, options) + }, preRewrite: (url, params, prefix): string => { expect(url).type.toBe() expect(params).type.toBe()