Skip to content

/commands/cors/test-cors-with-curl

Test CORS with curl: send a preflight OPTIONS request with an Origin header

verified onenvironmentlocal · windows 10.0.26200 arm64 · GNU bash 5.3.15 (Git Bash)updated
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:

bash
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)'
wait

Output

== 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 responseMust be
Status of OPTIONS2xx (200 or 204); a 401, 404 or 405 fails the preflight
Access-Control-Allow-OriginExactly the Origin you sent, or * (not * when the request uses credentials)
Access-Control-Allow-MethodsIncludes the method from Access-Control-Request-Method (GET, HEAD, POST do not need it)
Access-Control-Allow-HeadersIncludes every header from Access-Control-Request-Headers
Access-Control-Allow-Credentialstrue, if the browser request sends cookies or Authorization with credentials
Access-Control-Max-AgeOptional: seconds the browser may cache the preflight

Useful variations

bash
# 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.

Sources

glossary · corsWhat is a CORS preflight request?A CORS preflight is the OPTIONS request a browser sends before a cross-origin request with custom headers, a JSON body or PUT, to ask if it is allowed.errors · corsblocked by CORS policy: No 'Access-Control-Allow-Origin' headerThe browser blocks a cross-origin response without Access-Control-Allow-Origin. Fix it on the server by sending the header for your origin, or proxy the API.errors · corsCORS: Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight responseThe CORS preflight reply does not list a header the request sends, such as Authorization or a JSON Content-Type. Add it to Access-Control-Allow-Headers.errors · corsCORS error: 'Access-Control-Allow-Origin' must not be the wildcard '*' when credentials mode is 'include'Browsers reject Access-Control-Allow-Origin * on requests with cookies or credentials. Echo an allowed origin, add Allow-Credentials true and Vary: Origin.commands · corsAllow multiple origins in Access-Control-Allow-OriginAccess-Control-Allow-Origin takes one origin, not a list. Check Origin against an allowlist, reflect the match and add Vary: Origin, in Node.js or nginx map.commands · corsEnable CORS in Express with the cors middlewareEnable CORS in Express with the cors middleware: allow all origins or a list, credentials, preflight caching, the defaults, and why order of middleware matters.flags · corshttp-server --cors: serve local files with Access-Control-Allow-Originhttp-server --cors adds Access-Control-Allow-Origin: * and a fixed Allow-Headers list; --cors=Authorization extends it. No credentials or methods header.