CORS Checker & Preflight Analyzer
Work out whether a request is simple or preflighted, see which Access-Control headers the browser sends and which ones your server has to send back, then generate the matching Nginx or Express config. This is a rules analyzer and reference — it does not call the URL you enter. Nothing is requested or uploaded — everything runs in your browser.
What is CORS? Cross-Origin Resource Sharing is an HTTP header protocol that lets a server tell the browser which other origins are allowed to read its responses. An origin is the scheme, host and port taken together, so https://app.example.com and https://api.example.com are different origins. Unless the server returns an Access-Control-Allow-Origin header matching the caller, the browser withholds the response from your JavaScript, even when the request itself succeeded.
How to Use the CORS Checker
-
Enter the endpoint URL — The URL identifies the request you are reasoning about. It has to start with
http://orhttps://— the tool validates the scheme and remembers the value, but it never calls the endpoint. - Pick the HTTP method — GET, POST and HEAD are classed as simple. PUT, DELETE and PATCH always force a preflight, because they are not on the Fetch standard's list of CORS-safelisted methods.
-
List any custom request headers — Typing anything here —
Authorization,X-Api-Key,Content-Type— reclassifies the request as preflighted. One header outside the CORS safelist is enough to trigger an OPTIONS round trip on its own. -
Click Check to see the exchange — The table separates what the browser sends (
Origin, plusAccess-Control-Request-Methodand-Request-Headerson a preflight) from theAccess-Control-*headers your server must return. Server values read depends on server config because they come from your server, not from this page. - Fill in the config generator — Set the origin, methods and headers you actually want to allow, then choose Nginx or Node.js (Express) to get a config block wired to those values.
- Copy the config and diff it against production — Copy Config puts the block on your clipboard. Comparing it line by line with your live server config is usually how you find the directive that is missing, misspelled or scoped to the wrong location.
How This Tool Classifies a Request
This page is an analyzer and a reference, not a live prober. It does not send a request to the URL you type, and no page could: reading another origin's response headers from JavaScript requires that origin's permission, which is the exact thing you are trying to find out. A browser that let a script read those headers without permission would have already defeated CORS. So instead of guessing at a network result, the tool applies the classification rule from the Fetch standard to the method and headers you describe:
simple = (GET OR HEAD OR POST) AND no custom request headers
Anything else is preflighted. That is the same test the browser makes before it decides whether to send an OPTIONS request, and it is why adding a single Authorization header to a working GET endpoint can suddenly break it: the call that used to go straight out now needs a successful preflight first.
The real specification adds one condition this tool leaves to you. A POST only counts as simple when its Content-Type is application/x-www-form-urlencoded, multipart/form-data or text/plain. A POST carrying application/json — which is most JSON APIs — is preflighted in a real browser. Type Content-Type into the custom headers box to model that case correctly.
The Preflight Handshake
When a request is not simple, the browser pauses it and sends an OPTIONS request to the same URL first. That probe carries Origin, Access-Control-Request-Method naming the method it intends to use, and Access-Control-Request-Headers listing the non-safelisted headers it intends to send. The server has to answer with a 2xx status and echo back permission in Access-Control-Allow-Origin, Access-Control-Allow-Methods and Access-Control-Allow-Headers. Only then does the real request go out.
Two consequences catch people out. If the preflight fails, the actual request is never sent at all — so a POST that appears to fail may never have reached your handler, and your server logs will show only an OPTIONS. And many frameworks route OPTIONS through the same authentication middleware as everything else, which returns 401 to a probe that browsers deliberately send without credentials. Preflights must be answered before auth runs.
Why the Browser Error Tells You So Little
A blocked cross-origin call surfaces in JavaScript as a generic TypeError: Failed to fetch, while the network panel often shows a perfectly healthy 200. That asymmetry is intentional — the detail is withheld from the page so a malicious script cannot use error messages as a side channel. The specific reason is printed to the browser console instead, and that console line is the single most useful piece of evidence you have. Read it before changing anything.
To see the headers a server genuinely returns, ask from outside the browser, where the same-origin policy does not apply. This reproduces the preflight exactly:
curl -i -X OPTIONS https://api.example.com/data \ -H "Origin: https://app.example.com" \ -H "Access-Control-Request-Method: PUT" \ -H "Access-Control-Request-Headers: authorization,content-type"
Compare what comes back with the expected rows in the table above. A missing Access-Control-Allow-Headers, or one that omits authorization, is the usual culprit.
CORS Header Reference
Every header in the protocol, which side sends it, and what it controls. Request headers are set by the browser and cannot be overridden from JavaScript.
| Header | Sent by | What it does |
|---|---|---|
Origin | Browser | The calling origin, as scheme + host + port. Set automatically and not writable by script. |
Access-Control-Request-Method | Browser (preflight) | The method the real request intends to use. |
Access-Control-Request-Headers | Browser (preflight) | Comma-separated list of the non-safelisted headers the real request intends to send. |
Access-Control-Allow-Origin | Server | A single origin, or *. Lists of origins are not valid — echo the caller's origin instead. |
Access-Control-Allow-Methods | Server (preflight) | Methods permitted on this resource. Only read from a preflight response. |
Access-Control-Allow-Headers | Server (preflight) | Request headers the client may send. Must name each one; there is no wildcard when credentials are in play. |
Access-Control-Allow-Credentials | Server | true permits cookies and TLS client certificates. Incompatible with *. |
Access-Control-Expose-Headers | Server | Response headers your script may read beyond the safelisted seven. Without it, custom headers are invisible to response.headers.get(). |
Access-Control-Max-Age | Server (preflight) | Seconds the preflight result may be cached, so repeat calls skip the OPTIONS round trip. |
Vary: Origin | Server | Not a CORS header, but required whenever the allowed origin is computed per request — otherwise a CDN caches one caller's permission and serves it to another. |
Simple vs Preflighted Requests
The distinction decides whether one HTTP round trip happens or two, so it shows up as latency as well as correctness. The safelisted request headers — the ones that never trigger a preflight — are Accept, Accept-Language, Content-Language and a restricted Content-Type.
| Simple request | Preflighted request | |
|---|---|---|
| Methods | GET, HEAD, POST | PUT, PATCH, DELETE, and any other method |
| Headers | Safelisted headers only | Any header, including Authorization and custom X- headers |
| Content-Type | Form-encoded, multipart, or plain text | Anything else, including application/json |
| Round trips | One | Two — OPTIONS, then the real request |
| If permission is refused | Request is sent; the response is withheld from your script | Real request is never sent at all |
| Cacheable permission | Not applicable | Yes, via Access-Control-Max-Age |
Because a simple request is genuinely delivered even when the response is blocked, CORS does not protect against side effects. A simple POST that changes state still changes it. Guarding against that is the job of CSRF tokens and SameSite cookies, not of the Access-Control headers.
Wildcards, Credentials and Server Config
Access-Control-Allow-Origin: * is the permissive setting and the one most tutorials reach for, but it has a hard limit: the browser refuses it outright whenever the request carries credentials. If your frontend sends cookies or uses fetch(url, { credentials: 'include' }), the server must return one concrete origin rather than a wildcard, and must also send Access-Control-Allow-Credentials: true. The header accepts exactly one origin, so serving several frontends means reading the incoming Origin, checking it against an allowlist you control, and echoing it back with Vary: Origin.
The generator above always writes Access-Control-Allow-Credentials: true into the block. If you left the origin as *, that pairing is invalid and every credentialed request will be rejected by the browser. Either set a specific origin, or delete the credentials line from the generated config before deploying it.
The Apache option in the config dropdown currently produces an empty box. These are the equivalent directives for an .htaccess file or a virtual host, with mod_headers enabled:
Header set Access-Control-Allow-Origin "https://app.example.com" Header set Access-Control-Allow-Methods "GET, POST, OPTIONS" Header set Access-Control-Allow-Headers "Content-Type, Authorization" Header set Access-Control-Allow-Credentials "true" Header set Access-Control-Max-Age "86400"
Whichever server you use, the same two mistakes account for most failures. The first is applying headers only to successful responses, so a 404 or a 500 arrives without them and the browser reports a CORS error for what is really a routing or crash bug. Nginx's add_header needs the always flag for exactly this reason. The second is answering OPTIONS from an application layer that sits behind authentication or behind a redirect — preflights follow neither, so the probe must be answered directly, at the edge, with a 2xx.
Frequently Asked Questions
No. It classifies the request from the method and headers you describe and shows you which headers each side is responsible for; the values in the server column read depends on server config because they come from your server. A web page cannot read another origin's response headers without that origin's permission, which is the very thing being tested. To see real headers, use curl -i -X OPTIONS with an Origin header, or open your browser's network panel on the page that is actually failing.
An OPTIONS request the browser sends on its own, before your real request, to ask whether the call is permitted. It carries Origin, Access-Control-Request-Method and Access-Control-Request-Headers. The server must reply with a 2xx status and matching Access-Control-Allow-* headers. If it does not, the real request is never sent, which is why a failing POST sometimes leaves no trace in your application logs.
Three things together: the method is GET, HEAD or POST; every header is on the CORS safelist (Accept, Accept-Language, Content-Language, Content-Type); and Content-Type is form-encoded, multipart or plain text. Break any one and the browser preflights. Most JSON APIs are preflighted for this reason alone — application/json is not a safelisted content type.
Postman and curl are not browsers, so the same-origin policy never applies to them. CORS is enforced by the browser on behalf of the page, not by the server. An endpoint that answers curl perfectly can still be unusable from JavaScript because it never sends Access-Control-Allow-Origin. The fix is always on the server, never in your fetch call.
No. When a request is made with credentials, the browser rejects a wildcard and requires a single explicit origin plus Access-Control-Allow-Credentials: true. The wildcard is also refused for Access-Control-Allow-Headers in credentialed requests, so each header has to be named. To support several frontends, validate the incoming Origin against an allowlist and echo it back, adding Vary: Origin so caches keep the answers separate.
Yes — enter something like http://localhost:3000 and the classification works the same way. Remember that origins compare scheme, host and port exactly: http://localhost:3000 and http://localhost:8080 are different origins, and so are http://localhost and http://127.0.0.1. A dev proxy that serves the API under the same origin as the app sidesteps CORS entirely and is often the simpler local setup.
The cache is per browser, per origin and per URL, and it applies only to the preflight, not to the real request — so you should still see one OPTIONS the first time. Browsers also cap the value regardless of what you send, so a very large number does not mean the permission is cached for days. Values also get discarded when the browser cache is cleared or when a hard reload bypasses it, which is why the OPTIONS reappears while you are debugging.
No, and treating it as a security control is a common mistake. CORS only stops browser scripts on other origins from reading responses. It does not stop a server, a script, or a command-line client from calling your endpoint, and it does not stop a simple cross-origin POST from being delivered and taking effect. Real protection comes from authentication, authorization, CSRF tokens and SameSite cookies.
That option currently produces no output — a known bug in the generator's template lookup. Nginx and Node.js (Express) both work as expected. The equivalent Apache directives are listed in the server config section above; copy them from there and paste them into your .htaccess or virtual host with mod_headers enabled.
Use Cases
Diagnosing a Blocked Fetch
Your call worked yesterday and fails today after someone added an auth header. Describe the request here, confirm it is now preflighted, and go looking for the missing Access-Control-Allow-Headers entry rather than rewriting the client code.
Wiring Up a New Frontend Origin
A second app on a new subdomain needs access to an existing API. Enter the origin, methods and headers it will use, generate the Nginx or Express block, and hand a reviewed config to whoever owns the server.
Budgeting the Extra Round Trip
Before switching an endpoint from form-encoded POST to JSON, check what that does to the request class. Seeing it flip to preflighted tells you every call gains an OPTIONS hop, and that a sensible Access-Control-Max-Age belongs in the same change.
Explaining the Failure to a Backend Team
Paste the header exchange into a ticket so the conversation starts from which side owns which header. It replaces the usual back-and-forth over whether a CORS error is a frontend problem — it never is.
Auditing a Permissive Config Before Release
Staging often ends up with Allow-Origin: * and credentials enabled together. Generate the block you intend to ship, check it against the credentials rule, and tighten the origin to a real allowlist before it reaches production.