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
- curl 7.0 or later. Any build works; no HTTP/2 support is needed for this check. See the curl manual.
- The origin your front end serves from, written the way the browser sends it: scheme, host and port, with no trailing slash.
- The MDN reference on CORS for the meaning of each header.
Steps
- Step 1.
Send a plain GET with an
Originheader 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: * - Step 2.
Repeat the same request without the
Originheader and compare.curl -s 'https://api.github.com/repos/curl/curl' -D - -o /dev/null | grep -ci 'access-control-allow-origin'1A count of
0here while step 1 returned a header means the server reflects origins, which is the case step 3 checks. - 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
What to check next
- How to check rate limit headers: the same response usually carries the quota headers worth reading in one pass.
- How to check content-type of API response: a wrong content type breaks the client after CORS lets it through.
- How to test API with invalid input: the next thing a browser client sends that the API did not expect.
- Decode a JWT: when the blocked call carries a bearer token, read the token before blaming CORS.
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.
Related on this site
- Checker: cors preflight response, allowed origins, credentials, methods
- All api checks checks
basic4 minpublished updated Maks Verny