// This tests that proxied HTTP requests fail validation before sending any data.
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 target = http.createServer(common.mustNotCall());
target.listen(0);
await once(target, 'listening');
target.on('error', common.mustNotCall());
const { proxy, logs } = createProxyServer();
proxy.listen(0);
await once(proxy, 'listening');
const agent = new http.Agent({
proxyEnv: {
HTTP_PROXY: `http://localhost:${proxy.address().port}`,
},
});
const port = target.address().port;
const base = { host: 'localhost', port, path: '/test', agent };
function throwsWith(message, cases) {
for (const { name, options } of cases) {
assert.throws(() => {
http.request(options, common.mustNotCall());
}, { code: 'ERR_INVALID_ARG_VALUE', message }, name);
}
}
// Path authority or Host header disagrees with options.host:port.
throwsWith(/must match the request authority/, [
{ name: 'absolute path with different host',
options: { ...base, path: 'http://example.test/test' } },
{ name: 'object Host header with different host',
options: { ...base, headers: { Host: 'bad.example' } } },
{ name: 'object Host header with different port',
options: { ...base, headers: { Host: 'localhost:1' } } },
{ name: 'array Host header with different host',
options: { ...base, headers: ['Host', 'bad.example'] } },
{ name: 'array-of-pairs Host header with different host',
options: { ...base, headers: [['Host', 'bad.example']] } },
{ name: 'array Host header omitting port for non-default port',
options: { ...base, headers: ['Host', 'localhost'] } },
{ name: 'absolute path omitting port for non-default port',
options: { ...base, path: 'http://localhost/test' } },
{ name: 'object Host header omitting port when defaultPort suppresses it',
options: { ...base, defaultPort: port, headers: { Host: 'localhost' } } },
]);
// Absolute-form path uses a non-http scheme.
throwsWith(/must use http: scheme/, [
{ name: 'absolute path with https: scheme',
options: { ...base, path: `https://localhost:${port}/test` } },
]);
// Absolute-form path contains userinfo.
throwsWith(/must not contain userinfo/, [
{ name: 'absolute path with userinfo',
options: { ...base, path: `http://user:pass@localhost:${port}/test` } },
]);
// Origin-form path must start with /.
throwsWith(/must be in absolute-form or start with \//, [
{ name: 'path without leading slash but includes @',
options: { ...base, path: '@other.example/test' } },
]);
// Host header value is not a bare authority (contains userinfo, path, query, or fragment).
throwsWith(/must match the request authority/, [
{ name: 'Host with userinfo',
options: { ...base, headers: { Host: `user@localhost:${port}` } } },
{ name: 'Host with path',
options: { ...base, headers: { Host: `localhost:${port}/path` } } },
{ name: 'Host with query',
options: { ...base, headers: { Host: `localhost:${port}?x` } } },
{ name: 'Host with fragment',
options: { ...base, headers: { Host: `localhost:${port}#x` } } },
]);
// Multiple Host headers (invalid per RFC 9110 Section 5.3).
throwsWith(/must not contain duplicate Host headers/, [
{ name: 'flat array with duplicate Host',
options: { ...base, headers: ['Host', `localhost:${port}`, 'Host', 'bad.example'] } },
{ name: 'array-of-pairs with duplicate Host',
options: { ...base, headers: [['Host', `localhost:${port}`], ['Host', 'bad.example']] } },
{ name: 'case-insensitive duplicate Host',
options: { ...base, headers: ['host', `localhost:${port}`, 'HOST', 'bad.example'] } },
]);
// Non-array entry in array-of-pairs form.
throwsWith(/must be an array when headers is passed as an array of pairs/, [
{ name: 'object with numeric keys smuggling a Host',
options: { ...base, headers: [['Host', `localhost:${port}`], { 0: 'Host', 1: 'bad.example' }] } },
]);
// Invalid port.
for (const { name, badPort } of [
{ name: 'port >= 65536', badPort: 99999 },
{ name: 'port === 65536', badPort: 65536 },
]) {
assert.throws(() => {
http.request({ ...base, port: badPort }, common.mustNotCall());
}, { code: 'ERR_SOCKET_BAD_PORT' }, name);
}
assert.throws(() => {
http.request({ ...base, method: 'GET', path: '*' }, common.mustNotCall());
}, { code: 'ERR_INVALID_ARG_VALUE', message: /must be in absolute-form or start with \// });
assert.deepStrictEqual(logs, []);
target.close();
proxy.close();
agent.destroy();