CORS Tester
Describe the request, paste the response headers, and see exactly which CORS check the browser fails — on this device.
This is a simulator, not a prober
Everything on this page is computed from the request you describe and the response headers you paste. No request is sent to your endpoint — not on load, not on Simulate, not ever. Open your network tab and watch: using this tool sends nothing.
The simulator only needs header names, so paste a placeholder where a real token would go.
Blocked — your code cannot read this response
The browser sends an OPTIONS preflight first.
First failing check: allow-headers-covers
| Check | Phase | Verdict | What the browser decides | Clause |
|---|---|---|---|---|
| same-origin | Classify | Pass | Cross-origin: https://app.example.com is asking https://api.example.com, so the CORS protocol decides whether your code may read the answer. The rows below are that decision, in the order the browser makes it. (Fetch, HTTP-network-or-cache fetch.) | Fetch · HTTP-network-or-cache fetch |
| method-safelisted | Classify | Pass | POST is a CORS-safelisted method, so the method alone does not make the browser preflight. (Fetch, CORS-safelisted method.) | Fetch · CORS-safelisted method |
| headers-safelisted | Classify | Warning | Authorization and X-Request-Id are not CORS-safelisted, so the browser preflights this request and names them in Access-Control-Request-Headers. The safelisted names are Accept, Accept-Language, Content-Language, Content-Type, Range, each with a value limit. (Fetch, CORS-safelisted request-header.) | Fetch · CORS-safelisted request-header |
| content-type-safelisted | Classify | Warning | Content-Type: application/json is not CORS-safelisted — application/json is not one of the three safelisted values (application/x-www-form-urlencoded, multipart/form-data, text/plain) — so the browser preflights this request and asks for content-type in Access-Control-Request-Headers. This is the most common reason a JSON POST preflights. (Fetch, CORS-safelisted request-header.) | Fetch · CORS-safelisted request-header |
| preflight-status-ok | Preflight | Pass | The preflight answered 204, an ok status, so the browser goes on to read its Access-Control-* headers. The body of a preflight response is ignored. (Fetch, CORS preflight fetch.) | Fetch · CORS preflight fetch |
| preflight-not-redirected | Preflight | Pass | The preflight was answered at the URL it was sent to, with no redirect, which is what a preflight requires. (Fetch, CORS preflight fetch.) | Fetch · CORS preflight fetch |
| allow-methods-covers | Preflight | Pass | Access-Control-Allow-Methods does not have to name POST: a successful preflight always allows the CORS-safelisted methods GET, HEAD and POST. (Fetch, CORS preflight fetch.) | Fetch · CORS preflight fetch |
| allow-headers-covers | Preflight | Fail | The response sends no Access-Control-Allow-Headers header, but the preflight asks for authorization, content-type and x-request-id, so the browser stops after the preflight. (Fetch, CORS preflight fetch.)Fix Access-Control-Allow-Headers: authorization, content-type, x-request-id Answer the preflight with every header your code sets that is not safelisted; the names are compared in lower case. | Fetch · CORS preflight fetch |
| authorization-not-wildcard-covered | Preflight | Fail | Access-Control-Allow-Headers does not name authorization, and no wildcard can stand in for it: it is a CORS non-wildcard request-header name and has to be listed. (Fetch, CORS non-wildcard request-header name.)Fix Access-Control-Allow-Headers: authorization, content-type, x-request-id Authorization is a CORS non-wildcard request-header name: it has to appear by name, even next to a *. | Fetch · CORS non-wildcard request-header name |
| max-age | Preflight | Pass | The response sends no Access-Control-Max-Age, so the browser caches this preflight for 5 seconds — the value Fetch uses when the header is absent — and preflights again after that. (Fetch, CORS preflight cache.) | Fetch · CORS preflight cache |
| allow-origin-present | Response | Pass | The response sends Access-Control-Allow-Origin: *. (Fetch, CORS check.) | Fetch · CORS check |
| allow-origin-single-value | Response | Pass | The response sends exactly one Access-Control-Allow-Origin value, which is what the check requires. (Fetch, CORS check.) | Fetch · CORS check |
| wildcard-vs-credentials | Response | Fail | The response sends Access-Control-Allow-Origin: *, and this request sends credentials. With credentials: "include" the browser reads * as a literal origin name, so it matches nothing and the response is blocked. Echo the requesting origin instead. (Fetch, CORS check.)Fix Access-Control-Allow-Origin: https://app.example.com Choose the value from the request's Origin header, allow only the origins you mean, and add Vary: Origin. | Fetch · CORS check |
| allow-origin-match | Response | Not checked | Not checked: the credentialed-wildcard rule above already decides this response, and * is not compared as an origin. (Fetch, CORS check.) | Fetch · CORS check |
| allow-credentials-true | Response | Fail | This request sends credentials, and the response has no Access-Control-Allow-Credentials header, so the browser blocks it and drops any Set-Cookie it carried. (Fetch, CORS check.)Fix Access-Control-Allow-Credentials: true The value is compared to the byte string true, in lower case; nothing else enables credentials. | Fetch · CORS check |
| vary-origin | Response | Warning | The response allows one specific origin (*) but does not send Vary: Origin. A shared cache — a CDN, a proxy, or the browser's own HTTP cache — may then reuse this answer for a different origin, which shows up as a CORS error that comes and goes. (RFC 9110 §12.5.5.)Fix Vary: Origin Add Origin to the Vary header on every response whose Access-Control-Allow-Origin depends on the request. | RFC 9110 · §12.5.5 |
| expose-headers | Response | Pass | Without Access-Control-Expose-Headers your code can read only the seven CORS-safelisted response headers (Cache-Control, Content-Language, Content-Length, Content-Type, Expires, Last-Modified, Pragma). From this response that means nothing yet — the pasted response has no readable header; anything else is present on the wire but invisible to JavaScript. (Fetch, CORS-safelisted response-header.) | Fetch · CORS-safelisted response-header |
17 checks · 4 failing
What this page decides
Every row below is computed from what you paste, and cites the clause it comes from.
- Whether the browser sends a preflight, and which part of your request causes it (Fetch: CORS-safelisted method and request-header)
- The OPTIONS response: ok status, no redirect, Access-Control-Allow-Methods and -Allow-Headers coverage (Fetch: CORS preflight fetch)
- The CORS check on the response: Access-Control-Allow-Origin presence, single value and exact match (Fetch: CORS check)
- Credentialed requests: Access-Control-Allow-Credentials, and where * stops being a wildcard (Fetch: CORS check)
- Authorization against Access-Control-Allow-Headers: * (Fetch: CORS non-wildcard request-header name)
- Which response headers your code can read, from the safelisted seven plus Access-Control-Expose-Headers (Fetch: CORS-safelisted response-header)
- Preflight caching with Access-Control-Max-Age, and Vary: Origin behind a shared cache (Fetch: CORS preflight cache; RFC 9110 §12.5.5)
- The literal preflight request, preflight response and actual request this description produces
What it does not do
- Sending a real request to your endpoint. Nothing here reaches the network: the verdict is computed from the text you paste.
- Emitting nginx, Apache, Express or Cloudflare CORS configuration.
- Content-Security-Policy composition or evaluation.
- Private Network Access / Local Network Access preflights, which add their own headers and their own rules.
- no-cors requests. Their opaque response has status 0 and no readable headers by construction, so there is no CORS check to trace.
- WebSocket handshakes, and the crossorigin nuances of <img>, <script>, <video> and fonts beyond the checks listed above.
Press ? for shortcuts
Do it offline:$
How to use it
- Describe the request: the origin your code runs on, the URL it calls, the method, the headers your code sets, the credentials mode, the body's Content-Type.
- Paste the headers of the response your code reads, up to the first blank line — a network panel or a terminal transcript has them.
- Paste the OPTIONS response separately when it differs, with its status code and its redirect state.
- Read the trace downwards. Each row names its clause and prints its verdict as a word — Pass, Fail, Warning, Not checked — and a failing row prints the header that would satisfy it.
- Switch to the wire view for the literal OPTIONS exchange. Copy link puts the description in the fragment.
A cross-origin call fails and the console reports only that access was blocked, never which rule decided it. Describe the request, paste the response headers, and the WHATWG Fetch decision is replayed in order: preflight or not, every check with its clause, the row that stops it, the header that clears it. Nothing is sent anywhere — the verdict comes from the pasted text.
How it works
Two origins are the same when scheme, host and port agree; an origin has no path, so a value ending in a slash is not one. When they differ the browser still sends the request, then withholds the answer unless Access-Control-Allow-Origin returns the requesting origin, a wildcard, or null. That is why a call can sit in the network panel at status 200 while the promise rejects.
Others earn an OPTIONS round trip first. A request stays simple while its method is GET, HEAD or POST, every header your code sets is one of five safelisted names inside its value limits, and the body's Content-Type is one of three values. A bearer token or a JSON body ends that, and the OPTIONS answer must arrive 2xx, unredirected, covering what was asked.
Credentials rewrite the rules rather than adding to them. Once cookies travel along, a star in any Access-Control-Allow-* header is read as a literal matching nothing, Allow-Credentials must carry the lowercase byte string true, and Authorization has to be listed by name.
Examples
POST https://api.example.com/v1/orders from https://app.example.com · Content-Type: application/json · X-Request-Id: 7 · credentials: omitOutcome: blocked — stopped at allow-headers-covers
Preflight: yes — header-not-safelisted, content-type-not-safelisted
Readable response headers: none
Fix: Access-Control-Allow-Headers: content-type, x-request-idThe everyday JSON POST. Two separate things force the OPTIONS round trip and the trace names both, yet the response side is fine: the OPTIONS answer lists no header names, so nothing is sent after it.
GET https://api.example.com/v1/me from https://app.example.com · credentials: include · response Access-Control-Allow-Origin: *Outcome: blocked — stopped at wildcard-vs-credentials
Preflight: no
Readable response headers: none
Fix: Access-Control-Allow-Origin: https://app.example.comEdge case, and the one that costs the most time: nothing is preflighted, Allow-Credentials is spelled correctly, and it is still refused — cookies turn the star into a literal.
GET https://api.example.com/v1/orders from https://app.example.com · exact Access-Control-Allow-Origin · Expose-Headers: X-Total-CountOutcome: allowed-with-warnings
Preflight: no
Readable response headers: x-total-countA working call that still earns a warning: naming one origin makes the answer depend on who asked, and without Vary: Origin a shared cache may hand it to the next visitor.
Every example on this page is executed against the same engine that powers the tool, on every build.
Gotchas worth knowing
patch and PATCH are two different methods
Fetch byte-uppercases DELETE, GET, HEAD, OPTIONS, POST and PUT before the CORS machinery sees them. PATCH is absent, so lowercase patch travels as written and a list of PATCH misses it.
A safelisted header can stop being safelisted
Accept, Accept-Language, Content-Language, Content-Type and Range escape a preflight only within limits: past 128 bytes a value loses the exemption, and Range needs a plain single range.
Origin: null is not an absent origin
Sandboxed iframes, file: pages and some redirect chains send the opaque origin null, which only Access-Control-Allow-Origin: null matches — and every sandboxed document sends it.
A cached preflight makes a fix look ignored
Access-Control-Max-Age keeps the OPTIONS result cached, and while that entry lives the browser stops asking. Fetch sets no ceiling and each browser caps it privately, so none is invented.
no-cors responses have nothing to trace
A no-cors request returns an opaque response by construction: status 0, no legible headers, no body reachable. There is no check to fail, so this page names the mode instead of simulating it.
Common errors
The response sends no Access-Control-Allow-Origin header, so the browser refuses to let your code read it. This is the "No 'Access-Control-Allow-Origin' header is present on the requested resource" message in the console. (Fetch, CORS check.)- The commonest failure: the header must ride the response your code reads, not only the OPTIONS answer.
The preflight answered 302. A preflight is only accepted with an ok status, 200 to 299, so the browser stops here and your real request is never sent. A redirect on a preflight is a network error for the browser, even to the same path with a trailing slash. (Fetch, CORS preflight fetch.)- Authentication middleware and path canonicalisation both cause it: OPTIONS arrives unauthenticated by design.
Access-Control-Allow-Headers: * does not cover authorization. It is a CORS non-wildcard request-header name, so the wildcard cannot be relied on to cover it: shipping browsers differ, at least one accepts it when the request sends no credentials, and none accept it once credentials are included. This is why a config that looks permissive still blocks an authenticated call. (Fetch, CORS non-wildcard request-header name.)- The star reads as maximally permissive and is not: signed-in routes fail while public ones keep working.
The response sends Access-Control-Allow-Credentials: TRUE. The check compares the value to the byte string true, in lower case, so TRUE does not enable credentials and the response is blocked. (Fetch, CORS check.)- A boolean coerced by a template or a config loader lands here; there is no parsing step.
The response allows https://app.example.com/, but this request comes from https://app.example.com: the allowed value has a trailing slash, and an origin never has a path — not even /. (Fetch, CORS check.)- Copied from an address bar, which shows the slash, into a setting that wants an origin. Serialised origins are compared byte for byte.
"app.example.com" is not an origin. An origin is a scheme, a host and an optional port — https://app.example.com — or the literal null for a sandboxed document.- A refusal from the form, not a verdict about your configuration: with no scheme, nothing to compare.
FAQ
Does this page send a request to my API?
No. It holds no address field and issues no request of any kind: the verdict is computed from what you describe and paste, on this device. Whether your endpoint behaves as its headers claim cannot be answered here.
Why does my POST send an OPTIONS request first?
Because something left the simple set — almost always a JSON Content-Type or a header your code adds. The trace names each trigger separately, so you can see which to allow.
Can I keep the wildcard and still send cookies?
Not together. A credentialed request reads a star as an ordinary literal, so the value must be one origin chosen per request, beside Access-Control-Allow-Credentials: true and Vary: Origin.
Keyboard shortcuts
- ⌘/Ctrl + Enter
- Re-run the simulation as described.
- ⌘/Ctrl + C
- Copy the pane on screen, nothing selected.
- ⌘/Ctrl + Shift + C
- Copy the pane whatever is selected.
- ⌘/Ctrl + S
- Download the pane on screen, not the page.
- t
- Step to the next theme.
- ? or ⌘/Ctrl + /
- List these shortcuts.
- Esc
- Close the overlay on top.