Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
http: simplify the approach
  • Loading branch information
efekrskl committed Sep 16, 2026
commit be15e6624e2ce9ac6bd0c40b136f69801c59968e
3 changes: 0 additions & 3 deletions doc/api/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -4113,9 +4113,6 @@ changes:
E.G. `'/index.html?page=12'`. An exception is thrown when the request path
contains illegal characters. Currently, only spaces are rejected but that
may change in the future. **Default:** `'/'`.
With `method` set to `'CONNECT'`, `path` must be an authority in the form
`host:port`, such as `'www.example.com:80'` or `'[2001:db8::1]:443'`.
Invalid values throw an `ERR_INVALID_ARG_VALUE` error.
The content in `path` is sent as the [request target][] in the HTTP 1.1 message.
When `path` is an absolute URL, this means the request target in the message in [absolute form][].
If the receiving server is a proxy, the server typically forwards the request to the
Expand Down
46 changes: 15 additions & 31 deletions lib/_http_client.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
ObjectAssign,
ObjectDefineProperty,
ObjectKeys,
ObjectPrototypeHasOwnProperty,

Check failure on line 32 in lib/_http_client.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Do not use prototype primordials in this file

Check failure on line 32 in lib/_http_client.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Do not use prototype primordials in this file
ObjectSetPrototypeOf,
ReflectApply,
String,
Expand Down Expand Up @@ -60,7 +61,7 @@
const Agent = require('_http_agent');
const { Buffer } = require('buffer');
const { defaultTriggerAsyncIdScope } = require('internal/async_hooks');
const { URL, URLParse, urlToHttpOptions, isURL } = require('internal/url');
const { URL, urlToHttpOptions, isURL } = require('internal/url');
const {
kOutHeaders,
kNeedDrain,
Expand Down Expand Up @@ -121,7 +122,6 @@
});

const INVALID_PATH_REGEX = /[^\u0021-\u00ff]/;
const CONNECT_PATH_REGEX = /^(\[[^\]]+\]|[^:]+):(\d+)$/;
const kError = Symbol('kError');
const kPath = Symbol('kPath');
const kAuthority = Symbol('kAuthority');
Expand Down Expand Up @@ -194,22 +194,6 @@
return parsed.host === canonicalHost;
}

function isValidConnectPath(path) {
const match = CONNECT_PATH_REGEX.exec(path);
if (match === null || +match[2] === 0) {
return false;
}

const url = URLParse(`http://${path}`);
return url !== null &&
url.hostname !== '' &&
url.username === '' &&
url.password === '' &&
url.pathname === '/' &&
url.search === '' &&
url.hash === '';
}

// https://datatracker.ietf.org/doc/html/rfc9112#section-3.2
// When the request target is in absolute-form, ensure it is consistent with
// the request authority: same scheme, no userinfo, and an authority
Expand Down Expand Up @@ -348,12 +332,15 @@
function ClientRequest(input, options, cb) {
OutgoingMessage.call(this);

let pathIsFromURL = false;
if (typeof input === 'string') {
const urlStr = input;
input = urlToHttpOptions(new URL(urlStr));
pathIsFromURL = true;
} else if (isURL(input)) {
// url.URL instance
input = urlToHttpOptions(input);
pathIsFromURL = true;
} else {
cb = options;
options = input;
Expand All @@ -364,6 +351,13 @@
cb = options;
options = input || kEmptyObject;
} else {
const hasPathOverride = pathIsFromURL &&
options != null &&
ObjectPrototypeHasOwnProperty(options, 'path');
if (hasPathOverride) {
pathIsFromURL = false;
}

options = ObjectAssign({ __proto__: null }, input, options);
}

Expand Down Expand Up @@ -484,19 +478,9 @@
this.joinDuplicateHeaders = options.joinDuplicateHeaders;

let path = options.path || '/';
if (method === 'CONNECT' && options.path != null) {
path = String(options.path);
if (path[0] === '/') {
path = path.slice(1) || '/';
}

if (!isValidConnectPath(path)) {
throw new ERR_INVALID_ARG_VALUE(
'options.path',
path,
'must be a valid host:port combo',
);
}
// Strip the leading slash added when the CONNECT target comes from a URL.
if (method === 'CONNECT' && pathIsFromURL && path[0] === '/') {
path = path.slice(1) || '/';
}

this[kPath] = path;
Expand Down
27 changes: 4 additions & 23 deletions test/parallel/test-http-request-connect-path.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,18 @@ const common = require('../common');
const assert = require('assert');
const http = require('http');

for (const path of [
'',
'example.com',
'example.com:0',
'example.com:65536',
'example.com:8080/example',
'evil.com:666/good.org:777',
'/example.com',
]) {
assert.throws(() => http.request({
method: 'CONNECT',
path,
}), {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
message: /^The property 'options\.path' must be a valid host:port combo\./,
});
}

{
const server = http.createServer(common.mustNotCall());

server.on('connect', common.mustCall((req, socket) => {
assert.strictEqual(req.url, 'example.com:80');
assert.strictEqual(req.url, 'example.com');
socket.end('HTTP/1.1 501 Not Implemented\r\n\r\n');
}));

server.listen(0, common.mustCall(() => {
const port = server.address().port;
const req = http.request(
new URL(`http://localhost:${port}/example.com:80`),
new URL(`http://localhost:${port}/example.com`),
{ method: 'CONNECT' },
);

Expand All @@ -52,7 +33,7 @@ for (const path of [
const server = http.createServer(common.mustNotCall());

server.on('connect', common.mustCall((req, socket) => {
assert.strictEqual(req.url, '[2001:db8::1]:111');
assert.strictEqual(req.url, '/example.com');
socket.end('HTTP/1.1 501 Not Implemented\r\n\r\n');
}));

Expand All @@ -61,7 +42,7 @@ for (const path of [
host: 'localhost',
port: server.address().port,
method: 'CONNECT',
path: '[2001:db8::1]:111',
path: '/example.com',
});

req.on('connect', common.mustCall((res, socket) => {
Expand Down
Loading