-
-
Notifications
You must be signed in to change notification settings - Fork 34.3k
Description
Version: Node 8 and others
Problem
The semantics of the host option for server.listen() are incompatible with the conventions of other APIs, both in Node itself and in browsers, etc. This is a footgun that could be fixed by making the options for .listen() be consistent with other APIs.
Context
Most systems I interact with distinguish between a hostname and a host, where the latter includes a port number and the former does not.
In general, Node behaves this way, too (see os.hostname(), WHATWG URL, and the legacy url module APIs). However, the option object passed to server.listen(option) is weird. It only understands host (not hostname), but a port number is disallowed. This is confusing and leads to very surprising results in some cases.
Prior art
In browsers
location.href = 'https://localhost:3000';
console.log(location.host !== location.hostname); // true
const whatwgParsed = new URL('https://localhost:3000');
console.log(whatwgParsed.host !== whatwgParsed.hostname); // trueIn Node.js
const url = require('url');
const target = 'https://localhost:3000';
const legacyParsed = url.parse(target);
console.log(legacyParsed.host !== legacyParsed.hostname); // true
const whatwgParsed = new url.URL(target);
console.log(whatwgParsed.host !== whatwgParsed.hostname); // trueCurrent behavior
The server.listen() API understands a host option, but it is incompatible with host from other APIs, including WHATWG URL and others. This is because .listen() refuses perfectly valid hosts such as localhost:3000. A reasonable expectation would be to try hostname instead, which is the more common name for the current behavior of the host option. Unfortunately, that doesn't work either, as .listen() does not understand hostname, and it will be silently ignored.
// Reports an error correctly, since the API lacks a default port, but the error message is somewhat vague
server.listen({ host : 'localhost' }, () => {});
Error: Invalid listen argument: { host: 'localhost' }
// Listens on 0.0.0.0 rather than localhost (works as documented, but very confusing and as a footgun is arguably unsafe)
server.listen({ hostname : 'localhost', port : 3000}, () => {});
// Reports an error even though the host is valid (works as documented, but confusing and incompatible with other specs and APIs)
server.listen({ host : 'localhost:3000' }, () => {});
Error: Invalid listen argument: { host: 'localhost:3000' }
// Succeeds, even though host usually takes precedence over separate hostname and port options and the host lacks a port (works as documented, but a bit strange)
server.listen({ host : 'localhost', port : 3000 }, () => {});Expected behavior
APIs that accept a host should respect their port number to avoid confusion and inconsistency. Having separate options for hostname and port is also awesome, but in that case it should be named hostname instead of host. I would argue that Node should only support hostname instead of host (leaving parsing hosts to userland) but that would be a breaking change, so I'm not sure how feasible that is. At the very least, I think a new hostname option could be introduced with the correct behavior, and then host could be extended to respect port numbers, and the fact that host continues to be in the API would purely be for backwards compatibility reasons.
// Should report an intuitive error. The host is perfectly valid, but the API lacks a default port.
server.listen({ host : 'localhost' }, () => {});
Error: No port specified
// Should listen on localhost, whereas it currently listens on 0.0.0.0 instead because hostname is not understood.
server.listen({ hostname : 'localhost', port : 3000}, () => {});
// If host needs to be supported, then this should listen on localhost and port 3000, whereas it currently throws an error.
server.listen({ host : 'localhost:3000' }, () => {});
// Should probably throw an error, whereas it currently does not.
server.listen({ host : 'localhost', port : 3000 }, () => {});