Files
node/test/client-proxy/test-http-proxy-request-absolute-path-authority-match.mjs
Joyee Cheung fd3419178e http: document and validate options.path when it's in absolute-form
When `options.path` passed to `http.request()` contains an absolute
URL, `http.request` has been sending it directly as the request target
in the HTTP 1.1 message. If the receiving server is a proxy,
the proxy server typically forwards the request to the destination
specified in the request target and ignores the Host header.
This means eventually the request can be forwarded to a destination
that is not consistent with `options.host`, depending on how the
receiving server behaves. Mimatched Host header and request target
also violates RFC 9112 Section 3.2, which we have been entirely
leaving to the users to verify.

This patch documents this behavior and warns that the user needs
to ensure the `path`, `option` and `headers` conform to the RFC.
If the receiving server is known to be a proxy server because
the request is routed by Node.js' built-in HTTP proxy support,
we now do a best-effort check to verify that the authority in
`options.path` (if absolute), Host headers and `options.host`
agree at request construction time. Node.js will give up on the
require target rewriting and throw an error when they don't match
at request construction.

Signed-off-by: Joyee Cheung <joyeec9h3@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/64108
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Tim Perry <pimterry@gmail.com>
Reviewed-By: Ulises Gascón <ulisesgascongonzalez@gmail.com>
2026-07-01 10:44:52 +00:00

71 lines
2.1 KiB
JavaScript

// This tests that proxied HTTP requests succeed end-to-end when the
// absolute-form request-target matches the request authority derived from options.
import * as common from '../common/index.mjs';
import assert from 'node:assert';
import http from 'node:http';
import { once } from 'events';
import { createProxyServer } from '../common/proxy-server.js';
const server = http.createServer(common.mustCall((req, res) => {
res.end('Hello world');
}, 6));
server.on('error', common.mustNotCall());
server.listen(0);
await once(server, 'listening');
const { proxy, logs } = createProxyServer();
proxy.listen(0);
await once(proxy, 'listening');
const port = server.address().port;
const serverHost = `localhost:${port}`;
const requestUrl = `http://${serverHost}/test`;
const agent = new http.Agent({
proxyEnv: {
HTTP_PROXY: `http://localhost:${proxy.address().port}`,
},
});
async function roundTrip(options) {
const req = http.request({ agent, ...options });
req.end();
const [res] = await once(req, 'response');
res.setEncoding('utf8');
let body = '';
for await (const chunk of res) body += chunk;
assert.strictEqual(body, 'Hello world');
}
const baseAbsolute = { host: 'localhost', port, path: requestUrl };
const options = [
// No user-supplied headers.
baseAbsolute,
// Object form with an explicit Host that matches.
{ ...baseAbsolute, headers: { Host: serverHost } },
// Flat array form.
{ ...baseAbsolute, headers: ['Host', serverHost] },
// Array-of-pairs form.
{ ...baseAbsolute, headers: [['Host', serverHost]] },
// Contains defaultPort that matches options.port.
{ host: 'localhost', port, defaultPort: port, path: '/test' },
// Stringifiable non-string path object.
{ host: 'localhost', port, path: { toString() { return '/test'; } } },
];
for (const opts of options) {
await roundTrip(opts);
// Check what the proxy server received.
const log = logs.pop();
assert.strictEqual(logs.length, 0);
assert.strictEqual(log.method, 'GET');
assert.strictEqual(log.url, requestUrl);
assert.strictEqual(log.headers.host, serverHost);
}
server.close();
proxy.close();
agent.destroy();