Fix: blocked by CORS policy from origin localhost:3000
The browser, not your server, is refusing the response — read the exact wording of the console error to know which of three fixes actually applies.
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.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
OPTIONSat all, request fails directly → the server isn't sendingAccess-Control-Allow-Originon the real response. OPTIONSrequest 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:
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. On Express 5, prefer it to the app.options('*', cors()) line some guides still show: that route crashes at start-up with Missing parameter name, as Fix 'Missing parameter name at 1' in Express 5 routes explains.
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:
const corsOptions = {
origin: 'http://localhost:3000',
credentials: true, // sends Access-Control-Allow-Credentials: true
};
app.use(cors(corsOptions));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)
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. Setting mode: 'no-cors' does not get round this either: the browser then gives the script an opaque response with a null body, so response.json() throws, as why no-cors makes response.json() fail with Unexpected end of input explains.
5. A token-protected API that answers the preflight with 401#
A variant of the message ends differently, and the ending is the clue:
Response to preflight request doesn't pass access control check: It does not have HTTP ok status.The Fetch standard only accepts a preflight whose status is an "ok status", which it defines as "a status in the range 200 to 299, inclusive". Anything else, a 401 included, fails the preflight before your real request is sent. The usual cause is an API that checks a Bearer token: the preflight carries Access-Control-Request-Method and Access-Control-Request-Headers (which merely lists authorization), not your token, so an auth guard that runs on every method rejects the OPTIONS call. That is what the Stack Overflow report on a Vercel serverless API ran into: PATCH, POST and PUT from the browser failed with this message while the same calls worked outside it, and the asker later found their JWT middleware was rewriting the response for the token-less request.
A minimal Node server with that shape, called with curl on 3 October 2026 (Node 26.7, sending the same headers a browser sends), shows both states:
### server: broken — preflight sent by the browser (no Authorization header)
HTTP/1.1 401 Unauthorized
### server: broken — actual PATCH with the token
HTTP/1.1 200 OK
### server: fixed — preflight sent by the browser (no Authorization header)
HTTP/1.1 204 No Content
### server: fixed — actual PATCH with the token
HTTP/1.1 200 OKThe fix is to answer OPTIONS before the auth check, with the CORS headers and a 2xx status, and to list Authorization in Access-Control-Allow-Headers. In a Node serverless function such as a Vercel function:
export default function handler(req, res) {
res.setHeader('Access-Control-Allow-Origin', 'https://app.example.eu');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type');
if (req.method === 'OPTIONS') {
return res.status(204).end();
}
// token check and the real work only run for the actual request
}In Express, the same order means registering app.use(cors(corsOptions)) from step 2 before the auth middleware, so the cors package ends the OPTIONS request before the token check runs: its documented defaults are preflightContinue: false and a 204 status for the preflight.
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.
Everything runs in your browser — nothing you paste is sent anywhere, and tokens, cookies and passwords are masked in the output.
Optional: response headers (curl -i / curl -v output)
Verify the fix#
Reload the page and repeat the request. In DevTools → Network, the response headers for the real request should now include:
Access-Control-Allow-Origin: http://localhost:3000(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.
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.
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.
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: 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.
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.


