Repository navigation
npm cannot fetch from ANY local/loopback HTTP registry stub: no TCP connection is opened, request times out (FETCH_ERROR) — make-fetch-happen reproduces it standalone; node's own http clients work #10077
Description
Current Behavior
npm view / npm ping against a local HTTP registry stub (a plain http.createServer answering every request with 200) never opens a TCP connection to the stub and dies with FETCH_ERROR / request-timeout after the fetch timeout (~5s with --fetch-timeout=5000). With default flags the request just stalls (observed >30s). node's own clients against the same stub work instantly. The failure is identical on every box we tested — Linux and macOS.
Expected Behavior
npm should connect to and fetch from the local registry stub, as node's own http.request/fetch do.
Steps to Reproduce
Self-contained: start a stub in one node process, run npm view against it.
// stub.mjs — answers any request with a minimal packument
import http from "node:http";
let hits = 0, conns = 0;
http.createServer((req, res) => {
hits++;
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ _id: "left-pad", name: "left-pad", "dist-tags": { latest: "1.0.0" }, versions: { "1.0.0": { name: "left-pad", version: "1.0.0" } } }));
}).on("connection", () => conns++)
.listen(0, "[redacted:ip]", () => console.log(`stub http://[redacted:ip]:${stub.address().port} — watch hits/conns here`));npm view left-pad@^1.0.0 version --json --registry http://[redacted:ip]:<port> --fetch-retries=0 --fetch-timeout=5000
# -> ~5.2s, exit 1: npm error code FETCH_ERROR / "network timeout at: ..."
# stub-side: hits and TCP conns UNCHANGED — npm never connected
node -e 'fetch("http://[redacted:ip]:<port>/left-pad").then(r => console.log(r.status))'
# -> 200 in single-digit ms — node's own client is fine
npm ping --registry http://[redacted:ip]:<port> --fetch-retries=0 --fetch-timeout=5000
# -> same FETCH_ERROR, stub still never sees a socketLayer isolation (why we think it is make-fetch-happen, not npm wiring)
All of this in one session, npm 10.9.8 bundled tree:
- node
fetchandhttp.getagainst the stub: HTTP 200 in 3–25ms (stub sees the connection). - npm CLI (
view,ping):FETCH_ERRORat ~5.2s with--fetch-timeout=5000; stub-side TCP connection count and request bytes: 0.--loglevel=sillyshowsnpm http fetch GET ... attempt 1 failed with FETCH_ERROR, typerequest-timeout, stack insideminipass-fetch/lib/index.jstimeout timer. - make-fetch-happen@14.0.3 (npm's bundled copy) driven directly from a plain node script — no npm, no npmrc, no config:
Identical hang. Instrumenting
const mfh = makeRequire("<npm-install>/node_modules/make-fetch-happen/package.json")("make-fetch-happen"); await mfh("http://[redacted:ip]:<port>/", { timeout: 5000 }); // -> FETCH_ERROR at 5006ms, stub saw 0 connections
http.requestinside that child: the options are well-formed enough (hostname: "[redacted:ip]",port,path: "/"; note minipass-fetch'sgetNodeRequestOptionsalso setshost: parsedURL.hostwhich containsip:port— request.js:239-242 — but plain node handles that shape fine when hostname is present), the request fires'socket'with a socket that reportsremoteAddress/remotePortas the stub andconnecting: false— yet the stub never receives the connection.'error'never fires; the minipass-fetchrequest-timeouttimer is what ends it. - Plain node
http.requestwith the exact same options shape (including thehost: "ip:port"quirk and an equivalent keepAlive agent + extras): HTTP 200 in 2–33ms every time. So the fault is not the options shape, not node's http stack, and not npm's CLI wiring — it is inside make-fetch-happen's agent/fetch layer. - Controls: real registry fine (
npm view left-pad@^999.0.0→ fastE404round-trip, 270–570ms); closed loopback port → fastECONNREFUSED(216–355ms) — so npm's client does attempt connects and can see refusals; it is specifically the listening local server case that silently never connects. - Not a proxy/npmrc/env artifact:
npm config get proxy/https-proxy→ null; noHTTP(S)_PROXY/npm_config_*in env on the failing legs;.npmrcempty;http://localhost:<port>fails identically tohttp://[redacted:ip]:<port>.
Environment
- npm 10.9.8 / node 22.23.1 / macOS arm64 (Darwin 27)
- npm 10.9.9 / node 22.23.3 / Linux x64 (GitHub-hosted ubuntu-latest, Linux 6.17)
- npm 10.9.8 / node 22.23.2 / macOS arm64 (GitHub-hosted macos-14, Darwin 23.6)
- Bundled layer: make-fetch-happen@14.0.3, minipass-fetch@4.0.1
All three boxes fail byte-for-byte identically, including the standalone make-fetch-happen repro on box 1.
Why this matters
The "spin a stub registry and point npm at it" recipe is the standard way to make npm-behavior pins hermetic (E404 vs E5xx vs ECONNREFUSED classification, packument shapes). On current npm it silently cannot work on any of our Linux or macOS environments — the stub never sees a socket and the caller times out, which reads as a flake/hang rather than a clear error.
Happy to provide the full instrumented-child receipts or test candidate fixes.
Activity
docs: hermetic npm pins use the fake-npm shim, never a stub registry …