踩到一個 Node.js 小問題,用一小段程式重現。

簡單的 HTTP 客戶端測試,一人分飾兩角,預設為伺服器模式,利用內建 http 模組 Bind 127.0.0.1 9527 Port 跑一個簡單的伺服器(永遠回傳 Hello World);若帶入參數 client,則使用 fetch 呼叫 http://localhost:9527 驗證結果:
(學到冷知識:Node 18+ 內建 fetch API,若為早期版本依賴 node-fetch 套件)

const args = process.argv.slice(2);
const clientMode = args.length && args[0] === 'client';

if (clientMode) {
    console.log('client test');
    // 註:Node 18+ 內建 fetch API,之前版本需 npm install node-fetch
    const fetch = require('node-fetch');
    fetch('http://localhost:9527').then(res => res.text()).then(console.log);
}
else {
    console.log('server mode');
    const http = require('http');
    http.createServer((req, res) => {
        res.writeHead(200, { 'Content-Type': 'text/plain' });
        res.end('Hello World\n');
    }).listen(9527, '127.0.0.1');
}

狀況為程式在某些機器測試 OK,有些機器上 fetch() 動作會出現 FetchError: request to http://localhost:9527/ failed, reason: connect ECONNREFUSED ::1:9527 錯誤,但 curl http://localhost:9527 及瀏覽器檢視正常。

由程式碼與錯誤訊息中的 "::1" 可知跟 localhost 用 IPv6 或 IPv4 網址有關。從這篇Node.js 問題回報得到解答:

Node.js 17 版有個 Breaking Change - DNS.lookup 解析 localhost 結果,從 v17 起優先改回傳 IPv6;若伺服器繫結網址寫 127.0.0.1 但客戶端寫 localhost,就會出現 ECONNREFUSED 錯誤。參考
瀏覽器及 curl 不會出錯因為遇到 localhost 時,127.0.0.1 及 ::1 兩個網址都會試。參考

為了驗證這是 v16/v17 的差異,我找了一台有裝 NVM (Node 版本切換器)的 Linux 實測,證實同樣的程式用 v16 跑是好的,在 v17 才會出錯。

【參考資料】

知道原因,解法選項如下:

  1. bind() 或 listen() 的主機名稱改用 localhost,跟呼叫端統一
  2. 若伺服器端寫死 127.0.0.1 無法改,fetch 網址也改用 127.0.0.1
  3. 若伺服器端非 127.0.0.1 不 Bind,fetch 端也堅持要用 localhost (人生好難)... 可加入以下程式指定 IPv4 優先
     const dns = require('dns');
     dns.setDefaultResultOrder('ipv4first');    
     fetch('http://localhost:9527').then(res => res.text()).then(console.log);
    
    一樣可以解決問題

Issue of localhost resolving behavior breaking change in Node.js 17.


Comments

Be the first to post a comment

Post a comment