"Access to fetch at 'https://api.example.com' from origin 'https://app.example.com' has been blocked by CORS policy." If you build web apps, you'll see this error sooner or later. It's confusing because the same request works fine in curl or Postman. This guide explains what CORS actually is, why the browser blocks the request, and how to fix it properly.
Browsers enforce the same-origin policy: JavaScript on one origin can't read responses from another origin unless that server explicitly allows it. An origin is the combination of scheme, host and port, so these are three different origins:
texthttps://app.example.com https://api.example.com http://localhost:3000
CORS (Cross-Origin Resource Sharing) is how a server opts in. It sends headers telling the browser which other origins may read its responses.
Two consequences explain most confusion:
curl, Postman and server-to-server calls ignore it completely.fetch option can make an unwilling server allow your origin.For "simple" requests (GET, HEAD or POST with only basic headers and a Content-Type of form data or plain text), the browser sends the request and checks the response for:
textAccess-Control-Allow-Origin: https://app.example.com
Anything else triggers a preflight: before the real request, the browser sends an OPTIONS request asking for permission. Typical triggers are Content-Type: application/json, an Authorization header, or PUT, PATCH and DELETE. The server must answer with headers like:
textAccess-Control-Allow-Origin: https://app.example.com Access-Control-Allow-Methods: GET, POST, PATCH, DELETE Access-Control-Allow-Headers: Content-Type, Authorization Access-Control-Max-Age: 86400
If the preflight fails, the real request is never sent.
| Message mentions | Cause | Fix on the server |
|---|---|---|
No Access-Control-Allow-Origin header | The API doesn't send CORS headers, or not on this response | Add the header for your origin, including on error responses |
| Response to preflight doesn't pass access control | OPTIONS returns 404, 405 or a redirect | Handle OPTIONS and return 204 with the CORS headers |
Header not allowed by Access-Control-Allow-Headers | You send a custom header that isn't listed | Add it to Access-Control-Allow-Headers |
Wildcard * with credentials | credentials: 'include' with Allow-Origin: * | Return the specific origin and Access-Control-Allow-Credentials: true |
| Redirect not allowed for preflight | The API redirects, for example HTTP to HTTPS or adding a trailing slash | Call the final URL directly |
A frequent hidden cause: the server crashes or returns a 500 from a framework that skips the CORS middleware on errors. The browser reports a CORS error, while the real problem is a server error. Check the server logs.
cors for Express, django-cors-headers or CORSMiddleware in FastAPI.Origin header you receive, especially when credentials are allowed. That defeats the protection.* only for truly public, unauthenticated data.Vary: Origin when the allowed origin changes per request, so caches don't serve one origin's response to another.server.proxy and similar features send API calls from the same origin as your app, so the browser sees no cross-origin request.If a third-party API doesn't allow your origin, call it from your own backend (a small proxy endpoint or serverless function) and have your front end call that. Keep any API keys on the server. This is also why the GraphQL Client on this site forwards requests through a small server-side proxy: it lets you query endpoints that don't send CORS headers for this site. Production apps should still prefer configuring CORS properly on their own APIs.
OPTIONS preflight if there is one.Access-Control-Allow-*.Origin header with what the server allows, exactly, including scheme and port.For other API debugging tips, see Best practices for testing REST APIs.