Fix this error · Browser and network
Fix: blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource
Fix the CORS error on the server: Next.js route handlers, Express cors and Supabase Edge Functions, plus preflight, credentials and the wildcard.
Your page tried to read a response from another origin, and that server did not say your origin is allowed. The browser received the response and refused to hand it to your code. The fix belongs on the server you are calling: return Access-Control-Allow-Origin with your site's origin, and answer the OPTIONS preflight request with the same headers.
| Fact | Detail |
|---|---|
| Error | Access to fetch at 'https://api.example.com/data' from origin 'https://app.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. |
| Preflight form | … 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. |
| Firefox | Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://api.example.com/data. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing). Status code: 200. |
| Where you see it | Browser console. Your code only sees TypeError: Failed to fetch |
| Usual cause | The API does not send CORS headers, or does not answer OPTIONS |
| Usual fix | Add the headers on the server, for the exact origin, including on the preflight |
| BugTape catches it | Yes, as a failed request when it follows a click within 8 seconds. Page-load requests need one line |
What the error means
An origin is the scheme, host and port together: https://app.example.com and https://api.example.com are different origins, and so are http://localhost:3000 and http://localhost:8000. Browsers let a page send a request to another origin, but they only let your JavaScript read the response when the server includes Access-Control-Allow-Origin with your origin (or *) in it. This is CORS, Cross-Origin Resource Sharing.
For requests that are not "simple", such as a PUT or DELETE, a JSON body with Content-Type: application/json, or a custom header like Authorization, the browser first sends an OPTIONS request called the preflight. If the preflight response lacks the CORS headers, the real request is never sent.
CORS is enforced by the browser. The same request from curl, Postman or your server works, which is why the API can look fine when you test it by hand. A proxy or browser extension that disables CORS only changes your own browser.
Common causes
- The API sends no CORS headers at all. It was built to be called from a server, or from the same origin.
- The preflight is not handled. The
GETorPOSThandler sets the headers, butOPTIONSreturns 404, 405, or a response without them. Auth middleware that rejectsOPTIONSwith 401 causes the same thing. - The error response has no headers. The success path sets CORS headers, but a 500 or 401 from an error handler, proxy or gateway does not. The browser then reports a CORS error that hides the real status.
- The origin does not match exactly.
https://www.example.comversushttps://example.com, a different port, or a trailing slash in the configured value. - Credentials with a wildcard. With
credentials: 'include',*is not allowed. Chrome printsThe value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. - A redirect.
httptohttps, or a missing trailing slash, redirects to a URL that does not send the headers, and redirects on a preflight are not allowed. - You are calling a third-party API from the browser that is meant to be called from a server. It will never send CORS headers for your site, and it usually needs a secret key you should not put in the browser anyway.
How to fix it
Next.js App Router route handler
Set the headers on every response, including errors, and export an OPTIONS handler for the preflight. Check the request's Origin against a list and send Vary: Origin when the value depends on it.
// app/api/data/route.ts
const ALLOWED = new Set(['https://app.example.com', 'http://localhost:3000']);
function corsHeaders(origin: string | null): HeadersInit {
if (!origin || !ALLOWED.has(origin)) return { Vary: 'Origin' };
return {
'Access-Control-Allow-Origin': origin,
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Allow-Credentials': 'true',
Vary: 'Origin',
};
}
export async function OPTIONS(request: Request) {
return new Response(null, { status: 204, headers: corsHeaders(request.headers.get('origin')) });
}
export async function GET(request: Request) {
const headers = corsHeaders(request.headers.get('origin'));
try {
return Response.json({ ok: true }, { headers });
} catch {
return Response.json({ error: 'failed' }, { status: 500, headers });
}
}If you do not export OPTIONS, Next.js answers it with an Allow header and no CORS headers. For many routes, set the headers in Proxy (the file was called Middleware before Next.js 16) or in headers() in next.config.js.
If your frontend and API are the same Next.js app, call a relative URL (fetch('/api/data')). Same-origin requests need no CORS.
Express with the cors package
app.use(cors(...)) before your routes also answers preflight requests for every route.
import express from 'express';
import cors from 'cors';
const app = express();
app.use(cors({
origin: ['https://app.example.com', 'http://localhost:3000'],
credentials: true, // only if the browser sends cookies
}));
app.use(express.json());
app.get('/data', (req, res) => res.json({ ok: true }));Put cors() before auth middleware, so a preflight is not rejected with 401 before the CORS headers are added.
Supabase Edge Functions
Edge Functions do not add CORS headers for you. Answer OPTIONS and add the headers to every response, including errors. Recent versions of supabase-js export headers that match what the client sends:
import { corsHeaders } from 'npm:@supabase/supabase-js@^2/cors';
Deno.serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders });
}
try {
const body = await req.json();
return Response.json({ hello: body.name }, { headers: corsHeaders });
} catch {
return Response.json({ error: 'Invalid JSON' }, { status: 400, headers: corsHeaders });
}
});Credentials and the wildcard
If the request sends cookies (credentials: 'include', or withCredentials on XHR), the server must echo the exact origin, send Access-Control-Allow-Credentials: true, and send Vary: Origin. * works only for public data without credentials.
await fetch('https://api.example.com/me', { credentials: 'include' });Proxy the request through your own server
When you do not control the API, call it from your server and have the browser call your server on the same origin. In Next.js, a rewrite does this:
// next.config.js
module.exports = {
async rewrites() {
return [{ source: '/upstream/:path*', destination: 'https://api.example.com/:path*' }];
},
};mode: 'no-cors' does not fix this. It sends the request but gives your code an opaque response it cannot read.
Find the real status behind the CORS error
Open DevTools, Network, and click the failed request. If the status is 401, 404 or 500, fix that first, then make sure that response also carries the headers. If you only see the OPTIONS row failing, the preflight handler is the problem.
How to catch it in production with BugTape
A CORS failure is invisible to your code apart from TypeError: Failed to fetch (Chrome), NetworkError when attempting to fetch resource. (Firefox) or Load failed (Safari). The browser prints the CORS explanation itself. It does not go through console, so it is not in the recorded console output. The BugTape browser SDK records the failed request. When it follows a click within 8 seconds, BugTape reports it as User-triggered request failed: /data (failed to fetch) with the request URL and method, the element that was clicked, the replay of the page and the requests around it. Repeats of the same request from the same button become one issue with a count.
A request that runs on page load, on a timer or in the background is recorded in the report's network list but does not start a report. Catch the rejection and pass it to reportError(), which fires the window error event the SDK listens for. Give the error a message that names the endpoint, so different endpoints become different issues:
try {
const res = await fetch('https://api.example.com/data', { credentials: 'include' });
// …
} catch (err) {
reportError(new Error(`GET api.example.com/data failed: ${(err as Error).message}`, { cause: err }));
}An unhandled rejection from await fetch(...) without a catch is also reported, as Unhandled promise rejection: Failed to fetch. Requests cancelled with AbortController are not reported. The SDK records nothing before init() runs.
Set release in init(). When you mark the issue fixed, a report from a newer release reopens it as a regression, which tells you a deploy brought the missing header back.
import { init } from '@bugtape/sdk';
init({ apiKey: 'YOUR_PROJECT_KEY', release: import.meta.env.VITE_COMMIT_SHA });Your coding agent can read the issue over MCP (npx -y bugtape connect claude, or cursor or codex) and see the failing URL, method and origin before it edits the server. You review the pull request.
FAQ
Why does the request work in Postman or curl but not in the browser?
CORS is a browser rule. Postman, curl and server code do not check Access-Control-Allow-Origin, so they show the response. The browser checks it and blocks your page from reading the response when the header is missing or does not match.
Can I fix CORS in my frontend code?
No. The server you call decides which origins may read its responses. From the frontend you can only avoid the cross-origin call, by calling your own server on the same origin and letting it call the API.
Is Access-Control-Allow-Origin: * safe?
It is fine for public data that needs no login. It does not work with cookies or other credentials, and it lets any site read the response. For a private API, list the exact origins you trust.
Why do I get a CORS error when the server actually returned 500?
Error responses often come from a different code path, a proxy or a gateway that does not add the CORS headers. The browser then blocks the response and reports CORS instead of the status. Add the headers to error responses too, and check the status in the DevTools Network tab.
Sources
- MDN: Cross-Origin Resource Sharing (CORS) and MDN: CORS header 'Access-Control-Allow-Origin' missing, checked 2 Nov 2026.
- Firefox security.properties (
CORSMissingAllowOrigin2), checked 2 Nov 2026. - Chromium CORS error strings, checked 2 Nov 2026.
- Next.js route.js reference (CORS and
OPTIONS), checked 2 Nov 2026. - expressjs/cors README, checked 2 Nov 2026.
- Supabase: CORS support for Edge Functions and supabase-js
cors.ts, checked 2 Nov 2026. - MDN: Window.fetch() (exceptions), checked 2 Nov 2026.
Related errors
- TypeError: Failed to fetch:
TypeError: Failed to fetch - Mixed Content: request blocked:
Mixed Content: The page was loaded over HTTPS, but requested an insecure resource. This request has been blocked - "Script error." from cross-origin scripts:
Script error. - Cannot read properties of undefined:
TypeError: Cannot read properties of undefined (reading 'map') - Unexpected token < is not valid JSON:
SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON
All guides: Fix an error.
Start free. Free plan · No credit card · Set up in minutes. Or try the live demo first.