Fix: FetchError request to localhost:1337 ECONNREFUSED ::1
The exact fix for the ECONNREFUSED ::1:1337 error between Next.js and a local Strapi API, and why it depends on your Node version.
A Next.js getStaticProps calls fetch('http://localhost:1337/api/events') against a local
Strapi instance that is demonstrably running — the same URL returns real JSON in a browser —
yet Node throws:
FetchError: request to http://localhost:1337/api/events failed, reason: connect ECONNREFUSED ::1:1337Cause and fix: since Node 17, localhost no longer resolves to the IPv4 loopback first on
every lookup. Strapi's project template binds to 0.0.0.0 by default
(host: env('HOST', '0.0.0.0')),
which listens on IPv4 addresses only, not on the IPv6 loopback ::1 — so on a system and HTTP client where the lookup tries ::1 first, the
connection is refused exactly like this. Replace localhost with 127.0.0.1 in the API URL,
or force IPv4 resolution with --dns-result-order=ipv4first, and the ambiguity is gone either
way.
The error, verbatim#
This is the exact failure from the original report (Stack Overflow #71836634, 24 votes, 16k+ views, accepted answer at 54 votes):
FetchError: request to http://localhost:1337/api/events failed, reason: connect ECONNREFUSED ::1:1337Note the address at the end: ::1, not 127.0.0.1. That single detail names the cause — the
client tried the IPv6 loopback, and whatever is listening on port 1337 is not listening there.
The question itself reports the contrast that makes this error confusing: opening
localhost:1337/api/events in a browser, as an unauthenticated request, returns the real
Strapi JSON — events, pagination, the lot. Only the server-side fetch() inside
getStaticProps fails. That is consistent with the cause above: a browser and a Node process
resolve and connect to localhost through separate code paths on the same machine, so one can
succeed while the other is refused, with no change at all on the Strapi side in between.
Why it happens#
localhost is not one address; it is a hostname that typically resolves to both 127.0.0.1
(IPv4) and ::1 (IPv6). Which one a Node process tries first depends on
dns.lookup()'s result order,
and that default changed in Node 17.0.0 (nodejs/node#39987):
before it, the default order was ipv4first; from 17.0.0 on, the default is verbatim — Node
returns the addresses in whatever order the operating system's resolver gives them, instead of
always sorting IPv4 ahead of IPv6.
Reproducing this directly (Docker, node:16 vs node:18.20.8 vs node:20.20.2, no host ports,
raw output in repro.log), calling dns.getDefaultResultOrder() on each version:
v18.20.8 verbatim
v20.20.2 verbatim(Node 16 has no such function to call — it was only added in v18.17.0/v20.1.0, after the
default itself had already changed in v17.0.0.) The actual lookup result for localhost
confirms what that default does in practice. On Node 16, the order is still ipv4first:
[ { address: '127.0.0.1', family: 4 }, { address: '::1', family: 6 } ]On Node 18 and Node 20, where the default is verbatim, the same lookup on the same container
resolver returns:
[ { address: '::1', family: 6 }, { address: '127.0.0.1', family: 4 } ]127.0.0.1 comes first on Node 16 — this matches the Stack Overflow answer (11 votes) that
blamed "my node version being higher than v16" for the opposite symptom. ::1 comes first on
Node 18 and 20 on this resolver.
To confirm the refusal itself is real and not just a resolution-order curiosity, a plain HTTP
server was started bound explicitly to 127.0.0.1:1337 only (the same single-interface bind a
default Strapi or Express app uses), then a raw socket was connected directly to ::1:1337 on
the same running process:
direct ::1 connect error: connect ECONNREFUSED ::1:1337Nothing was listening on the IPv6 loopback, because the server was never told to bind there —
only to 127.0.0.1. That is the same address and port combination the original question's
stack trace names.
Version dependency, stated plainly. Running the same scenario through an actual
fetch()/node-fetch@2 call on Node 18.20.8 and 20.20.2 in this container did not fail —
the HTTP client's own internal lookup resolved straight to 127.0.0.1. The DNS-order change and
the ::1 refusal are both real and reproduced above; whether a given app actually hits the
::1 branch depends on the exact lookup options the HTTP client passes and the host's
resolver, which is also why the Stack Overflow thread has readers who hit the crash and others
who didn't on the identical fix. The portable fix below removes the ambiguity either way.
Fix#
Before changing anything, confirm which interface the backend actually bound, rather than guessing from the framework's defaults. On Linux (including inside the Strapi container or VM):
ss -ltnp | grep 1337A Strapi default install prints a line with 127.0.0.1:1337, not [::]:1337 or *:1337 — that
single line is the same diagnosis as the error, read from the server side instead of the
client side. If it instead shows [::]:1337, the backend already accepts both stacks and the
cause of the error lies elsewhere (a firewall rule, a different port, a process that isn't
actually running).
1. Use 127.0.0.1 instead of localhost (the accepted answer)#
// config/index.js
export const API_URL =
process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:1337'127.0.0.1 is a literal IPv4 address, not a hostname — there is no DNS order to resolve, so
there is nothing for Node to get backwards. This is the fix from the accepted answer (54 votes)
and the second answer (22 votes) independently. When Next.js and Strapi run as separate Docker Compose services, 127.0.0.1 is the Next.js container itself and the same fetch fails with ECONNREFUSED 127.0.0.1:1337; the Next.js and Strapi in Docker fix shows server code calling Strapi by its service name while the browser keeps localhost:1337.
2. Or force IPv4-first resolution at the process level#
If changing every localhost reference in the codebase isn't practical, force the old default
back with the documented CLI flag:
node --dns-result-order=ipv4first node_modules/.bin/next devThis restores the pre-17 behaviour for the whole process without touching application code.
3. Or make the backend listen on both stacks#
If you control the backend (Strapi's config/server.js, or any http.createServer(...).listen()
call), bind it to the IPv6 wildcard ::, which on most systems also accepts IPv4 connections:
server.listen(1337, '::')Pick one fix, not all three — changing more than one thing at once proves nothing if the error goes away.
Verify the fix#
curl -v http://127.0.0.1:1337/api/eventsshould return the same JSON the browser showed, and the curl connect line should read
Connected to 127.0.0.1 (127.0.0.1) port 1337, not an address in brackets ([::1]). Restart
next dev (or redeploy) after editing config/index.js or .env.local — API_URL is read
once when the module is first imported, not on every request, so a running dev server keeps
using the old value until it restarts.
Then reload the actual page that called getStaticProps. The fix is confirmed once the build
or dev log stops printing the FetchError and the page renders the events list instead of
throwing during data fetching — a stale error in the browser's console after the backend
restarted usually means the Next.js process itself still needs restarting, not another code
change.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
The next error you'll likely hit#
Once the connection succeeds, the question's own evidence shows the shape of the problem that
comes next. A public, unauthenticated request to /api/events on this Strapi instance returns:
{"data":[{"id":1,"attributes":{"name":"Throwback Thursday with DJ Manny Duke", "..." : "..."}}],"meta":{"pagination":{"page":1,"pageSize":25,"pageCount":1,"total":3}}}That is Strapi v4's wrapped response shape — an object with data and meta keys — not a bare
array. The original code in the question does:
const events = await res.json()
return { props: { events: events.slice(0, 3) } }events here is the whole { data, meta } object, which has no .slice method — the next
error is TypeError: events.slice is not a function. The fix is to read the array out of the
wrapper first:
const { data } = await res.json()
return { props: { events: data.slice(0, 3) } }If the port itself is the problem rather than the address — Next.js's own dev server refusing
to start because 3000 is already taken, or a stale process still holding it — that is a
separate, simpler fix covered in
How to Change the Port in Next.js (and Fix EADDRINUSE).
When the real problem is an API URL that never reached the running app at all — a build tool
that failed to inline an environment variable rather than a DNS mismatch — that failure mode is
Fix "supabaseUrl is required" in Supabase JS.
And if the data this page fetches from Strapi also needs to be exposed to other callers —
not just rendered on this one page — rather than a form mutation handled by a Server Action,
see Next.js Server Actions vs API Routes: When to Use Each,
which covers exactly that external-integration case for choosing a dedicated API Route.
The underlying lesson holds beyond Strapi: any time a Node process talks to a backend by the
name localhost rather than a literal address, the Node version in use — not just the backend's
configuration — decides which loopback address gets tried first.
Further reading: Fix: blocked by CORS policy from origin localhost:3000.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Continue Reading
Fix 'Next.js 15 requires Node.js 18.18' Build Error
Next.js 15 drops support for older Node versions. Here is how to upgrade to Node.js 18.18.0 or later and fix build errors.
Fix Turbopack Build Errors from a Webpack Config
Find the plugin behind the unexpected webpack hook, choose a compiler deliberately, and verify that the behaviour you need still runs.
Fix Turbopack's Wrong Workspace Root Warning
Trace the lockfile behind an unexpected workspace root, choose the right standalone or monorepo boundary, and separate a reported memory spike from a verified cause.


