CORS Preflight Failures: Debugging with curl -X OPTIONS
Direct Answer: CORS preflight failures occur when the browser’s OPTIONS request to the server is denied due to missing or incorrect Access-Control-Request-Headers, Access-Control-Allow-Methods, or Origin validation. Debugging with curl -X OPTIONS helps verify server responses, identify misconfigured headers, and resolve mismatches before deployment.
When CORS preflight requests break in production, using curl -X OPTIONS can pinpoint the exact misconfiguration. This guide walks you through step‑by‑step debugging and fixing.
At 02:12 UTC on March 12, 2024, the on‑call engineer opened the incident page after a surge of 403 Forbidden errors flooding the client SPA console. The Network tab listed OPTIONS requests to /api/data returning 403 with headers Access-Control-Allow-Origin: * and noAccess-Control-Allow-Methods or Access-Control-Allow-Headers. The error message read: Refused to send form data to https://api.example.dev/data because it violates “Preflight response is invalid”.
A correlation ID inc-2024-0312-1128 was attached to all logs, enabling a rapid溯源. The first step was to reproduce the failure locally and capture the exact request/response pair before any mitigation was applied.
How does the CORS preflight request work?
When a browser sends a non‑simple request—such as PUT, PATCH, DELETE, or any request that includes custom headers like X-API-Key—it first fires an OPTIONS request to the same URL. This preflight is a safety handshake defined in the CORS spec (RFC 6454). The browser constructs the preflight request with three mandatory headers:
Header
Example
Purpose
Origin
https://app.example.com
Indicates the requesting origin.
Access-Control-Request-Method
POST
The actual HTTP method the client will use.
Access-Control-Request-Headers (optional)
X-Custom-Header,Authorization
Lists any non‑standard headers the client will attach.
The server must reply with a set of CORS‑specific headers that mirror (or expand) the client’s request. The following table compares typical configuration choices and their impact on latency and security:
Header
* (wildcard)
Exact origin (https://client.example.com)
Notes
Access-Control-Allow-Origin
Grants any origin – fastest but no credentials
Restricts to a single origin – required when or cookies are sent
About the author
Admin SarangAI · AI Tools Editor
Part of the SarangAI editorial team covering AI tools, product releases, emerging technology trends, and practical web utilities for developers and creators.
Use exact origin when credentials: “include” is needed
Access-Control-Allow-Methods
GET,POST,PUT,DELETE (pre‑defined list)
GET,POST,PUT (only those actually used)
Overspecifying is harmless; underspecifying breaks the preflight
Access-Control-Allow-Headers
content-type,authorization
content-type,authorization,x-custom-header
Must include every header the client sends
Access-Control-Max-Age
600 (10 minutes)
3600 (1 hour) for stable APIs
Higher values reduce preflight traffic but increase risk if permissions change
Vary: Origin
Present
Present
Signals to CDNs that the response varies by Origin; omission can cause cache poisoning
If any of these headers are missing or do not match the request, the browser aborts the real request with the “Preflight response is invalid” error. The preflight is stateless and per‑origin, meaning each distinct Origin triggers its own OPTIONS exchange unless cached via Access-Control-Max-Age. When credentials are involved, the server cannot safely respond with * for Access-Control-Allow-Origin; it must echo the exact origin header.
Why did the production request fail? (Failure case study)
Date: May 5, 2023 Environment: Node.js v18.16.0 on AWS Elastic Beanstalk, Express v4.18.2 with custom CORS middleware. Deployment: Container image myapp:1.2.3 (commit a8b2c5d). Symptom: End‑users in Europe saw Refused to send form data… “Preflight response is invalid”. Network trace captured the following curl reproduction (simplified):
Root cause: A recent refactor introduced a typo in the CORS options object. The line methods: "GET,POST", was missing a trailing comma, causing the middleware to interpret "GET,POST" as a single method token. The library (cors v3.0.0) consequently rejected the preflight and returned a 403 without the required Access-Control-Allow-Methods or Access-Control-Allow-Headers.
Investigation timeline:
00:12 UTC: PagerDuty alert, incident created.
00:14 UTC: On‑call engineer reproduced locally using the same container version.
00:18 UTC: Stack trace from the container logs (container logs myapp-1.2.3) revealed the malformed methods entry.
00:22 UTC: Fix applied: corrected the comma, rebuilt image myapp:1.2.4.
00:28 UTC: Deployed to Elastic Beanstalk, triggered a blue‑green swap.
00:35 UTC: Ran the CORS preflight check (curl -X OPTIONS https://api.example.dev -H "Origin: https://app.example.com"). Output: PASS. Error rate fell from 5 % to <0.1 % within 15 minutes.
Post‑mortem actions: Added an automated preflight validation step in the CI pipeline (GitHub Actions) that runs a curl -X OPTIONS assertion and fails the PR if any required CORS header is missing. The team also introduced a lint rule in the code formatter to enforce trailing commas in exported objects.
What are the exact steps to fix a misconfigured preflight? (Remediation walkthrough)
Capture the failing OPTIONS request – Use curl -v to see the request line, status, and headers.
Expected: Response includes Access-Control-Allow-Methods: GET,POST,PUT,DELETE and Access-Control-Allow-Headers: content-type,authorization,x-custom-header.
Add a curl assertion to CI – Automate the preflight check and fail the build when a required header is missing.
Expected: The command exits 0 and the response includes the required CORS header.
Deploy and run integration tests – Push the new image myapp:1.2.5 and execute the test suite that includes a CORS preflight assertion. Expected: All tests pass; no regression in the test coverage for CORS.
Which common mistakes cause preflight denials?
Wildcard origin with credentials – Using Access-Control-Allow-Origin: * while the client sends cookies or an Authorization header violates the spec. Wrong:*. Right:https://app.example.com.
Missing the client’s verb – Forgetting to include POST in Access-Control-Allow-Methods when the browser requests it. Wrong:GET,PUT. Right:GET,POST,PUT.
Spelling header names – Access-Control-Allow-Header instead of ...Headers. This typo causes the browser to ignore the response. Wrong:-Header. Right:-Headers.
Ignoring Vary: Origin – Omitting this header lets edge caches serve the wrong origin to other sites, creating security leaks. Wrong: No Vary. Right:Vary: Origin.
Incorrect Access-Control-Max-Age – Setting it to 0 or 30 forces a preflight on every request, adding latency. Wrong:30. Right:600 (or higher for stable APIs).
Each of these mistakes can be caught early by asserting the response headers with curl against the exact preflight request.
How can you verify with curl and Python?
Developers can reproduce the preflight locally and assert required headers with a simple script.
import sys
import requests
url = "https://api.example.dev/data"
origin = "https://app.example.com"
method = "POST"
headers = {
"Origin": origin,
"Access-Control-Request-Method": method,
"Access-Control-Request-Headers": "x-custom-header"
}
resp = requests.options(url, headers=headers)
print(f"Status: {resp.status_code}")
for k, v in resp.headers.items():
print(f"{k}: {v}")
# Success criteria: 200 status and the required CORS header present
if resp.status_code == 200 and "access-control-allow-origin" in resp.headers:
sys.exit(0)
else:
sys.exit(1)
Running this script prints the server’s CORS headers and exits with 0 on success, 1 on failure. It can be integrated into CI pipelines for automated preflight validation.
A quick Bash one‑liner can also extract relevant headers:
It outputs each matching header line, providing a fast manual check.
When should you use SarangAI tools?
During large rollouts, the free DNS propagation checker on SarangAI confirms hostnames resolve across edge locations, preventing hidden CORS failures caused by DNS latency. SarangAI's SSL chain checker validates certificate ordering, which can affect preflight when mutual TLS (TLS Client Auth) is required—mis‑ordered certificates cause the server to return a 403 before CORS headers are emitted. A short curl assertion provides the same preflight audit in CI and can fail the build when a required header is missing. For example, a nightly pipeline can run:
The pipeline exits non-zero when the expected header is absent.
Key Takeaways
CORS preflight fails when server headers do not mirror the browser’s OPTIONS request; verify Access-Control-Allow-Origin, -Methods, and -Headers.
A single typo in methods or allowedHeaders can cause 403 preflight errors, as seen in the May 2023 incident.
Remediation follows a repeatable pattern: capture, inspect, fix config, reload, verify with curl, and run a curl preflight assertion.
Common pitfalls include using * for allowed origin when credentials are needed, missing the client’s verb, and misspelling header names.
Automate validation with a Python script and integrate SarangAI checks into CI to catch misconfigurations before production.
Frequently Asked Questions
What does the Access-Control-Max-Age header control?
It tells the browser how long to cache the preflight response, reducing extra OPTIONS calls. Set it to a reasonable value (e.g., 600 seconds) and update it after config changes.
Why is Vary: Origin important for CORS?
Vary: Origin signals that response content depends on the request’s Origin header, preventing caching proxies from serving the wrong origin to other sites.
How can I test a preflight locally without a browser?
Use curl -X OPTIONS with the exact Origin and request headers. Compare the response status and headers against the CORS spec.
What tools can automate CORS preflight checks?
A curl -X OPTIONS assertion, the free DNS propagation checker on SarangAI, and SarangAI's SSL chain checker each cover a different aspect of request validation.
When should I rotate the Access-Control-Allow-Origin value?
Rotate when you add or retire client applications, or when you need to enforce stricter security policies. Always test with curl first to ensure the new origin is accepted.