How to test CORS with curl

Send the request with an Origin header and print the response headers: curl -s https://api.example.com/ -H 'Origin: https://app.example.com' -D - -o /dev/null. A browser accepts the response only when access-control-allow-origin comes back holding that exact origin or a star.

Checker offline. Follow the manual steps below, they give the same answer.

Why check this

CORS failures reach you as a bug report that says the feature works in Postman and breaks in the browser. Postman sends no Origin, so it never triggers the check. Run this before front end sign-off on any API whose host differs from the page host, including a staging API called from a local dev server on a different port.

What the check settles is narrow and worth stating. It tells you which origins the server admits and on which methods, read from the server's own answer rather than from a configuration file that may not be the one deployed. It does not tell you whether the route is authorised, whether the body is correct, or whether a proxy in front of the service rewrites the headers on the way back. Those are separate checks, and a passing CORS read is not evidence for any of them.

Prerequisites

Steps

  1. Step 1.

    Send a plain GET with an Origin header and keep only the response headers.

    curl -s 'https://api.github.com/repos/curl/curl' -H 'Origin: https://h2check.org' -D - -o /dev/null | grep -i -E 'access-control|vary|^HTTP'
    
    HTTP/2 200
    vary: Accept,Accept-Encoding, Accept, X-Requested-With
    access-control-expose-headers: ETag, Link, Location, Retry-After, …
    access-control-allow-origin: *
  2. Step 2.

    Repeat the same request without the Origin header and compare.

    curl -s 'https://api.github.com/repos/curl/curl' -D - -o /dev/null | grep -ci 'access-control-allow-origin'
    
    1

    A count of 0 here while step 1 returned a header means the server reflects origins, which is the case step 3 checks.

  3. Step 3.

    Send an origin the API should reject and read what comes back.

    curl -s 'https://api.github.com/repos/curl/curl' -H 'Origin: https://evil.example' -D - -o /dev/null | grep -i 'access-control-allow-origin'
    
    access-control-allow-origin: *

How to read the result

| What you see | What it means | What to do | | --- | --- | --- | | access-control-allow-origin: https://app.example.com | The origin is on the allow list | Nothing. This is the shape you want. | | access-control-allow-origin: * | Any site may read the response | Fine for a public read-only API. Wrong for anything behind a session. | | No such header at all | The browser blocks the response | The API has no CORS layer on this route. Check the route, not the whole service. | | The header echoes whatever you sent | Origins are reflected without a list | Confirm a list exists. Reflection plus credentials is an account takeover. |

Common mistakes

Sign: The API works in Postman and fails in the browser.Cause: Postman sends no Origin header, so the server never runs its CORS logic and the response looks correct. Only a request that carries Origin exercises the code path the browser uses.
Sign: curl shows access-control-allow-origin, yet the browser still blocks the call.Cause: The header is on the GET but missing on the OPTIONS preflight, which the browser sends first for any request with a custom header or a non-simple method. Test the preflight separately.
Sign: Wildcard and credentials are both present and the browser rejects the response.Cause: A star allow-origin cannot be combined with access-control-allow-credentials. The specification forbids it, so the browser drops the response even though both headers look permissive.

What to check next

FAQ

How to test CORS headers without a browser?

The commands above are the test. Origin is an ordinary request header, so curl can send it and the server cannot tell the difference. What curl cannot do is enforce the result, which is why step 3 needs reading rather than trusting.

How to test CORS in the browser?

Open DevTools, Network tab, click the failing request, then the Headers panel. Compare the Origin you sent with the access-control-allow-origin you got. The Console tab names which rule failed.

Does a 200 status mean CORS passed?

No. The server answers normally and the browser discards the response afterwards. Status and CORS are independent, which is why the check reads headers rather than the status line.

Which origin should I use for a local dev server?

The one in the address bar, port included. http://localhost:3000 and http://localhost:5173 are different origins, and a list holding one does not admit the other.

Why does the vary header matter here?

A response whose allow-origin depends on the request origin must name Origin in vary, or a shared cache can store the answer given to one origin and hand it to another. The GitHub response in step 1 varies on Accept, Accept-Encoding and X-Requested-With, and its allow-origin is a constant star, so it has nothing to vary on. A server that reflects the origin and omits the header has a caching defect that curl shows you and the browser will not.

Verified

Verified by Maks Vernycurl 8.21.0

Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.

  • Checker: cors preflight response, allowed origins, credentials, methodsserver
  • All api checks checkscluster

basic4 minpublished updated Maks Verny