Home

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.

FactDetail
ErrorAccess 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.
FirefoxCross-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 itBrowser console. Your code only sees TypeError: Failed to fetch
Usual causeThe API does not send CORS headers, or does not answer OPTIONS
Usual fixAdd the headers on the server, for the exact origin, including on the preflight
BugTape catches itYes, 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

  1. The API sends no CORS headers at all. It was built to be called from a server, or from the same origin.
  2. The preflight is not handled. The GET or POST handler sets the headers, but OPTIONS returns 404, 405, or a response without them. Auth middleware that rejects OPTIONS with 401 causes the same thing.
  3. 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.
  4. The origin does not match exactly. https://www.example.com versus https://example.com, a different port, or a trailing slash in the configured value.
  5. Credentials with a wildcard. With credentials: 'include', * is not allowed. Chrome prints The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.
  6. A redirect. http to https, or a missing trailing slash, redirects to a URL that does not send the headers, and redirects on a preflight are not allowed.
  7. 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

All guides: Fix an error.

Start free. Free plan · No credit card · Set up in minutes. Or try the live demo first.