DEV Community

Cover image for Fix: blocked by CORS policy from origin localhost:3000
Mahdi BEN RHOUMA
Mahdi BEN RHOUMA

Posted on Originally published at iloveblogs.blog

Fix: blocked by CORS policy from origin localhost:3000

TL;DR

has been blocked by CORS policy means the browser received the response and
then refused to hand it to your JavaScript, because the server's headers
didn't authorise your page's origin to read it. The fix lives on the
server, not the client: add the right Access-Control-Allow-Origin
header (an explicit origin, not *, if the request carries credentials) and
make sure it applies to the preflight OPTIONS request too.

The error

A plain axios.get from a React app running on localhost:3000 throws
before the request handler even logs anything:

Access to XMLHttpRequest at 'https://api.example.com/data' from origin
'http://localhost:3000' has been blocked by CORS policy: Response to
preflight request doesn't pass access control check: No
'Access-Control-Allow-Origin' header is present on the requested resource.
Enter fullscreen mode Exit fullscreen mode

This is one of the most-viewed CORS questions on Stack Overflow,
and the confusing part is usually what happens next: the exact same request
in Postman, curl, or a server-side fetch works fine. Only the browser
complains. On the original question, the developer was calling a third-party
API (api.ipify.org) directly from the browser and had set two headers on
the request: "Access-Control-Allow-Origin": "*" and
"Access-Control-Allow-Methods": "GET,PUT,POST,DELETE,PATCH,OPTIONS". Both
are meant to be response headers a server sends back — setting them on the
request does nothing for authorisation, and it actively makes things worse:
neither is one of the browser's safelisted request headers, so sending them
turns what could have been a plain, unpreflighted GET into a preflighted
request, adding the exact OPTIONS round-trip this error is complaining
about.

Why it happens

CORS (Cross-Origin Resource Sharing) is a browser-enforced rule, not a
network-level or server-level restriction. For a plain, "simple" cross-origin
request — a GET with no custom headers — the browser sends the request and
lets the server respond, then checks the Access-Control-Allow-Origin
header on that response before handing it to your JavaScript. If the header
is missing, or names a different origin, the browser discards the response
instead of exposing it to your code. That's why Postman never sees the
problem — Postman isn't a browser page enforcing same-origin policy, it just
makes the HTTP call and shows you whatever comes back, headers included.

Most real API calls aren't "simple" in that narrow sense, though — a custom
header, a Content-Type: application/json body, or a non-GET/POST
method all disqualify a request from being simple. For those, MDN's CORS
reference

describes a second layer: "the browser first sends an HTTP request using the
OPTIONS method to the resource on the other origin, in order to determine if
the actual request is safe to send." This preflight is the request that
appears just before the real one in DevTools. Crucially, if the preflight's
response doesn't grant permission — a missing Access-Control-Allow-Origin,
or an OPTIONS route that isn't handled at all and returns a 404 — the
browser stops there and never sends the real request in the first place.
That's exactly the "Response to preflight request doesn't pass access
control check" wording in the error above: it's not the real GET or POST
being rejected, it's the reconnaissance request ahead of it.

A third, easy-to-miss rule applies once your request sends cookies or auth
headers (credentials: true in axios, credentials: 'include' in
fetch). MDN is explicit that in that case "the server must not specify the
* wildcard for the Access-Control-Allow-Origin response-header value, but
must instead specify an explicit origin" — and if it doesn't, "the browser
will block access to the response, and report a CORS error." A server that
"works" for public GETs but breaks the moment you add a cookie is almost
always missing this rule.

Fix

1. Confirm which layer is actually failing

Open DevTools → Network, find the failed request, and check whether an
OPTIONS request was sent right before it:

  • No OPTIONS at all, request fails directly → the server isn't sending Access-Control-Allow-Origin on the real response.
  • OPTIONS request appears but returns 404 or is missing CORS headers → the preflight itself is unhandled.

2. Add CORS middleware once, for every route

In Express, the fix is almost always the cors package applied globally,
before your routes are declared — not a manual header on each handler:

```javascript title="server.js"
const cors = require('cors');
const app = require('express')();

app.use(cors()); // must run before your route handlers

app.get('/data', (req, res) => {
res.json({ ok: true });
});




`app.use(cors())` with no options answers both the real request and the
`OPTIONS` preflight for every route, which fixes the "no `OPTIONS` sent" and
"preflight unhandled" cases in one change.

### 3. Restrict the origin and allow credentials, if you send cookies

For anything beyond a public API, don't leave the origin wide open — and if
your requests carry cookies or an `Authorization` header, the wildcard
won't work regardless:



```javascript title="server.js"
const corsOptions = {
  origin: 'http://localhost:3000',
  credentials: true, // sends Access-Control-Allow-Credentials: true
};

app.use(cors(corsOptions));
Enter fullscreen mode Exit fullscreen mode

```javascript title="client.js"
axios.get('http://localhost:4000/data', { withCredentials: true });




Both sides need `credentials` set — the server header and the client
request option — or the browser drops the cookie even when the origin
matches. If your frontend and API run on different ports during development
(a very common setup, and the most likely reason `localhost:3000` shows up
in the error at all — see
[How to Change the Port in Next.js (and Fix EADDRINUSE)](https://www.iloveblogs.blog/post/how-to-set-port-in-nextjs)
if you're not sure which port your own dev server is actually bound to),
double-check that the `origin` value matches the port your frontend is
served from exactly, including the scheme — `http://localhost:3000` and
`https://localhost:3000` are different origins as far as CORS is concerned.

### 4. Calling a third party you don't control

If the blocked request goes to an API you don't own (the original question
was calling `api.ipify.org`), you can't add a header to their server — but
first check what your own request is sending. If you copied a stray
`Access-Control-Allow-Origin` or `Access-Control-Allow-Methods` header onto
the *request* the way the original question did, remove them: they do
nothing for authorisation on a request you send, and by turning it into a
preflighted request they can trigger this exact error against a third-party
API that would otherwise have accepted a plain `GET`. Once the request is
back to a plain call with no invented headers, if it still fails, the only
remaining fixes are: check whether that API offers a CORS-enabled endpoint
or a server-side SDK, or proxy the call through your own backend so the
browser only ever talks to your origin, which already sends the right
headers.

If you have the failing request's console error and the response headers
from DevTools in front of you, paste them below — it reads the exact wording
and narrows down which class of CORS failure you're looking at, so you don't
have to work through the Network tab by hand.

<CorsErrorExplainer />

## Verify the fix

Reload the page and repeat the request. In DevTools → Network, the response
headers for the real request should now include:



```text
Access-Control-Allow-Origin: http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

(or the exact origin you configured — never * if credentials: true is
set). If a preflight OPTIONS request appears, it should return 204 or
200, not 404, and carry the same header.

Deploying to production changes the origin again

A CORS configuration that works on localhost:3000 will fail the same way
in production if the allowed origin isn't updated — Vercel and other hosts
assign a different domain, and origin: 'http://localhost:3000' will not
match https://app.example.com. Set the allowed origin from an environment
variable instead of hardcoding it, and if you're also running your API as
Next.js route handlers rather than a separate Express server, there's no
cors middleware to reach for — you set the header directly on the
response, and, for any method beyond a simple GET, you also need to export
an OPTIONS handler from the same route file that returns the same headers
with an empty body, or the preflight has nothing to talk to and fails the
same way an unhandled Express route would. Related deployment issues that
surface at the same stage — a build that passes locally but breaks the
moment it's live — are covered in
Next.js Build Passed But Production Broke.
A broader pre-launch pass for a Next.js + Supabase app, including the
allowed-origins check worth running before your first production traffic, is
in
Next.js + Supabase Production Launch Checklist (47 Items).

Getting the preflight and the credentials rule right once, in one
middleware, avoids re-debugging the same error on every new endpoint you
add.


Originally published at https://www.iloveblogs.blog

Top comments (0)