CORS preflight tester, response header diagnostic and config generator for nginx, Apache, Express and FastAPI
Fetch the CORS response headers of any URL from a calling origin, see whether the preflight is required, diagnose the most common CORS failures and get a ready-to-paste server config that fixes them.
What CORS actually checks and where most teams trip
CORS (Cross-Origin Resource Sharing) is the browser security mechanism that decides whether a script running on one origin is allowed to read a response from another origin. The check is enforced on the client side by the browser, never on the server, which is the first counterintuitive part: a server can serve a response normally to curl and still be refused by a browser if its CORS headers are wrong. The second counterintuitive part is that CORS does not block the request from being sent. The browser sends it, the server processes it, the response comes back, then the browser hides the response from the JavaScript that asked for it. By the time you see “blocked by CORS” in the console, the side effect on the server already happened.
Most teams trip on three specific scenarios. The first is the wildcard plus credentials trap: Access-Control-Allow-Origin: * combined with Access-Control-Allow-Credentials: true is explicitly forbidden by the spec and modern browsers refuse it. The fix is to echo the requesting origin in the Allow-Origin header instead of using a wildcard. The second is the missing preflight handler: any non-simple request (PUT, DELETE, custom Content-Type, custom header) triggers an OPTIONS preflight that the server must answer with the appropriate Allow-Methods and Allow-Headers; most frameworks need an explicit middleware or route to handle this. The third is the cached preflight surprise: Access-Control-Max-Age caches the preflight result for the configured duration; a config change that requires a new preflight does not take effect until the cache expires.
How the diagnostic works
The tool fetches the target URL through our backend (so the browser does not block the inspection itself) and applies the CORS specification rules against the headers actually returned. The diagnostic distinguishes between simple requests (GET, HEAD, POST with form encoding) where only the response headers matter, and non-simple requests where a preflight is required and must succeed independently before the actual request is allowed.
For every diagnostic the tool calls out the exact spec rule being checked, the header value observed, and what value is expected for the request to succeed. The output is intentionally verbose because CORS issues are almost always caused by one tiny mismatch buried in three correct headers — the goal is to make the mismatch impossible to miss.
The server config snippets and what they protect against
The generated config for nginx, Apache, Express, FastAPI and Django all follow the same defensive pattern: echo the request origin only if it appears in an explicit allow-list, set Vary: Origin so caches do not mix responses for different callers, handle the OPTIONS preflight explicitly with a 204 response, and avoid the wildcard. The allow-list approach is the right default in 2026: a server that accepts cross-origin calls from one or two known frontend origins should never use *, even on public endpoints. Wildcards are appropriate for genuinely public APIs (a typeface CDN, a stock-price ticker) where the only state involved is in the URL.
Common CORS mistakes the tool detects
- Wildcard with credentials:
Allow-Origin: *+Allow-Credentials: true— browser refuses the response entirely. - Wrong protocol mismatch: caller is HTTPS, allow-listed origin is HTTP (or vice versa) — spec treats these as different origins.
- Port mismatch:
localhost:3000andlocalhost:3001are different origins; common in development setups with multiple dev servers. - Missing OPTIONS handler: framework returns 404 or 405 on OPTIONS, so the preflight fails before the actual request is attempted.
- Header in request not in Allow-Headers: any custom header (Authorization, X-Custom-*) must be listed in
Access-Control-Allow-Headersof the preflight response. - Trailing space or slash in Allow-Origin:
https://app.example.com/with a trailing slash does not matchOrigin: https://app.example.com; the comparison is exact. - Missing Vary: Origin: a CDN caches the response for the first caller and serves it to every subsequent caller with a stale Allow-Origin.
How to deploy CORS changes without breaking traffic
CORS changes have a higher accidental-blast-radius than most config changes because they are checked client-side and a wrong header silently breaks one segment of users (the segment whose browsers actually enforce the failed rule). Three precautions reduce the risk. First, deploy the change to a staging environment with the same browser test you use for production; CORS headers behave differently across Chrome, Firefox and Safari at the edges. Second, monitor the count of OPTIONS requests in your access logs in real time after the change; a sharp drop or rise signals a preflight cache issue or a misconfigured handler. Third, set Access-Control-Max-Age short during the rollout (60 seconds is reasonable) so a regression can be corrected without waiting for the cache to expire, then raise it to 86400 once stable.
Frequently asked questions
Why does my CORS work in Postman but not in the browser?
Postman is not a browser and does not enforce CORS. Browsers are the only thing that enforces CORS. A request that succeeds in Postman or curl tells you the server accepts the request; it tells you nothing about whether the browser will let your JavaScript read the response. Test in an actual browser, including Safari for iOS coverage.
Should I use the wildcard for a public API?
Only if the API is genuinely public (no authentication, no per-caller state) and you never plan to send credentials. The moment you need cookies or an Authorization header from the browser, the wildcard becomes a blocker and you have to refactor to an explicit allow-list anyway. Start with the allow-list to save the migration later.
What is the difference between a simple and a non-simple request?
A simple request is GET, HEAD, or POST with a Content-Type of application/x-www-form-urlencoded, multipart/form-data, or text/plain, and only the standard headers. Anything else (PUT, DELETE, custom Content-Type like application/json, custom headers like Authorization) is non-simple and triggers a preflight. The preflight rules are strictly more demanding.
Can I just put a CORS proxy in front of my API?
For a development workaround, yes. For production, no: a CORS proxy adds latency, a single point of failure and a security boundary that has to be hardened. Configure the API server itself to return the correct headers; the cost is the same five lines of nginx config.
Does CORS apply to images and stylesheets?
Cross-origin loading of images and stylesheets is allowed by default (they are subject to the same-origin policy only when accessed by script via canvas pixel reads or CSSOM). The CORS dance applies to fetch, XMLHttpRequest and other script-readable resource loads. The cross-origin crossorigin attribute on script tags adds CORS semantics to module imports.
My CORS is fine but I see “Failed to fetch” in the console — what now?
“Failed to fetch” with no further detail typically means the preflight was outright refused (the server returned 405 or 500 on OPTIONS) or the network connection was blocked (DNS failure, mixed-content blocked, firewall). Check the Network tab and look for a red OPTIONS request just before the failed request; that is your culprit.













