Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: http: replace url.parse with WHATWG URL parser #20288

Closed
wants to merge 3 commits into from
Closed
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
12 changes: 12 additions & 0 deletions doc/api/deprecations.md
Original file line number Diff line number Diff line change
Expand Up @@ -994,6 +994,16 @@ because it also made sense to interpret the value as the number of bytes
read by the engine, but is inconsistent with other streams in Node.js that
expose values under these names.

<a id="DEP00XX"></a>
### DEP00XX: http.request() and http.get() URL parsing

Type: Runtime

Some previously supported (but strictly invalid) URLs were accepted through the
[`http.request()`][] and related APIs. This behavior is now deprecated. It is
recommended that one passes a [`URL`][] object to the API as the first argument
which ensures URL validity.
Copy link
Member

Choose a reason for hiding this comment

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

hmmm... the wording here I think can be improved.

### DEP00XX: http.request() and http.get() support for invalid URLs

Some previously supported (but strictly invalid) URLs were accepted through the
[`http.request()`][] and [`http.get()`][] APIs because those were accepted by the
legacy `url.parse()` API. The `http.request()` and request APIs now use the WHATWG
URL parser that requires strictly valid URLs. Passing an invalid URL is deprecated
and support will be removed in the future.

I specifically don't want to get in to recommending that users pass the URL object directly into the api as the URL spec itself recommends against that. Let's keep encouraging passing in the URLs as strings and put more attention on the underlying change that is prompting the deprecation.


[`--pending-deprecation`]: cli.html#cli_pending_deprecation
[`Buffer.allocUnsafeSlow(size)`]: buffer.html#buffer_class_method_buffer_allocunsafeslow_size
[`Buffer.from(array)`]: buffer.html#buffer_class_method_buffer_from_array
Expand All @@ -1009,6 +1019,7 @@ expose values under these names.
[`Server.getConnections()`]: net.html#net_server_getconnections_callback
[`Server.listen({fd: <number>})`]: net.html#net_server_listen_handle_backlog_callback
[`SlowBuffer`]: buffer.html#buffer_class_slowbuffer
[`URL`]: url.html#url_class_url
[`asyncResource.runInAsyncScope()`]: async_hooks.html#async_hooks_asyncresource_runinasyncscope_fn_thisarg_args
[`child_process`]: child_process.html
[`console.error()`]: console.html#console_console_error_data_args
Expand All @@ -1035,6 +1046,7 @@ expose values under these names.
[`fs.read()`]: fs.html#fs_fs_read_fd_buffer_offset_length_position_callback
[`fs.readSync()`]: fs.html#fs_fs_readsync_fd_buffer_offset_length_position
[`fs.stat()`]: fs.html#fs_fs_stat_path_callback
[`http.request()`]: http.html#http_http_request_options_callback
[`os.networkInterfaces`]: os.html#os_os_networkinterfaces
[`os.tmpdir()`]: os.html#os_os_tmpdir
[`process.env`]: process.html#process_process_env
Expand Down
26 changes: 22 additions & 4 deletions lib/_http_client.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const { OutgoingMessage } = require('_http_outgoing');
const Agent = require('_http_agent');
const { Buffer } = require('buffer');
const { defaultTriggerAsyncIdScope } = require('internal/async_hooks');
const { urlToOptions, searchParamsSymbol } = require('internal/url');
const { urlToOptions, searchParamsSymbol, URL } = require('internal/url');
const { outHeadersKey, ondrain } = require('internal/http');
const {
ERR_HTTP_HEADERS_SENT,
Expand All @@ -51,6 +51,8 @@ const { validateTimerDuration } = require('internal/timers');

const INVALID_PATH_REGEX = /[^\u0021-\u00ff]/;

let urlWarningEmitted = false;

function validateHost(host, name) {
if (host !== null && host !== undefined && typeof host !== 'string') {
throw new ERR_INVALID_ARG_TYPE(`options.${name}`,
Expand All @@ -64,9 +66,25 @@ function ClientRequest(options, cb) {
OutgoingMessage.call(this);

if (typeof options === 'string') {
options = url.parse(options);
if (!options.hostname) {
throw new ERR_INVALID_DOMAIN_NAME();
const urlStr = options;
try {
options = urlToOptions(new URL(urlStr));
} catch (err) {
try {
options = url.parse(urlStr);
if (!options.hostname) {
throw new ERR_INVALID_DOMAIN_NAME();
}
if (!urlWarningEmitted) {
urlWarningEmitted = true;
process.emitWarning(
`The provided URL ${urlStr} is not a valid URL, and is supported ` +
'in the http module solely for compatibility.',
'DeprecationWarning', 'DEP00XX');
}
} catch (urlParseErr) {
throw err;
}
}
} else if (options && options[searchParamsSymbol] &&
options[searchParamsSymbol][searchParamsSymbol]) {
Expand Down
1 change: 1 addition & 0 deletions lib/internal/safe_globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const makeSafe = (unsafe, safe) => {
return safe;
};

exports.makeSafe = makeSafe;
exports.SafeMap = makeSafe(Map, class SafeMap extends Map {});
exports.SafeSet = makeSafe(Set, class SafeSet extends Set {});
exports.SafePromise = makeSafe(Promise, class SafePromise extends Promise {});
19 changes: 18 additions & 1 deletion lib/internal/url.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const {
isHexTable
} = require('internal/querystring');

const { makeSafe } = require('internal/safe_globals');

const { getConstructorOf, removeColors } = require('internal/util');
const {
ERR_ARG_NOT_ITERABLE,
Expand Down Expand Up @@ -1308,11 +1310,26 @@ function domainToUnicode(domain) {
return _domainToUnicode(`${domain}`);
}

const SafeURL = makeSafe(URL, class SafeURL extends URL {
// Wraps an existing URL in SafeURL's prototype object so we can be sure that
// the getters/setters have not been tampered with. No such guarantees for
// the url.searchParams object however. All modifications reflect on the
// original URL object.
static fromURL(url) {
return {
__proto__: SafeURL.prototype,
[context]: url[context],
[searchParams]: url[searchParams]
};
}
});

// Utility function that converts a URL object into an ordinary
// options object as expected by the http.request and https.request
// APIs.
function urlToOptions(url) {
var options = {
url = SafeURL.fromURL(url);
const options = {
protocol: url.protocol,
hostname: url.hostname.startsWith('[') ?
url.hostname.slice(1, -1) :
Expand Down
19 changes: 19 additions & 0 deletions test/parallel/test-whatwg-url-properties.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,25 @@ assert.strictEqual(url.searchParams, oldParams);
assert.strictEqual(hostname, '::1');
}

// Ensure urlToOptions works on URL with prototype that has been tampered with.
{
const url = new URL('http://user:[email protected]:21/aaa/zzz?l=24#test');
Object.setPrototypeOf(url, Object.create(null));
const opts = urlToOptions(url);
assert.strictEqual(opts instanceof URL, false);
assert.strictEqual(opts.protocol, 'http:');
assert.strictEqual(opts.auth, 'user:pass');
assert.strictEqual(opts.hostname, 'foo.bar.com');
assert.strictEqual(opts.port, 21);
assert.strictEqual(opts.path, '/aaa/zzz?l=24');
assert.strictEqual(opts.pathname, '/aaa/zzz');
assert.strictEqual(opts.search, '?l=24');
assert.strictEqual(opts.hash, '#test');

const { hostname } = urlToOptions(new URL('http://[::1]:21'));
assert.strictEqual(hostname, '::1');
}

// Test special origins
[
{ expected: 'https://whatwg.org',
Expand Down