host vs hostname inconsistency
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); // true
In 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); // true
Current 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 host
s 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 }, () => {});