CORS errors are one of the most common reasons a frontend that looks correct still cannot talk to an API. The browser reports a policy failure, the server may appear healthy, and developers can lose time changing the wrong layer. This guide gives you a reusable checklist for diagnosing and fixing CORS issues safely, with practical examples, clear explanations of the headers involved, and scenario-based steps you can revisit whenever a new frontend, backend, proxy, or environment enters the stack.
Overview
If you need a fast answer, start here: a CORS error usually means the browser blocked a cross-origin request because the server response did not explicitly allow it. “Origin” is the combination of scheme, host, and port. That means http://localhost:3000 and http://localhost:5173 are different origins, even though both are local.
CORS, short for Cross-Origin Resource Sharing, is a browser security mechanism. It is not a backend authentication feature, and it is not enforced the same way by tools like curl or many server-to-server requests. That distinction matters. If curl succeeds but your browser app fails, the problem is often not the API logic itself. It is the browser refusing to expose the response to JavaScript because the required CORS headers are missing or inconsistent.
In practice, most CORS problems fall into a few categories:
- The API does not send
Access-Control-Allow-Origin. - The API sends the wrong allowed origin.
- A preflight
OPTIONSrequest is failing. - Credentials are involved, but the server uses a wildcard origin.
- A proxy, CDN, gateway, or framework middleware strips or overrides headers.
- The frontend is sending custom headers or methods that trigger preflight unexpectedly.
The safest way to fix CORS is to treat it as a server configuration problem first, then verify the browser request details second. Do not “solve” it by disabling browser security in development, because that bypasses the actual issue and can hide production failures.
Before changing code, capture three pieces of information from your browser’s network tab:
- The requesting origin, such as
http://localhost:3000. - The exact request method and headers your frontend sends.
- The response headers returned by the API, including any response to an
OPTIONSpreflight.
If you regularly compare payloads and headers while debugging integrations, a side-by-side response tool can help reduce guesswork. For example, JSON Diff Tools Compared: Best Ways to Compare API Responses and Config Files is useful when the issue is partly CORS and partly an unexpected API response shape.
Checklist by scenario
Use this section as your working checklist. Find the scenario that matches your error and verify each item in order.
Scenario 1: Simple frontend to API request is blocked
You open the app, make a GET request, and see a message like “Cross-Origin Request Blocked” or “No 'Access-Control-Allow-Origin' header is present.”
- Confirm the exact frontend origin. Check protocol, domain, and port. A mismatch as small as
localhostversus127.0.0.1can matter. - Inspect the API response headers. Look for
Access-Control-Allow-Origin. If it is absent, the browser will usually block access. - Verify that the allowed origin matches the request origin. If your frontend runs at
http://localhost:5173, the API must explicitly allow that origin unless you intentionally use a wildcard for public, non-credentialed requests. - Check whether the request is really hitting the expected backend. Reverse proxies, local dev proxies, and API gateways can route requests somewhere unexpected.
- Retest in the browser after each change. CORS is browser-enforced, so browser verification matters more than curl alone.
Scenario 2: Preflight request fails before the real request runs
If your request uses a non-simple method like PUT, PATCH, or DELETE, or includes custom headers such as Authorization, the browser may send an OPTIONS preflight first. If that fails, the main request never happens.
- Look for an
OPTIONSrequest in DevTools. If you see one, inspect its status code and response headers. - Ensure the server handles
OPTIONS. Some backends define routes forGETandPOSTbut forgetOPTIONS. - Return the required headers on the preflight response. Common ones include
Access-Control-Allow-Origin,Access-Control-Allow-Methods, andAccess-Control-Allow-Headers. - Include the method you actually use. If the browser asks to send
DELETEand the server only allowsGET, POST, the preflight will fail. - Include the headers the browser requested. If the request uses
AuthorizationorContent-Type, your allow-list must match what the browser asks for.
A common pattern is that the backend team configures CORS for the application routes but not for framework-level handling of OPTIONS. That makes the endpoint look configured while the browser still blocks it.
Scenario 3: Requests with cookies or auth tokens fail
Credentialed requests are more restrictive. If you send cookies or use fetch with credentials: 'include', the server must be configured carefully.
- Do not use
*forAccess-Control-Allow-Originwith credentials. Browsers reject this combination. - Set a specific allowed origin. Return the exact frontend origin.
- Send
Access-Control-Allow-Credentials: true. Without it, the browser will not expose the response for credentialed requests. - Check cookie settings. If cookies are involved, review attributes such as
SameSiteandSecure. A cookie issue can look like a CORS issue when authentication silently fails. - Confirm frontend fetch settings. If the browser should send cookies, the client must request that behavior explicitly.
If you are decoding tokens during API debugging, keep a dedicated utility nearby so you can separate token-content issues from CORS transport issues. That distinction saves time.
Scenario 4: It works in Postman or curl but not in the browser
This is one of the clearest signs that the issue is CORS-related rather than a general API failure.
- Remember that non-browser clients usually do not enforce CORS. Success there only proves the API can respond, not that the browser will allow JavaScript to read it.
- Use the browser network panel as the primary source of truth. Inspect both request and response headers there.
- Compare headers between working and failing cases. The browser may add
Originand trigger preflight in a way your manual test does not. - Check for redirects. A redirect to a login page or different host can drop expected headers or change origins.
Scenario 5: Local development works inconsistently across tools
Different local ports, dev servers, and proxies make CORS debugging noisy. A frontend on Vite, a backend on Express, and a separate auth service can introduce multiple origins at once.
- List every origin in your local stack. Frontend app, API, auth service, docs server, mock server, and asset host.
- Decide whether to solve CORS at the backend or use a local development proxy. A proxy can reduce noise by making requests appear same-origin during development.
- Avoid mixing localhost variants casually. Stick to one hostname convention where possible.
- Review environment variables. One wrong API base URL can make a healthy app call the wrong origin.
- Restart dev servers after config changes. Middleware and proxy changes are often cached until restart.
If your project setup itself is causing friction, related configuration guides can help stabilize the environment. For example, TypeScript Config Guide: tsconfig Options That Matter for Modern Projects is useful when frontend build behavior or path setup contributes to confusing local debugging.
Scenario 6: CORS breaks behind a proxy, CDN, or API gateway
When an application grows, CORS may be configured in more than one place: app middleware, reverse proxy, ingress controller, CDN rules, or gateway policy.
- Trace the real response path. Determine whether headers come from the app server, proxy, gateway, or a cached edge response.
- Check for duplicate headers. Multiple layers may inject competing
Access-Control-Allow-Originvalues. - Verify that error responses also carry CORS headers. Some stacks only add them on 200 responses, leaving 401 or 500 responses blocked by the browser.
- Inspect cached responses carefully. Header changes may not appear immediately if an intermediate layer caches them.
- Keep one source of truth if possible. It is easier to reason about CORS when a single layer owns the policy.
Safe configuration examples
The exact syntax depends on your framework, but the policy should follow the same ideas.
Public read-only API without credentials:
Access-Control-Allow-Origin: https://example-frontend.comOr, if truly public and no credentials are involved:
Access-Control-Allow-Origin: *Authenticated API with cookies or credentialed requests:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: truePreflight support for token-based API requests:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-TypeBe deliberate. The goal is not “allow everything.” The goal is “allow the exact origins, methods, and headers the application needs.”
What to double-check
This section is the practical sanity list to run before you keep changing server code.
- The error message may mention CORS even when the root issue is different. A 500 response without CORS headers can appear as a browser CORS block. Check server logs too.
- Redirects change the picture. A request to one origin may redirect to another, and the second response may not allow your frontend.
- Header spelling and casing still matter in configuration tools. While HTTP header names are case-insensitive in principle, config interfaces or code paths may not behave as expected if values are misspelled.
Content-Type: application/jsoncan trigger preflight. Developers sometimes assume only auth headers do this.- Errors on non-2xx responses need attention. Your API should return consistent CORS headers even when the request is unauthorized or invalid.
- Origin reflection should be controlled. Reflecting any incoming origin without validation is easy to implement and easy to misuse.
- Browser cache can confuse verification. Hard refresh and retest after config updates, especially when proxies or service workers are involved.
- Development and production often differ. A config that works on localhost may fail once the app runs on a different subdomain, protocol, or port.
A useful debugging rhythm is: inspect browser network tab, compare expected versus actual headers, then review server logs for the same request. That three-way view usually narrows the issue quickly.
Common mistakes
Most recurring CORS incidents come from a short list of avoidable mistakes.
Using a wildcard with credentials
This is one of the most frequent misconfigurations. If credentials are enabled, the server cannot broadly allow every origin with *. Use an explicit origin instead.
Forgetting the preflight path
A route may support POST just fine, but if OPTIONS is not handled, the browser never sends the real request. Always test preflight for methods and headers beyond the simplest cases.
Adding CORS only at the application route level
Framework middleware order matters. If authentication or error middleware runs before CORS headers are attached, failed requests may appear as blocked cross-origin responses.
Trying to fix CORS only in the frontend
You can adjust fetch options to reduce unnecessary preflight in some cases, but the main policy decision lives on the server or gateway. Frontend changes alone rarely fix a true CORS policy mismatch.
Assuming localhost is a single origin
Different ports are different origins. So are different schemes. Moving from HTTP to HTTPS can reintroduce a problem that seemed solved.
Reflecting every origin automatically
Dynamic origin reflection can be useful, but it should validate the incoming origin against an allow-list. Otherwise you risk turning CORS from a specific access rule into an unrestricted one.
Ignoring infrastructure layers
Load balancers, CDNs, ingress rules, and serverless platforms can rewrite or drop headers. When a CORS issue appears only after deployment, the app code may not be the only place to inspect.
For teams that maintain multiple utility workflows, documenting these patterns in a shared markdown runbook can help. A live preview tool can make that easier; see Markdown Previewer Tools: Best Options for README Writing, Docs, and Live Rendering.
When to revisit
CORS configuration is not a “set once and forget forever” task. Revisit it whenever the shape of your application changes.
- When you add a new frontend origin. New admin panel, staging app, mobile web shell, or docs site.
- When you move environments. Local, staging, preview, and production often run on different hosts and protocols.
- When authentication changes. Switching from bearer tokens to cookies, or vice versa, can change what headers and credentials must be allowed.
- When infrastructure changes. New reverse proxy, gateway, CDN, load balancer, or serverless deployment model.
- When request patterns change. A simple GET endpoint becomes a PATCH endpoint with authorization headers and starts preflighting.
- When browser behavior surfaces new edge cases. Even without relying on changing policy details, different environments can expose assumptions your previous setup hid.
Here is a practical maintenance checklist you can keep in your project documentation:
- List all approved frontend origins by environment.
- List which endpoints require credentials.
- Document allowed methods and headers for authenticated APIs.
- Test one successful request and one failing request path in the browser.
- Verify that error responses also include the intended CORS headers.
- Review gateway or proxy behavior after deployment changes.
- Retest whenever environment variables or base URLs change.
If you want a simple rule to remember, use this one: fix CORS where the browser expects permission, not where the error is loudest. That usually means checking server and infrastructure headers first, then narrowing frontend request details second.
Save this checklist for the next time a working integration suddenly breaks between environments. CORS errors are rarely mysterious once you inspect the actual request, response, and preflight path with discipline.