Skip to content

Commit 8c8cdba

Browse files
committed
Use strictSingleValueFields:false to support proxying semi-valid HTTP/2
1 parent 2516d1d commit 8c8cdba

2 files changed

Lines changed: 47 additions & 0 deletions

File tree

src/rules/requests/request-step-impls.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,12 @@ export class PassThroughStepImpl extends PassThroughStep {
790790
? rawHeadersToObjectPreservingCase(rawHeaders)
791791
: flattenPairedRawHeaders(rawHeaders) as any,
792792
setDefaultHeaders: shouldTryH2Upstream, // For now, we need this for unexpected H2->H1 header fallback
793+
...({
794+
// Disable strict HTTP/2 single-value field validation, extracted here
795+
// due to types. Needed for compatibility with weird servers, Node 25.7+
796+
strictSingleValueFields: false,
797+
}),
798+
793799
lookup: getDnsLookupFunction(this.lookupOptions) as typeof dns.lookup,
794800
// ^ Cast required to handle __promisify__ type hack in the official Node types
795801
agent,

test/integration/http2.spec.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,47 @@ nodeOnly(() => {
604604
await cleanup(proxiedClient, client);
605605
});
606606

607+
it("can pass through HTTP/2 with duplicate single-value request headers", async function () {
608+
if (!nodeSatisfies(">=25.7.0")) this.skip();
609+
610+
await server.forGet(`https://localhost:${targetPort}/`)
611+
.thenPassThrough({ ignoreHostHttpsErrors: ['localhost'] });
612+
613+
const client = http2.connect(server.url);
614+
615+
const req = client.request({
616+
':method': 'CONNECT',
617+
':authority': `localhost:${targetPort}`
618+
});
619+
620+
// Initial response, the proxy has set up our tunnel:
621+
const responseHeaders = await getHttp2Response(req);
622+
expect(responseHeaders[':status']).to.equal(200);
623+
624+
// We can now read/write to req as a raw TCP socket to our target server
625+
const proxiedClient = http2.connect(`https://localhost:${targetPort}`, {
626+
// Tunnel this request through the proxy stream
627+
createConnection: () => tls.connect({
628+
socket: req as any,
629+
ALPNProtocols: ['h2']
630+
}),
631+
// Allow the test client to send duplicate single-value headers:
632+
strictSingleValueFields: false
633+
} as any);
634+
635+
const proxiedRequest = proxiedClient.request({
636+
':path': '/',
637+
'user-agent': ['agent-1', 'agent-2'] as any
638+
});
639+
const proxiedResponse = await getHttp2Response(proxiedRequest);
640+
expect(proxiedResponse[':status']).to.equal(200);
641+
642+
const responseBody = await getHttp2Body(proxiedRequest);
643+
expect(responseBody.toString('utf8')).to.equal("Real HTTP/2 response");
644+
645+
await cleanup(proxiedClient, client);
646+
});
647+
607648
});
608649

609650
});

0 commit comments

Comments
 (0)