/commands/cors/test-cors-with-curl
Test CORS with curl: send a preflight OPTIONS request with an Origin header
- tool
- CORS
- versions
- curl 7.x / 8.x, Fetch Standard
- os
- linux, macos, windows
To see what a browser would get, send the same headers it sends. For a preflight: curl -i -X OPTIONS https://api.example.com/items -H "Origin: https://app.example.com" -H "Access-Control-Request-Method: PUT" -H "Access-Control-Request-Headers: content-type,authorization". The response must contain Access-Control-Allow-Origin with your origin (or *), the method in Access-Control-Allow-Methods and every requested header in Access-Control-Allow-Headers. curl itself never enforces CORS; it only shows the headers, so you compare them yourself.
Example against a local API
The API below allows only https://app.example.com:
cat > api.js <<'EOF'
const http = require("http");
const allowed = "https://app.example.com";
http.createServer((req, res) => {
if (req.headers.origin === allowed) {
res.setHeader("Access-Control-Allow-Origin", allowed);
res.setHeader("Vary", "Origin");
}
if (req.method === "OPTIONS") {
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
res.setHeader("Access-Control-Max-Age", "600");
res.writeHead(204);
return res.end();
}
res.setHeader("Content-Type", "application/json");
res.end('{"ok":true}');
}).listen(4807, "127.0.0.1");
setTimeout(() => process.exit(0), 5000);
EOF
node api.js &
sleep 1
echo "== preflight from the allowed origin"
curl -s -i -X OPTIONS http://127.0.0.1:4807/items \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: PUT" \
-H "Access-Control-Request-Headers: content-type,authorization" \
| tr -d '\r' | grep -iE '^(HTTP|access-control|vary)'
echo "== simple GET from another origin"
curl -s -i http://127.0.0.1:4807/items -H "Origin: https://other.example" \
| tr -d '\r' | grep -iE '^(HTTP|access-control)'
waitOutput
== preflight from the allowed origin HTTP/1.1 204 No Content Access-Control-Allow-Origin: https://app.example.com Vary: Origin Access-Control-Allow-Methods: GET, POST, PUT Access-Control-Allow-Headers: Content-Type, Authorization Access-Control-Max-Age: 600 == simple GET from another origin HTTP/1.1 200 OK
The second response is 200 OK but has no Access-Control-Allow-Origin: curl prints the body, while a browser on https://other.example would block it with "No 'Access-Control-Allow-Origin' header is present".
What to check in the output
| Header in the response | Must be |
|---|---|
| Status of OPTIONS | 2xx (200 or 204); a 401, 404 or 405 fails the preflight |
| Access-Control-Allow-Origin | Exactly the Origin you sent, or * (not * when the request uses credentials) |
| Access-Control-Allow-Methods | Includes the method from Access-Control-Request-Method (GET, HEAD, POST do not need it) |
| Access-Control-Allow-Headers | Includes every header from Access-Control-Request-Headers |
| Access-Control-Allow-Credentials | true, if the browser request sends cookies or Authorization with credentials |
| Access-Control-Max-Age | Optional: seconds the browser may cache the preflight |
Useful variations
# The real request after the preflight, with the headers the app sends
curl -s -i -X PUT https://api.example.com/items/1 -H "Origin: https://app.example.com" \
-H "Content-Type: application/json" -d '{}'
# Only the headers, discarding the body (GET)
curl -s -o /dev/null -D - https://api.example.com/items -H "Origin: https://app.example.com"If curl shows the right headers but the browser still fails, compare with the browser's own request in DevTools (Network tab, the OPTIONS row): a different URL (trailing slash, redirect), an extra header added by a library, or a CDN that caches a response for a different origin (missing Vary: Origin) are the usual differences. A redirect on the preflight fails in browsers too.