Skip to content

Commit 8e2c24e

Browse files
committed
Take the beforeRequest+forwarding fix further to cover more edge cases
1 parent 51bb2c1 commit 8e2c24e

File tree

2 files changed

+89
-23
lines changed

2 files changed

+89
-23
lines changed

src/rules/requests/request-handlers.ts

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,19 @@ export class PassThroughHandler extends PassThroughHandlerDefinition {
415415
let { method, url: reqUrl, rawHeaders } = clientReq as OngoingRequest;
416416
let { protocol, hostname, port, path } = url.parse(reqUrl);
417417

418+
// Check if this request is a request loop:
419+
if (isSocketLoop(this.outgoingSockets, (<any> clientReq).socket)) {
420+
throw new Error(oneLine`
421+
Passthrough loop detected. This probably means you're sending a request directly
422+
to a passthrough endpoint, which is forwarding it to the target URL, which is a
423+
passthrough endpoint, which is forwarding it to the target URL, which is a
424+
passthrough endpoint...` +
425+
'\n\n' + oneLine`
426+
You should either explicitly mock a response for this URL (${reqUrl}), or use
427+
the server as a proxy, instead of making requests to it directly.
428+
`);
429+
}
430+
418431
// We have to capture the request stream immediately, to make sure nothing is lost if it
419432
// goes past its max length (truncating the data) before we start sending upstream.
420433
const clientReqBody = clientReq.body.asStream();
@@ -456,19 +469,8 @@ export class PassThroughHandler extends PassThroughHandlerDefinition {
456469
// If it's an explicit custom value, use that directly.
457470
hostHeader[1] = updateHostHeader;
458471
} // Otherwise: falsey means don't touch it.
459-
}
460472

461-
// Check if this request is a request loop:
462-
if (isSocketLoop(this.outgoingSockets, (<any> clientReq).socket)) {
463-
throw new Error(oneLine`
464-
Passthrough loop detected. This probably means you're sending a request directly
465-
to a passthrough endpoint, which is forwarding it to the target URL, which is a
466-
passthrough endpoint, which is forwarding it to the target URL, which is a
467-
passthrough endpoint...` +
468-
'\n\n' + oneLine`
469-
You should either explicitly mock a response for this URL (${reqUrl}), or use
470-
the server as a proxy, instead of making requests to it directly.
471-
`);
473+
reqUrl = new URL(`${protocol}//${hostname}${(port ? `:${port}` : '')}/${path}`).toString();
472474
}
473475

474476
// Override the request details, if a transform or callback is specified:
@@ -571,10 +573,14 @@ export class PassThroughHandler extends PassThroughHandlerDefinition {
571573
}
572574
} else if (this.beforeRequest) {
573575
const completedRequest = await waitForCompletedRequest(clientReq);
576+
const clientRawHeaders = completedRequest.rawHeaders;
577+
const clientHeaders = rawHeadersToObject(clientRawHeaders);
578+
574579
const modifiedReq = await this.beforeRequest({
575580
...completedRequest,
576-
headers: _.cloneDeep(rawHeadersToObject(completedRequest.rawHeaders)),
577-
rawHeaders: _.cloneDeep(completedRequest.rawHeaders)
581+
url: reqUrl, // May have been overwritten by forwarding
582+
headers: _.cloneDeep(clientHeaders),
583+
rawHeaders: _.cloneDeep(clientRawHeaders)
578584
});
579585

580586
if (modifiedReq?.response) {
@@ -597,15 +603,24 @@ export class PassThroughHandler extends PassThroughHandlerDefinition {
597603
reqUrl = modifiedReq?.url || reqUrl;
598604

599605
headersManuallyModified = !!modifiedReq?.headers;
600-
const clientHeaders = rawHeadersToObject(clientReq.rawHeaders)
601606
let headers = modifiedReq?.headers || clientHeaders;
602-
if (!this.forwarding || this.forwarding.updateHostHeader === false) {
603-
Object.assign(headers,
604-
isH2Downstream
605-
? getH2HeadersAfterModification(reqUrl, clientHeaders, modifiedReq?.headers)
606-
: { 'host': getHostAfterModification(reqUrl, clientHeaders, modifiedReq?.headers) }
607+
608+
// We need to make sure the Host/:authority header is updated correctly - following the user's returned value if
609+
// they provided one, but updating it if not to match the effective target URL of the request:
610+
const expectedTargetUrl = modifiedReq?.url
611+
?? (
612+
// If not overridden, we fall back to the original value, but we need to handle changes that forwarding
613+
// might have made as well, especially if it's intentionally left URL & headers out of sync:
614+
this.forwarding?.updateHostHeader === false
615+
? clientReq.url
616+
: reqUrl
607617
);
608-
}
618+
619+
Object.assign(headers,
620+
isH2Downstream
621+
? getH2HeadersAfterModification(expectedTargetUrl, clientHeaders, modifiedReq?.headers)
622+
: { 'host': getHostAfterModification(expectedTargetUrl, clientHeaders, modifiedReq?.headers) }
623+
);
609624

610625
validateCustomHeaders(
611626
clientHeaders,

test/integration/proxying/proxy-transforms.spec.ts

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,15 +106,66 @@ nodeOnly(() => {
106106
it("can update the host header when used with beforeRequest", async () => {
107107
let remoteEndpointMock = await remoteServer.forGet('/get').thenReply(200, "mocked data");
108108
await server.forAnyRequest().thenForwardTo(remoteServer.url, {
109-
beforeRequest: () => {},
110-
forwarding: { updateHostHeader: true }
109+
beforeRequest: () => {}
111110
});
112111

113112
await request.get(server.urlFor("/get"));
114113

115114
let seenRequests = await remoteEndpointMock.getSeenRequests();
116115
expect(seenRequests[0].headers.host).to.equal(`localhost:${remoteServer.port}`);
117116
});
117+
118+
it("can avoid updating the host header when used with beforeRequest", async () => {
119+
let remoteEndpointMock = await remoteServer.forGet('/get').thenReply(200, "mocked data");
120+
await server.forAnyRequest().thenForwardTo(remoteServer.url, {
121+
beforeRequest: () => {},
122+
forwarding: { updateHostHeader: false }
123+
});
124+
125+
await request.get(server.urlFor("/get"));
126+
127+
let seenRequests = await remoteEndpointMock.getSeenRequests();
128+
expect(seenRequests[0].headers.host).to.equal(`localhost:${server.port}`);
129+
});
130+
131+
it("doesn't override the host header if beforeRequest does instead", async () => {
132+
let remoteEndpointMock = await remoteServer.forGet('/get').thenReply(200, "mocked data");
133+
await server.forAnyRequest().thenForwardTo(remoteServer.url, {
134+
beforeRequest: () => ({ url: 'http://never.test' })
135+
});
136+
137+
const response = await request.get(server.urlFor("/get")).catch(e => e);
138+
139+
expect(response).to.be.instanceOf(Error);
140+
expect(response.message).to.include('ENOTFOUND never.test');
141+
});
142+
143+
it("overrides the host header correctly if not set", async () => {
144+
let remoteEndpointMock = await remoteServer.forGet('/get').thenReply(200, "mocked data");
145+
await server.forAnyRequest().thenForwardTo(remoteServer.url, {
146+
beforeRequest: () => ({ headers: { 'other-header': 'injected-value' } })
147+
});
148+
149+
await request.get(server.urlFor("/get")).catch(e => e);
150+
151+
let seenRequests = await remoteEndpointMock.getSeenRequests();
152+
expect(seenRequests[0].headers.host).to.equal(`localhost:${remoteServer.port}`); // <-- Preserves new host
153+
expect(seenRequests[0].headers['other-header']).to.equal('injected-value');
154+
});
155+
156+
it("overrides the host header correctly if not set", async () => {
157+
let remoteEndpointMock = await remoteServer.forGet('/get').thenReply(200, "mocked data");
158+
await server.forAnyRequest().thenForwardTo(remoteServer.url, {
159+
beforeRequest: () => ({ headers: { 'other-header': 'injected-value' } }),
160+
forwarding: { updateHostHeader: false }
161+
});
162+
163+
await request.get(server.urlFor("/get")).catch(e => e);
164+
165+
let seenRequests = await remoteEndpointMock.getSeenRequests();
166+
expect(seenRequests[0].headers.host).to.equal(`localhost:${server.port}`); // <-- Preserves original host
167+
expect(seenRequests[0].headers['other-header']).to.equal('injected-value');
168+
});
118169
});
119170

120171
describe("that transforms requests automatically", () => {

0 commit comments

Comments
 (0)