Next.js

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.

Mahdi Ben RhoumaMahdi Ben Rhouma
Updated
7 min read

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:

text
FetchError: request to http://localhost:1337/api/events failed, reason: connect ECONNREFUSED ::1:1337

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

text
FetchError: request to http://localhost:1337/api/events failed, reason: connect ECONNREFUSED ::1:1337

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

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

text
[ { 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:

text
[ { 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:

text
direct ::1 connect error: connect ECONNREFUSED ::1:1337

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

bash
ss -ltnp | grep 1337

A 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)#

js
// 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:

bash
node --dns-result-order=ipv4first node_modules/.bin/next dev

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

js
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#

bash
curl -v http://127.0.0.1:1337/api/events

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

json
{"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:

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

js
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

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote