From 4c66fccaaa4a133b866a7c7f884d4c0fd129f85a Mon Sep 17 00:00:00 2001 From: Philipp Winterle Date: Thu, 17 Sep 2026 15:48:50 +0200 Subject: [PATCH] fix: Don't crash when a remote beforeRequest callback returns nothing beforeRequest is typed as returning CallbackRequestResult | void, and an inspection-only callback that returns nothing is explicitly supported: the in-process path reads its result as modifiedReq?.response, and serialize() sends undefined over the wire for exactly that case. The receiving side didn't expect it. It passed the result straight into withDeserializedCallbackBuffers, which reads .body off it, so every such callback failed the request with "Cannot read properties of undefined (reading 'body')" whenever the rule ran behind an admin server. beforeResponse, a few lines below, already guards this. beforeRequest now does the same, and its channel.request type admits undefined, matching what serialize() declares on the other end. --- src/rules/requests/request-step-impls.ts | 20 ++++++++++++-------- test/integration/remote-client.spec.ts | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/rules/requests/request-step-impls.ts b/src/rules/requests/request-step-impls.ts index 4ac122875..54af70222 100644 --- a/src/rules/requests/request-step-impls.ts +++ b/src/rules/requests/request-step-impls.ts @@ -1328,14 +1328,18 @@ export class PassThroughStepImpl extends PassThroughStep { let beforeRequest: ((req: CompletedRequest) => MaybePromise) | undefined; if (data.hasBeforeRequestCallback) { beforeRequest = async (req: CompletedRequest) => { - const result = withDeserializedCallbackBuffers( - await channel.request< - BeforePassthroughRequestRequest, - WithSerializedCallbackBuffers - >('beforeRequest', { - args: [await withSerializedBodyReader(req, bodySerializer)] - }) - ); + const callbackResult = await channel.request< + BeforePassthroughRequestRequest, + WithSerializedCallbackBuffers | undefined + >('beforeRequest', { + args: [await withSerializedBodyReader(req, bodySerializer)] + }); + + // Inspection-only callbacks return nothing at all, and the client + // sends that through as undefined, so there's nothing to deserialize: + if (!callbackResult) return callbackResult; + + const result = withDeserializedCallbackBuffers(callbackResult); if (result.response && typeof result.response !== 'string') { result.response = withDeserializedCallbackBuffers( diff --git a/test/integration/remote-client.spec.ts b/test/integration/remote-client.spec.ts index 2c0534ca0..6c2672733 100644 --- a/test/integration/remote-client.spec.ts +++ b/test/integration/remote-client.spec.ts @@ -154,6 +154,25 @@ nodeOnly(() => { expect(await targetEndpoint.getSeenRequests()).to.deep.equal([]); }); + it("should successfully pass through requests when beforeRequest returns nothing", async () => { + await targetServer.forGet('/').thenReply(200, 'target response'); + + let seenMethod: string | undefined; + await remoteServer.forGet(targetServer.url).thenPassThrough({ + beforeRequest: (req) => { + // Inspection-only callbacks return nothing at all: + seenMethod = req.method; + } + }); + + const response = await request.get(targetServer.url, { + proxy: remoteServer.url + }); + + expect(seenMethod).to.equal('GET'); + expect(response).to.equal('target response'); + }); + it("should successfully replace request & response bodies", async () => { // Echo the incoming request await targetServer.forAnyRequest().thenCallback(async (req) => ({