CORS Tester
Configure a cross-origin request — URL, Origin, method, custom headers, credentials — and we'll issue the OPTIONS preflight server-side, send the actual request only when a browser would, then explain the verdict.
Custom request headers
Learn More
CORS is a browser-enforced policy: the browser refuses to expose a cross-origin response to JavaScript unless the response carries the right Access-Control-* headers. Server-to-server requests (curl your backend this tool) don't enforce CORS at all — they read every response header normally. That's why CORS bugs typically only show up when you ship: in dev you've got Vite or Next.js proxying the API to localhost the request is same-origin no CORS check happens the bug is invisible. Production splits front-end origin from API origin the browser starts enforcing and the requests start failing. This tool runs the browser sequence: it sends the OPTIONS preflight when the request is non-simple inspects the response and sends the actual method only if that preflight permits it. It then checks the actual response separately because successful preflight headers do not authorize the actual response. The output is what the browser would say if it had a useful error message instead of silent CORS-blocked-the-response — exact missing headers mismatched values and methods or headers the server didn't allow.
A request is 'CORS-safelisted' — no preflight required — only when ALL of these hold: method is GET / HEAD / POST; every request header is in the safelist (Accept Accept-Language Content-Language Content-Type Range); for Content-Type the value is one of three (application/x-www-form-urlencoded multipart/form-data text/plain); no ReadableStream body no XHR upload event listener etc. Anything else triggers a preflight: the browser sends an OPTIONS request to the same URL with Access-Control-Request-Method (the planned actual method) and Access-Control-Request-Headers (the planned custom headers) and waits for the response. The server's preflight response must return a 2xx status and authorize the request with Access-Control-Allow-Origin Access-Control-Allow-Methods and Access-Control-Allow-Headers. Wildcards have credential-mode restrictions and Authorization must always be named explicitly rather than matched by an allow-headers wildcard. If any gate fails the browser blocks the actual request without sending it. This tool now does the same and reports each missing piece individually — including proxy-stripped headers.
If your client uses fetch(url credentials: 'include' ) (or XHR's withCredentials = true the response MUST set Access-Control-Allow-Credentials: true AND Access-Control-Allow-Origin to the specific request origin (NOT *. Browsers reject the wildcard with credentials — silently in some with a console warning in others. The fix is reflective-origin: read the request's Origin header server-side validate it against an allowlist and echo the matching origin verbatim into the ACAO response header. Add Vary: Origin to prevent CDN cache poisoning (otherwise origin-A's response can be served to origin-B's browser). The other classic gotcha: Access-Control-Allow-Headers on the preflight must list every custom header the actual request will carry. Authorization is custom; Content-/json is custom (the safelisted Content-Type values don't include JSON); any X-* header is custom. Servers that ship Access-Control-Allow-Headers: Content-Type and forget Authorization are common — and the failure mode is that the user's auth token never reaches the API even though the API endpoint exists because the browser blocks the request at preflight. The findings here flag exactly which header is missing.
Frequently asked questions
Almost always: in dev your front-end is reverse-proxied to the API at the same origin (Vite proxy Next.js rewrites webpack devServer.proxy) so no CORS check happens. In production the front-end origin (https://app.example.com) and API origin (https://api.example.com) split the browser starts enforcing CORS and any missing Access-Control-* header surfaces as a failed request. The fix is to set the right CORS headers on the API in production: ACAO matching your app origin (or * if no credentials) and ACAH covering every custom header you send.
It's a metadata round-trip. Before sending the actual cross-origin request the browser sends an OPTIONS request with Origin Access-Control-Request-Method and Access-Control-Request-Headers headers. Your server should respond with Access-Control-Allow-Origin (matching) Access-Control-Allow-Methods (covering the planned method) Access-Control-Allow-Headers (covering every requested custom header) and a 2xx status. The browser then fires the actual request only if the preflight succeeds. The actual request must ALSO carry ACAO (the preflight headers don't carry over). Yes that means TWO requests for non-simple cross-origin calls — Access-Control-Max-Age lets you cache the preflight result for up to 24h to soften the latency cost.
It would let any origin read responses with the user's cookies attached. CORS spec explicitly forbids the combination — browsers reject the response (silently in some console warning in others). The fix: reflective ACAO. Read the request's Origin header server-side validate it against an allowlist of permitted origins echo the matching origin verbatim. Most server frameworks have CORS middleware that does this when you pass an array of allowed origins instead of *. Don't forget Vary: Origin to stop CDN cache poisoning.
No — CORS is an authorization model for the BROWSER not for the SERVER. The server still has to authenticate every request itself (cookies tokens etc.). CORS controls whether the browser will EXPOSE the response to JavaScript on the requesting origin. A malicious origin can still send the request to your server (the request itself is allowed); CORS just stops the response body from being readable. So your server's auth must be the real boundary; CORS is the additional browser-level safeguard.
Simple requests skip the preflight: GET / HEAD / POST with only safelisted headers (Accept Accept-Language Content-Language Range and Content-Type=application/x-www-form-urlencoded / multipart/form-data / text/plain). Anything else is non-simple and triggers OPTIONS. In practice almost every modern API call is non-simple — application/json content-type alone disqualifies you — so plan on dealing with preflight. The exception is image / form-encoded uploads which can stay simple.
By default JavaScript on the requesting page can only read a small set of standard response headers (Cache-Control Content-Language Content-Type Expires Last-Modified Pragma) regardless of what the server sent. Custom response headers (X-Request-Id X-RateLimit-Remaining anything you've added) are invisible to JS unless you list them in Access-Control-Expose-Headers. So if your front-end is reading a custom header and getting null despite seeing the header in DevTools that's the cause.
Access-Control-Max-Age: 86400 on the preflight response (24 hours; some browsers cap at 7200 = 2 hours Firefox at 24h). The browser caches the preflight result and skips OPTIONS for subsequent identical requests within the window. The cache key includes (URL method request-headers) so a request with different custom headers triggers another preflight even within the cache window. Production APIs should always set Max-Age to something — without it every cross-origin POST/PUT/DELETE adds a full RTT before the actual request fires.
More in Domain Health
Foundational integrity of your domain — TLS, DNS, ownership, and email authentication.