nodejs / node

Node.js JavaScript runtime ✨🐢🚀✨
https://nodejs.org
Other
105.87k stars 28.75k forks source link

Subdomains on localhost return ENOTFOUND on some OS. #50871

Open spthiel opened 8 months ago

spthiel commented 8 months ago

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?

  1. 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

spthiel commented 8 months ago

Workaround for this for the time being:

fetch("http://127.0.0.1/", {
    headers: {
        host: "test.localhost"
    }
});
feychenie commented 4 months ago

@marco-ippolito there is the same issue (and workaround) on mac-os

cristianoliveira commented 3 months ago

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 :)

spthiel commented 2 months ago

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"
    }
});
ozgeneral commented 2 months ago

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'
}
ozgeneral commented 2 months ago

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

ozgeneral commented 2 months ago

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
$ 
ozgeneral commented 2 months ago

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
$ 
ozgeneral commented 2 months ago

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.