Node.js "TypeError: fetch failed" (undici) in CI
Node's global fetch (powered by undici) wraps low-level failures in a terse TypeError: fetch failed. The real reason is in the cause chain - usually a transient DNS, connection, or TLS problem.
What this error means
A request made with the built-in fetch rejects with TypeError: fetch failed. The same call works locally and often passes when the job is retried, pointing at a flaky network rather than a code bug.
TypeError: fetch failed
at node:internal/deps/undici/undici:13392:13
cause: Error: connect ECONNREFUSED 127.0.0.1:443
code: 'ECONNREFUSED'Diagnose it: what is different about the runner?
A build that passes locally and fails on a runner differs in a small number of predictable ways. Check those before changing build configuration, because the build config is usually not the thing that changed.
- run: |
node --version && npm --version
echo "NODE_ENV=$NODE_ENV CI=$CI"
nproc && free -h && df -h /
ls -la node_modules/.bin | headCommon causes
Transient network or DNS failure
A dropped connection, slow DNS, or a momentary registry/API blip surfaces as fetch failed. These are intermittent and pass on retry.
A service or proxy not reachable from the runner
The target host is firewalled, the proxy env is missing, or a local server under test is not up yet, so the connection is refused.
How to fix it
Inspect err.cause and retry transient failures
Log err.cause to see the underlying code, then wrap the request in a bounded retry for transient classes.
try {
const res = await fetch(url);
} catch (err) {
console.error('fetch failed cause:', err.cause);
throw err;
}Ensure the dependency is reachable
Confirm DNS/proxy settings and that any local service the test calls is started before the request runs.
- Check HTTP(S)_PROXY/NO_PROXY env in the runner.
- Wait for local servers to be ready before issuing requests.
- Verify the host resolves and is allowlisted from the runner.
The three that account for most of them
- Case sensitivity. Linux runners are case sensitive, macOS is not. An import with the wrong case resolves locally and fails in CI.
- Out of memory. Exit code 137 is a SIGKILL from the kernel, not a build error. Raise
--max-old-space-sizeor use a larger runner. - devDependencies pruned.
NODE_ENV=productionmakesnpm ciskip devDependencies, so the build tool itself goes missing. Set it after install, not before.
How to prevent it
- Always read
err.causeto classify the underlying failure. - Add bounded retries around outbound calls for genuinely transient classes.
- Wait for dependent services to be ready before making requests.