Subdomains on localhost return ENOTFOUND on some OS.
Version
Tested on v21.2.0, v20.9.0, v18.18.2
Platform
Microsoft Windows NT 10.0.19045.0 x64
Subsystem
No response
What steps will reproduce the bug?
- run
fetch("http://test.localhost/")
How often does it reproduce? Is there a required condition?
Happens on windows every time
What is the expected behavior? Why is that the expected behavior?
Expected behaviour is ECONNREFUSED or content of what the webpage contains equally to how a node on Linux, cURL or a browser would handle it
What do you see instead?
Uncaught [TypeError: fetch failed] {
cause: Error: getaddrinfo ENOTFOUND test.localhost
at GetAddrInfoReqWrap.onlookupall [as complete] (node:dns:118:26)
at GetAddrInfoReqWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
errno: -3008,
code: 'ENOTFOUND',
syscall: 'getaddrinfo',
hostname: 'test.localhost'
}
}
Additional information
Recreated locally in a VM but also on an actual Windows device
Workaround for this for the time being:
fetch("http://127.0.0.1/", {
headers: {
host: "test.localhost"
}
});
@marco-ippolito there is the same issue (and workaround) on mac-os
For those suffering from this issue. I'd say a more future proof solution is to add your "subdomains" in /etc/hosts (I'm not sure where it is in windows)
Add this to hosts
127.0.0.1 test.localhost
That should do the trick :)
With Node 18.18+ the workaround I posted is no longer possible (because node fetch disallowed setting the host header).
Instead use npm i undici
import {request} from "undici";
request("http://127.0.0.1/", {
headers: {
host: "test.localhost"
}
});
Also happens on mac, if not connected to internet. Steps to reproduce:
1- listen to port 3000 on localhost:
const http = require('http');
http.createServer((request, res) => {
res.write('Working!');
res.end();
}).listen(3000);
2- disconnect from wifi (works if connected) 3- try below:
// works:
await axios.get("http://localhost:3000")
// fails: Uncaught AxiosError: getaddrinfo ENOTFOUND test.localhost
await axios.get("http://test.localhost:3000")
info:
> process.versions
{
node: '20.3.1',
acorn: '8.8.2',
ada: '2.5.0',
ares: '1.19.1',
base64: '0.5.0',
brotli: '1.0.9',
cjs_module_lexer: '1.2.2',
cldr: '43.1',
icu: '73.2',
llhttp: '8.1.1',
modules: '115',
napi: '9',
nghttp2: '1.54.0',
openssl: '3.1.1',
simdutf: '3.2.12',
tz: '2023c',
undici: '5.22.1',
unicode: '15.0',
uv: '1.45.0',
uvwasi: '0.0.18',
v8: '11.3.244.8-node.9',
zlib: '1.2.11'
}
I suggest removing windows from bug title, as this seems to be non-OS specific issue.
one workaround could be to add subdomain.localhost to hosts in OS, but this seems like a bug to me, given curl and browsers are able to route localhost subdomains
Interesting observation, it seems like OS can't resolve the dns with subdomains if not connected to internet:
$ # connected to internet
$ dscacheutil -q host -a name localhost
name: localhost
ipv6_address: ::1
name: localhost
ip_address: 127.0.0.1
$ dscacheutil -q host -a name test.localhost
name: test.localhost
ipv6_address: ::1
name: test.localhost
ip_address: 127.0.0.1
$ # not connected to internet
$ dscacheutil -q host -a name localhost
name: localhost
ipv6_address: ::1
name: localhost
ip_address: 127.0.0.1
$ dscacheutil -q host -a name test.localhost
$
Interesting observation, it seems like OS can't resolve the dns with subdomains if not connected to internet:
$ # connected to internet
$ dscacheutil -q host -a name localhost
name: localhost
ipv6_address: ::1
name: localhost
ip_address: 127.0.0.1
$ dscacheutil -q host -a name test.localhost
name: test.localhost
ipv6_address: ::1
name: test.localhost
ip_address: 127.0.0.1
$ # not connected to internet
$ dscacheutil -q host -a name localhost
name: localhost
ipv6_address: ::1
name: localhost
ip_address: 127.0.0.1
$ dscacheutil -q host -a name test.localhost
$
here is a somewhat hacky fix, I think this should be handled in both OS and nodejs level, nevertheless:
var axios = require("axios")
var http_adapter = require('axios/lib/adapters/http')
var settle = require('axios/lib/core/settle')
// also works
await axios.get("http://testing.localhost:3000", {
adapter: (config) => {
const regex = /(?<prefix>^https?:\/\/)(?<subdomain>.*\.)(?<suffix>localhost.*)/gi
const subdomain_matches = Array.from(config.url.matchAll(regex))
if (subdomain_matches.length > 0) {
config.headers["Host"] = config.url
config.url = `${subdomain_matches[0].groups.prefix}${subdomain_matches[0].groups.suffix}`
}
return new Promise((resolve, reject) => {
http_adapter(config).then(response => {settle(resolve, reject, response)}).catch(reject)
})
}
})
although there is a solution with adapter, I suggest adding the localhost juggling inside nodejs package with proper testing so it can handle all users' request more robust out of box.
Interesting observation, it seems like OS can't resolve the dns with subdomains if not connected to internet:
$ # connected to internet $ dscacheutil -q host -a name localhost name: localhost ipv6_address: ::1 name: localhost ip_address: 127.0.0.1 $ dscacheutil -q host -a name test.localhost name: test.localhost ipv6_address: ::1 name: test.localhost ip_address: 127.0.0.1 $ # not connected to internet $ dscacheutil -q host -a name localhost name: localhost ipv6_address: ::1 name: localhost ip_address: 127.0.0.1 $ dscacheutil -q host -a name test.localhost $
This issue persists on my mac (m1 running Sonoma 14.6.1) regardless of an internet connection. Running the same commands, dscacheutil is able to resolve the DNS with subdomains with or without an internet connection. The issue also happens with Python using requests but cURL and Postman always work.
An alternative is to rely on localtest.me for routing to subdomains of localhost but this will not work offline + you're now relying on an external system.