Skip to content

Debugging CORS errors step by step in Chrome and Firefox DevTools

A repeatable routine for debugging CORS errors: read the console, find the preflight in the Network panel, compare headers, and reproduce the request with curl.

The ProxifyEdge team 5 min read

CORS errors are frustrating partly because your code gets almost no information. fetch rejects with TypeError: Failed to fetch, and that’s it: no status, no headers, no body. The browser deliberately hides the details from your script. It doesn’t hide them from you, though. Everything you need to debug a CORS error is in DevTools, if you know where to look. This guide is a routine you can follow every time.

Step 1: read the full console message

Start with the console, not the Network panel. The browser writes a specific reason for every CORS failure, and the wording tells you which check failed.

In Chrome, look for “has been blocked by CORS policy” followed by the reason:

Access to fetch at 'https://api.example.com/v1/items' from origin 'http://localhost:5173'
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 puts the reason in parentheses:

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource
at https://api.example.com/v1/items. (Reason: CORS preflight response did not succeed).
Status code: 404.

The phrase that matters most is whether it mentions a preflight. If it does, the failing response is the OPTIONS request, not your actual request. Here’s how the common reasons map between the two browsers:

Chrome saysFirefox saysMeaning
No ‘Access-Control-Allow-Origin’ header is presentCORS header ‘Access-Control-Allow-Origin’ missingThe response didn’t opt in at all
The ‘Access-Control-Allow-Origin’ header has a value ’…’ that is not equal to the supplied originCORS header ‘Access-Control-Allow-Origin’ does not match ’…’The server allows a different origin
It does not have HTTP ok statusCORS preflight response did not succeedThe OPTIONS request returned a non-2xx status
Request header field x-foo is not allowed by Access-Control-Allow-Headersmissing token ‘x-foo’ in CORS header ‘Access-Control-Allow-Headers’A request header isn’t allowed
…must not be the wildcard ’*’ when the request’s credentials mode is ‘include’Credential is not supported if the CORS header ‘Access-Control-Allow-Origin’ is ’*’Cookies were sent to a wildcard response

Firefox’s messages link to an MDN page per reason, which is worth bookmarking. Chrome also collects CORS problems in the Issues panel, with the affected request and the header that caused it.

Step 2: find the request, and the preflight, in the Network panel

Open the Network panel and reload with it open, because requests made before DevTools opens aren’t recorded. Filter by Fetch/XHR to cut the noise.

In Chrome, a failed request shows CORS error in the Status column. If the request was preflighted, there’s a separate row for the OPTIONS request with the type preflight. Firefox shows the OPTIONS request as its own row too, and labels blocked requests with the reason, such as “CORS Missing Allow Origin.”

Look at the pair together:

  • Preflight failed, real request never sent. Fix the OPTIONS response first. Nothing else matters until it passes.
  • Preflight passed, real request blocked. The OPTIONS handler is fine, but the actual response is missing the headers. This often happens when error responses skip the CORS middleware.
  • No preflight row at all. The request was “simple” and went straight out. Only the main response’s headers matter.

If you don’t see a preflight you expected, the browser may have answered it from its preflight cache, which honors Access-Control-Max-Age. Open the page in a fresh private window, or wait for the cached entry to expire, to force a new one.

Step 3: compare the request and response headers

Click the failing row and open the Headers tab. You’re comparing two lists.

From the request (or, for a preflight, what the browser is asking for):

  • Origin: the exact value the server must allow, including scheme and port. http://localhost:5173 and http://localhost:3000 are different origins.
  • Access-Control-Request-Method and Access-Control-Request-Headers on a preflight.
  • Whether cookies were attached, shown in the Cookies tab.

From the response:

  • Access-Control-Allow-Origin: must equal the Origin exactly, or be * when no credentials are used.
  • Access-Control-Allow-Methods and Access-Control-Allow-Headers on a preflight: must cover what was requested. Authorization is never covered by *.
  • Access-Control-Allow-Credentials: true if you used credentials: 'include'.
  • The status code. A preflight must return 2xx.

Typical mismatches are a trailing slash or www in the allowed origin, http vs https, a header name your server forgot, or a reverse proxy that strips the CORS headers on the way out.

When the response tab is empty

Chrome often shows “Failed to load response data” for a blocked response. That’s DevTools respecting the same rules as your script. The status code and headers are still visible, which is usually enough. If you need the body, reproduce the request outside the browser (step 4).

Step 4: reproduce it with curl

curl doesn’t enforce CORS, which makes it perfect for seeing what the server really sends. Right-click the request in the Network panel and choose Copy → Copy as cURL. You get the exact URL, headers and body, ready to paste into a terminal. Add -i to print the response headers:

curl -i 'https://api.example.com/v1/items' \
  -H 'Origin: http://localhost:5173' \
  -H 'Accept: application/json'

To replay a preflight by hand, send the OPTIONS request with the headers the browser would add:

curl -i -X OPTIONS 'https://api.example.com/v1/items' \
  -H 'Origin: http://localhost:5173' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: content-type, authorization'

Now you can change one thing at a time on the server and watch the headers respond, without clicking around your app. If curl shows the right headers but the browser still fails, compare the Origin values carefully. The browser’s is the source of truth.

Step 5: rule out problems that only look like CORS

Several failures surface as CORS errors even though CORS isn’t the cause:

  • The server crashed. A 500 from an unhandled exception often skips the middleware that adds CORS headers, so the browser reports a CORS error. Check the server logs and the status code.
  • Content Security Policy. If your page’s CSP connect-src doesn’t allow the API’s origin, the request is blocked before it’s sent, with a CSP message instead of a CORS one. See CORS, CSP, CORP and COEP compared.
  • Mixed content. An https page calling an http API is blocked outright.
  • Network failures. DNS errors, certificate problems and ad blockers also produce Failed to fetch. The Network panel shows them as failed requests with no CORS label.

A shortcut for third-party APIs

If the server you’re debugging isn’t yours, the investigation usually ends with “it doesn’t send CORS headers, and I can’t change that.” At that point, see how to fix ‘No Access-Control-Allow-Origin header is present’ for your options.

One of them is ProxifyEdge. The playground sends a request through https://api.proxifyedge.com/proxy from your browser and shows the status, timing, response headers and body side by side. It’s a quick way to confirm whether an API works once CORS is handled for it.

Key takeaways

  • The console always states the reason. Look first for whether it mentions a preflight.
  • In the Network panel, check the preflight row and the real request together. Disable the cache to force a fresh preflight.
  • Compare Origin against Access-Control-Allow-Origin character by character, including scheme and port.
  • Copy as cURL plus -i shows exactly what the server sends, without CORS in the way.
  • Crashes, CSP, mixed content and blocked requests can all look like CORS errors. Check the status code and the exact message.

Are you sure?