diff --git a/src/_utils.ts b/src/_utils.ts index 5c4ea97..02bada3 100644 --- a/src/_utils.ts +++ b/src/_utils.ts @@ -80,6 +80,16 @@ export function setupOutgoing( } } + // Derive `host` from `hostname` (+ port) when the target was provided as a + // plain object without `host`. IPv6 literals must be bracketed in URI host + // syntax (RFC 3986 3.2.2), otherwise the `changeOrigin` branch below would + // produce a malformed `Host` header like `undefined:`. + if (outgoing.host === undefined && typeof outgoing.hostname === "string") { + const isIPv6Literal = outgoing.hostname.includes(":") && !outgoing.hostname.startsWith("["); + const bracketedHost = isIPv6Literal ? `[${outgoing.hostname}]` : outgoing.hostname; + outgoing.host = outgoing.port ? `${bracketedHost}:${outgoing.port}` : bracketedHost; + } + outgoing.method = options.method || req.method; outgoing.headers = { ...req.headers }; diff --git a/test/_utils.test.ts b/test/_utils.test.ts index 9e1026f..0bd43ca 100644 --- a/test/_utils.test.ts +++ b/test/_utils.test.ts @@ -507,6 +507,40 @@ describe("lib/http-proxy/common.js", () => { ); expect(outgoing.headers!.host).to.eql("mycouch.com:6984"); }); + + it("should derive host from hostname when target is a plain object without host", () => { + const outgoing = createOutgoing(); + common.setupOutgoing( + outgoing, + { + target: { + protocol: "http:", + hostname: "example.com", + port: 8080, + }, + changeOrigin: true, + }, + stubIncomingMessage({ url: "/" }), + ); + expect(outgoing.headers!.host).to.eql("example.com:8080"); + }); + + it("should bracket IPv6 hostname when target is a plain object without host (RFC 3986 ยง3.2.2)", () => { + const outgoing = createOutgoing(); + common.setupOutgoing( + outgoing, + { + target: { + protocol: "http:", + hostname: "::1", + port: 8080, + }, + changeOrigin: true, + }, + stubIncomingMessage({ url: "/" }), + ); + expect(outgoing.headers!.host).to.eql("[::1]:8080"); + }); }); it("should pass through https client parameters", () => {