Reproducing CORS Failures with curl

A CORS failure in the browser is opaque: the console reports that a response was blocked, but the response body and most of its headers are hidden from JavaScript by design. curl removes that opacity. Because curl is not a browser, it never applies the Same-Origin Policy and never suppresses a response — it prints the exact raw headers the server returned, letting you reason precisely about what a browser would then decide.

This page is part of Cross-Origin Debugging & Error Diagnosis, which covers the full methodology for isolating CORS failures across the browser, network, and proxy layers. Use curl when you need a deterministic, scriptable reproduction that strips away browser-specific behaviour and shows you the ground truth of the server’s response.

What curl Reproduces — and What It Cannot

The WHATWG Fetch Standard (§4.8, CORS-preflight fetch) defines the exact headers a browser sends during preflight and the exact response headers it checks. curl lets you send those request headers by hand and read the response, but it will never enforce the check for you. The distinction matters:

This is a feature. It means curl isolates server and proxy behaviour from browser enforcement, which is exactly what you need when a request works in one place and fails in another.

Preflight Request Fields Reference

To faithfully reproduce a browser preflight, send the same request headers the browser would. The table below maps each field to the curl flag that sets it and the response header the server must return in reply.

Browser preflight field curl flag Server must reply with
Request method -X OPTIONS 204 or 200 status
Origin -H "Origin: https://app.example.com" Access-Control-Allow-Origin matching that origin
Access-Control-Request-Method -H "Access-Control-Request-Method: POST" Access-Control-Allow-Methods listing that method
Access-Control-Request-Headers -H "Access-Control-Request-Headers: Authorization, Content-Type" Access-Control-Allow-Headers listing those headers
Credentials (cookie / token) -H "Cookie: ..." or -H "Authorization: Bearer ..." Access-Control-Allow-Credentials: true + exact origin
Show response headers -i (or -s to silence progress)

How curl Fits the Debugging Flow

Reproducing a CORS failure with curl curl sends a synthetic preflight and actual request carrying Origin headers; the server returns raw headers; you compare them against the browser's enforcement rules to locate the fault. curl -X OPTIONS + Origin header Server / proxy returns raw headers Allow-Origin matches → browser OK missing / wrong → browser blocks
Figure — curl surfaces the raw response headers; you apply the browser's rules to decide whether a real request would pass.

Step-by-Step Reproduction

Step 1 — Reproduce the preflight

Send the synthetic OPTIONS request the browser would send before a non-simple request. This is the canonical command; the dedicated Simulating a Preflight with curl -X OPTIONS page walks through every flag:

curl -si -X OPTIONS https://api.example.com/v1/orders \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Authorization, Content-Type" \
  | grep -iE "HTTP/|access-control|vary"

A correct preflight replies with HTTP/2 204, an Access-Control-Allow-Origin equal to the origin you sent, an Access-Control-Allow-Methods that includes POST, and an Access-Control-Allow-Headers that lists Authorization and Content-Type.

Step 2 — Reproduce the actual request

Preflight success does not carry over to the real response. Confirm the actual request also carries the origin header — a frequent bug is emitting CORS headers only on the OPTIONS handler:

curl -si -X POST https://api.example.com/v1/orders \
  -H "Origin: https://app.example.com" \
  -H "Authorization: Bearer test-token" \
  -H "Content-Type: application/json" \
  -d '{"sku":"A1"}' \
  | grep -iE "HTTP/|access-control|vary"

Step 3 — Bisect the network path

If the headers are correct here but wrong through the CDN, the fault is an intermediary. Re-run Step 1 against the origin server directly (resolve the hostname to the origin IP) and compare. This is the core technique behind troubleshooting CORS at the proxy layer:

curl -si -X OPTIONS https://api.example.com/v1/orders \
  --resolve api.example.com:443:203.0.113.10 \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: POST" \
  | grep -iE "HTTP/|access-control"

Edge Cases and Security Boundaries

The null origin. Sandboxed iframes, data: URIs, and some redirects send Origin: null. Reproduce it with -H "Origin: null" and confirm your server does not reflect Access-Control-Allow-Origin: null unless you explicitly support sandboxed contexts — reflecting null grants access to any sandboxed page. See dynamic origin validation patterns for safe matching.

Case sensitivity. Access-Control-Allow-Origin comparison is an exact, case-sensitive string match against the serialized origin. https://App.example.com is a different origin from https://app.example.com; reproduce both to catch a normalization bug.

Credentials. A wildcard Access-Control-Allow-Origin: * is forbidden with credentials. Reproduce the credentialed path (Step 2) and verify the origin is echoed exactly and Access-Control-Allow-Credentials: true is present. The rules are covered in Credential Sharing & Security Boundaries.

Interaction with Caching Layers

When you reproduce a preflight repeatedly through a CDN, a cached 204 may mask a recent policy change. Add a cache-busting query string or inspect the CDN’s cache-status header to confirm you are hitting the origin. A cached preflight served without Vary: Origin is the classic cross-origin cache-poisoning failure; verify it by reproducing the request from two different Origin values and confirming each gets its own matching header. The mechanics are detailed in Handling Vary: Origin Header Correctly.

DevTools + curl Verification Checklist

Common Mistakes

Issue Technical impact Mitigation
Running curl without an Origin header Server skips CORS logic entirely; you test a code path the browser never hits Always send -H "Origin: ..." matching the real front-end origin
Concluding the server is fine because curl printed the body curl ignores CORS; a printed body says nothing about browser enforcement Judge the Access-Control-Allow-Origin header, not the body
Testing only OPTIONS, never the actual method Misses servers that set CORS headers on preflight but not on the real response Reproduce both the preflight and the actual request
Forgetting Access-Control-Request-Headers Preflight passes in curl but the browser’s real preflight fails on a missing allowed header Send the exact header list the browser would send

FAQ

If curl succeeds but the browser still fails, is the server correct?

Not necessarily. A plain curl always returns the body because it never enforces CORS. To reproduce what the browser evaluates you must send the Origin header (and, for preflight, Access-Control-Request-Method and Access-Control-Request-Headers) and inspect whether the response echoes a matching Access-Control-Allow-Origin. If curl shows correct headers but the browser fails, the gap is usually credentials mode, a missing header on the actual response, or an intermediary that only mutates browser traffic.

Why does curl return the response body even when CORS should block it?

CORS is enforced by the browser, not by the network or the server. curl is not a browser: it never applies the Same-Origin Policy and never hides a body based on Access-Control-Allow-Origin. That is exactly why it is useful — it shows the raw headers the server sent so you can reason about what a browser would do next.

Do I need to send Access-Control-Request-Headers in a curl preflight?

Only when the actual request will carry non-safelisted headers such as Authorization or Content-Type: application/json. The browser lists those in Access-Control-Request-Headers, and the server must reflect them in Access-Control-Allow-Headers. Send the same value the browser would to reproduce the preflight faithfully.

How do I test a credentialed request with curl?

Send the Origin header plus the credential (-H "Cookie: ..." or -H "Authorization: Bearer ...") and inspect the response for both Access-Control-Allow-Origin (the exact origin, never *) and Access-Control-Allow-Credentials: true. If either is missing or the origin is a wildcard, a real browser would block the credentialed response.

Topics in This Section