Next.js

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.

Mahdi Ben RhoumaMahdi Ben Rhouma
Updated
7 min read

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:

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

server.js
javascript
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:

server.js
javascript
const corsOptions = {
  origin: 'http://localhost:3000',
  credentials: true, // sends Access-Control-Allow-Credentials: true
};
 
app.use(cors(corsOptions));
client.js
javascript
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:

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

text
### 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 OK

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

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

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

(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

|

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