Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion lib/http-proxy/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ export function setupOutgoing(

outgoing.method = options.method || req.method;
outgoing.headers = { ...req.headers };
if (req.headers?.[":authority"]) {
outgoing.headers.host = req.headers[":authority"];
}

if (options.headers) {
outgoing.headers = { ...outgoing.headers, ...options.headers };
Expand Down Expand Up @@ -189,7 +192,8 @@ export function getPort(
req: Request,
// Return the port number, as a string.
): string {
const res = req.headers.host ? req.headers.host.match(/:(\d+)/) : "";
const hostHeader = (req.headers[":authority"] as string | undefined) || req.headers.host;
const res = hostHeader ? hostHeader.match(/:(\d+)/) : "";
return res ? res[1] : hasEncryptedConnection(req) ? "443" : "80";
}

Expand Down
2 changes: 1 addition & 1 deletion lib/http-proxy/passes/web-incoming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export function XHeaders(req: Request, _res: Response, options: ServerOptions) {
(req.headers["x-forwarded-" + header] || "") + (req.headers["x-forwarded-" + header] ? "," : "") + values[header];
}

req.headers["x-forwarded-host"] = req.headers["x-forwarded-host"] || req.headers["host"] || "";
req.headers["x-forwarded-host"] = req.headers["x-forwarded-host"] || req.headers[":authority"] || req.headers["host"] || "";
}

// Does the actual proxying. If `forward` is enabled fires up
Expand Down
4 changes: 2 additions & 2 deletions lib/http-proxy/passes/web-outgoing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export function setRedirectHostRewrite(
if (options.hostRewrite) {
u.host = options.hostRewrite;
} else if (options.autoRewrite) {
u.host = req.headers["host"] ?? "";
u.host = (req.headers[":authority"] as string | undefined) ?? req.headers["host"] ?? "";
}
if (options.protocolRewrite) {
u.protocol = options.protocolRewrite;
Expand Down Expand Up @@ -143,7 +143,7 @@ export function writeHeaders(

for (const key0 in proxyRes.headers) {
let key = key0;
if (_req.httpVersionMajor > 1 && (key === "connection" || key === "keep-alive")) {
if (_req.httpVersionMajor > 1 && (key === "connection" || key === "keep-alive")) {
// don't send connection header to http2 client
continue;
}
Expand Down
19 changes: 19 additions & 0 deletions lib/test/lib/http-proxy-passes-web-incoming.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,32 @@ describe("#XHeaders", () => {
host: "192.168.1.2:8080",
} as Record<string, string>,
};
const stubHttp2Request = {
connection: {
remoteAddress: "192.168.1.2",
remotePort: "8080",
},
headers: {
':authority': "192.168.1.2:8080",
} as Record<string, string>,
};

it("set the correct x-forwarded-* headers", () => {
// @ts-ignore
XHeaders(stubRequest, {}, { xfwd: true });
expect(stubRequest.headers["x-forwarded-for"]).toEqual("192.168.1.2");
expect(stubRequest.headers["x-forwarded-port"]).toEqual("8080");
expect(stubRequest.headers["x-forwarded-proto"]).toEqual("http");
expect(stubRequest.headers["x-forwarded-host"]).toEqual("192.168.1.2:8080");
});

it("set the correct x-forwarded-* headers for http2", () => {
// @ts-ignore
XHeaders(stubHttp2Request, {}, { xfwd: true });
expect(stubHttp2Request.headers["x-forwarded-for"]).toEqual("192.168.1.2");
expect(stubHttp2Request.headers["x-forwarded-port"]).toEqual("8080");
expect(stubHttp2Request.headers["x-forwarded-proto"]).toEqual("http");
expect(stubHttp2Request.headers["x-forwarded-host"]).toEqual("192.168.1.2:8080");
});
});

Expand Down
10 changes: 10 additions & 0 deletions lib/test/lib/http-proxy-passes-web-outgoing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,16 @@ describe.each( ["http://backend.com", url.parse("http://backend.com")])("#setRed
"http://ext-auto.com/",
);
});

it("on " + code + " (http2)", () => {
state.proxyRes.statusCode = code;
state.req.headers[":authority"] = state.req.headers.host;
delete state.req.headers.host;
setRedirectHostRewrite(state.req, {}, state.proxyRes, state.options);
expect(state.proxyRes.headers.location).toEqual(
"http://ext-auto.com/",
);
});
});

it("not on 200", () => {
Expand Down
122 changes: 122 additions & 0 deletions lib/test/lib/http2-proxy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import * as httpProxy from "../..";
import * as http from "node:http";
import * as http2 from "node:http2";
import getPort from "../get-port";
import { join } from "node:path";
import { readFileSync } from "node:fs";
import { describe, it, expect } from 'vitest';

const ports: { [port: string]: number } = {};
let portIndex = -1;
const gen = {} as { port: number };
Object.defineProperty(gen, "port", {
get: function get() {
portIndex++;
return ports[portIndex];
},
});

describe("HTTP2 to HTTP", () => {
it("creates some ports", async () => {
for (let n = 0; n < 50; n++) {
ports[n] = await getPort();
}
});

it("should proxy the request, then send back the response", () => new Promise<void>(done => {
const ports = { source: gen.port, proxy: gen.port };
const source = http
.createServer((req, res) => {
expect(req.method).toEqual("GET");
expect(req.headers.host?.split(":")[1]).toEqual(`${ports.proxy}`);
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Hello from " + ports.source);
})
.listen(ports.source);

const proxy = httpProxy
.createProxyServer({
target: "http://127.0.0.1:" + ports.source,
ssl: {
key: readFileSync(
join(__dirname, "..", "fixtures", "agent2-key.pem"),
),
cert: readFileSync(
join(__dirname, "..", "fixtures", "agent2-cert.pem"),
),
ciphers: "AES128-GCM-SHA256",
},
})
.listen(ports.proxy);

const client = http2.connect(`https://localhost:${ports.proxy}`)
const req = client.request({ ':path': '/' });
req.on('response', (headers, _flags) => {
expect(headers[':status']).toEqual(200);
req.setEncoding('utf8');
req.on('data', (chunk) => {
expect(chunk.toString()).toEqual("Hello from " + ports.source);
});
req.on('end', () => {
source.close();
proxy.close();
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The HTTP2 client connection should be closed to prevent resource leaks. Add client.close() before calling done(). For example:

req.on('end', () => {
  source.close();
  proxy.close();
  client.close();
  done();
});
Suggested change
proxy.close();
proxy.close();
client.close();

Copilot uses AI. Check for mistakes.
done();
});
});
req.end();
}));
});

describe("HTTP2 to HTTP using own server", () => {
it("should proxy the request, then send back the response", () => new Promise<void>(done => {
const ports = { source: gen.port, proxy: gen.port };
const source = http
.createServer((req, res) => {
expect(req.method).toEqual("GET");
expect(req.headers.host?.split(":")[1]).toEqual(`${ports.proxy}`);
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Hello from " + ports.source);
})
.listen(ports.source);

const proxy = httpProxy.createServer({
agent: new http.Agent({ maxSockets: 2 }),
});

const ownServer = http2
.createSecureServer(
{
key: readFileSync(
join(__dirname, "..", "fixtures", "agent2-key.pem"),
),
cert: readFileSync(
join(__dirname, "..", "fixtures", "agent2-cert.pem"),
),
ciphers: "AES128-GCM-SHA256",
},
(req, res) => {
// @ts-expect-error -- ignore type incompatibility
proxy.web(req, res, {
target: "http://127.0.0.1:" + ports.source,
});
},
)
.listen(ports.proxy);

const client = http2.connect(`https://localhost:${ports.proxy}`)
const req = client.request({ ':path': '/' });
req.on('response', (headers, _flags) => {
expect(headers[':status']).toEqual(200);
req.setEncoding('utf8');
req.on('data', (chunk) => {
expect(chunk.toString()).toEqual("Hello from " + ports.source);
});
req.on('end', () => {
source.close();
ownServer.close();
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The HTTP2 client connection should be closed to prevent resource leaks. Add client.close() before calling done(). For example:

req.on('end', () => {
  source.close();
  ownServer.close();
  client.close();
  done();
});
Suggested change
ownServer.close();
ownServer.close();
client.close();

Copilot uses AI. Check for mistakes.
done();
});
});
req.end();
}));
});