Skip to main content

CORS Preflight and CORS Check API

REAL DATA

Explain whether a cross-origin request needs a preflight and whether the browser would allow the response, naming the exact Fetch step that failed.

Authoritative reference data or standards computation

Base URL
/api/cors-preflight
Capabilities
2 routes
Last updated
July 30, 2026
Data source

GET /api/cors-preflight/preflight

Reports preflightRequired plus the exact condition that forces it, and reconstructs the OPTIONS request the browser would send. Nothing is fetched.

Live requestRuns against the public API
No key required
GET/api/cors-preflight/preflight?method=PUT&origin=https%3A%2F%2Fapp.example.com

Request parameters

HTTP method the page requests. Normalized like the Request constructor, which uppercases only DELETE, GET, HEAD, OPTIONS, POST and PUT. Forbidden methods (CONNECT, TRACE, TRACK) are rejected.

Comma-separated 'Name: value' pairs the page sets on the request (max 32 entries, max 2048 characters per value). An entry with no colon means that name with an empty value, and a repeated name models the repeated header-list entries a browser really sends. Because the list itself is comma-separated, a value containing a comma cannot be expressed.

Request mode. Only 'cors' can produce a preflight; the other two are explained in meta.warnings.

Credentials mode of the request. 'include' is the mode that disables every Access-Control-* wildcard: allow-origin, allow-methods, allow-headers and expose-headers. It cannot change whether a preflight is required — nothing in the Fetch Standard makes it — so on /preflight it only echoes back and adds a note, and every wildcard rule it controls is evaluated on /check.

Requesting origin as scheme://host[:port], with no path, query, fragment or trailing slash. It is serialized the way a browser serializes the Origin header before sending it — scheme and domain lowercased, an IPv6 literal compressed to its shortest form, a default port dropped — and the serialization is what appears in the reconstructed preflight.

Advanced response options4 options

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

Return all fields except these (comma-separated).

Pretty-print the JSON response.

Drop the envelope: return the raw array/object without data/meta wrapper.

Response

Example parameters are ready. Send the request to inspect the live response.

Route reference

Code samples Ready-to-copy requests in 4 languages
Choose a code sample language
curl "https://randomapi.dev/api/cors-preflight/preflight?method=PUT&origin=https%3A%2F%2Fapp.example.com"
const res = await fetch("https://randomapi.dev/api/cors-preflight/preflight?method=PUT&origin=https%3A%2F%2Fapp.example.com");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/cors-preflight/preflight?method=PUT&origin=https%3A%2F%2Fapp.example.com").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/cors-preflight/preflight?method=PUT&origin=https%3A%2F%2Fapp.example.com"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 5
method string required

HTTP method the page requests. Normalized like the Request constructor, which uppercases only DELETE, GET, HEAD, OPTIONS, POST and PUT. Forbidden methods (CONNECT, TRACE, TRACK) are rejected.

allowed: 1 – 64
example: method=PUT
requestHeaders list

Comma-separated 'Name: value' pairs the page sets on the request (max 32 entries, max 2048 characters per value). An entry with no colon means that name with an empty value, and a repeated name models the repeated header-list entries a browser really sends. Because the list itself is comma-separated, a value containing a comma cannot be expressed.

example: requestHeaders=Content-Type:application/json
mode enum

Request mode. Only 'cors' can produce a preflight; the other two are explained in meta.warnings.

default: cors
allowed: cors | no-cors | same-origin
example: mode=cors
credentials enum

Credentials mode of the request. 'include' is the mode that disables every Access-Control-* wildcard: allow-origin, allow-methods, allow-headers and expose-headers. It cannot change whether a preflight is required — nothing in the Fetch Standard makes it — so on /preflight it only echoes back and adds a note, and every wildcard rule it controls is evaluated on /check.

default: same-origin
allowed: omit | same-origin | include
example: credentials=include
origin string

Requesting origin as scheme://host[:port], with no path, query, fragment or trailing slash. It is serialized the way a browser serializes the Origin header before sending it — scheme and domain lowercased, an IPv6 literal compressed to its shortest form, a default port dropped — and the serialization is what appears in the reconstructed preflight.

allowed: ≤ 256
example: origin=https://app.example.com
Universal parameters Shared response and formatting options 4
fields list

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

example: fields=method,mode
exclude list

Return all fields except these (comma-separated).

example: exclude=preflightRequest
pretty boolean

Pretty-print the JSON response.

default: false
example: pretty=true
unwrap boolean

Drop the envelope: return the raw array/object without data/meta wrapper.

default: false
example: unwrap=true
Response schema Fields returned in each record 10
method string

Method token after 'normalize a method', which uppercases only DELETE, GET, HEAD, OPTIONS, POST and PUT.

example: PUT

mode string

Request mode that was evaluated: cors, no-cors or same-origin.

example: cors

credentials string

Credentials mode echoed back. It does not affect whether a preflight is required.

example: same-origin

origin string nullable

Requesting origin after the URL Standard's host serializer — lowercased, IPv6 literal compressed, default port dropped — or null when none was supplied.

example: https://app.example.com

preflightRequired boolean

Whether a browser with a cold preflight cache would send an OPTIONS request first.

example: true

reasons object[]

Conditions that force the preflight: { code, header, detail }. code is one of non-safelisted-method, non-safelisted-header-name, unsafe-content-type, unsafe-header-value-byte, unsafe-range-value, header-value-too-long or safelist-value-size-exceeded; header names the header for the five per-header codes and is null for the two whole-request ones. Empty when no preflight is required.

example: [{"code":"non-safelisted-method","header":null,"detail":"'PUT' is not a CORS-safelisted method; only GET, HEAD and POST avoid a preflight."}]

headers object[]

Per-header verdict: { name, valueLength, forbidden, safelisted, unsafe, note }. safelisted and unsafe are null for forbidden headers. In mode=no-cors the note also says when the Headers guard drops the header outright, which it does to Range even though Range is CORS-safelisted.

example: [{"name":"content-type","valueLength":16,"forbidden":false,"safelisted":false,"unsafe":true,"note":"The Content-Type essence 'application/json' is not safelisted. Only application/x-www-form-urlencoded, multipart/form-data and text/plain avoid a preflight."}]

unsafeRequestHeaderNames string[]

The CORS-unsafe request-header names for this header list: lowercased, de-duplicated and sorted. A preflight puts exactly this list in Access-Control-Request-Headers.

example: ["content-type"]

safelistValueSize integer

Combined byte length of the safelisted header values; above 1024 every safelisted name becomes CORS-unsafe too.

example: 16

preflightRequest object nullable

The CORS headers of the OPTIONS request the browser would send, or null when there is no preflight.

example: {"method":"OPTIONS","headers":[{"name":"Accept","value":"*/*"},{"name":"Access-Control-Request-Method","value":"PUT"},{"name":"Access-Control-Request-Headers","value":"content-type"}]}

Documented examples

Build-generated requests and complete responses
4
Does PUT trigger a preflight?
GET /api/cors-preflight/preflight?method=PUT&origin=https%3A%2F%2Fapp.example.com
{
  "data": {
    "method": "PUT",
    "mode": "cors",
    "credentials": "same-origin",
    "origin": "https://app.example.com",
    "preflightRequired": true,
    "reasons": [
      {
        "code": "non-safelisted-method",
        "header": null,
        "detail": "'PUT' is not a CORS-safelisted method; only GET, HEAD and POST avoid a preflight."
      }
    ],
    "headers": [],
    "unsafeRequestHeaderNames": [],
    "safelistValueSize": 0,
    "preflightRequest": {
      "method": "OPTIONS",
      "headers": [
        {
          "name": "Accept",
          "value": "*/*"
        },
        {
          "name": "Access-Control-Request-Method",
          "value": "PUT"
        },
        {
          "name": "Origin",
          "value": "https://app.example.com"
        }
      ]
    }
  },
  "meta": {
    "endpoint": "cors-preflight",
    "route": "/preflight",
    "params": {
      "method": "PUT",
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
GET with only safelisted headers stays simple
GET /api/cors-preflight/preflight?method=GET&requestHeaders=Accept%3Aapplication%2Fjson
{
  "data": {
    "method": "GET",
    "mode": "cors",
    "credentials": "same-origin",
    "origin": null,
    "preflightRequired": false,
    "reasons": [],
    "headers": [
      {
        "name": "accept",
        "valueLength": 16,
        "forbidden": false,
        "safelisted": true,
        "unsafe": false,
        "note": null
      }
    ],
    "unsafeRequestHeaderNames": [],
    "safelistValueSize": 16,
    "preflightRequest": null
  },
  "meta": {
    "endpoint": "cors-preflight",
    "route": "/preflight",
    "params": {
      "method": "GET",
      "requestHeaders": [
        "Accept:application/json"
      ],
      "mode": "cors",
      "credentials": "same-origin"
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
JSON body forces an OPTIONS round trip
GET /api/cors-preflight/preflight?method=POST&requestHeaders=Content-Type%3Aapplication%2Fjson
{
  "data": {
    "method": "POST",
    "mode": "cors",
    "credentials": "same-origin",
    "origin": null,
    "preflightRequired": true,
    "reasons": [
      {
        "code": "unsafe-content-type",
        "header": "content-type",
        "detail": "The Content-Type essence 'application/json' is not safelisted. Only application/x-www-form-urlencoded, multipart/form-data and text/plain avoid a preflight."
      }
    ],
    "headers": [
      {
        "name": "content-type",
        "valueLength": 16,
        "forbidden": false,
        "safelisted": false,
        "unsafe": true,
        "note": "The Content-Type essence 'application/json' is not safelisted. Only application/x-www-form-urlencoded, multipart/form-data and text/plain avoid a preflight."
      }
    ],
    "unsafeRequestHeaderNames": [
      "content-type"
    ],
    "safelistValueSize": 0,
    "preflightRequest": {
      "method": "OPTIONS",
      "headers": [
        {
          "name": "Accept",
          "value": "*/*"
        },
        {
          "name": "Access-Control-Request-Method",
          "value": "POST"
        },
        {
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Authorization alone is enough to preflight
GET /api/cors-preflight/preflight?method=GET&requestHeaders=Authorization%3ABearer%20t0ken&origin=https%3A%2F%2Fapp.example.com
{
  "data": {
    "method": "GET",
    "mode": "cors",
    "credentials": "same-origin",
    "origin": "https://app.example.com",
    "preflightRequired": true,
    "reasons": [
      {
        "code": "non-safelisted-header-name",
        "header": "authorization",
        "detail": "'authorization' is not on the CORS request-header safelist, which covers only accept, accept-language, content-language, content-type and range."
      }
    ],
    "headers": [
      {
        "name": "authorization",
        "valueLength": 12,
        "forbidden": false,
        "safelisted": false,
        "unsafe": true,
        "note": "'authorization' is not on the CORS request-header safelist, which covers only accept, accept-language, content-language, content-type and range."
      }
    ],
    "unsafeRequestHeaderNames": [
      "authorization"
    ],
    "safelistValueSize": 0,
    "preflightRequest": {
      "method": "OPTIONS",
      "headers": [
        {
          "name": "Accept",
          "value": "*/*"
        },
        {
          "name": "Access-Control-Request-Method",
          "value": "GET"
        },
        {
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}

GET /api/cors-preflight/check

Runs the CORS check and the preflight's method and header rules against the Access-Control-* values you declare, and names the first failing step. Assumes a cross-origin URL in mode 'cors'; the declared headers are applied to both the preflight response and the real response, and the preflight's status code is not modeled.

Live requestRuns against the public API
No key required
GET/api/cors-preflight/check?method=GET&origin=https%3A%2F%2Fapp.example.com&credentials=include&allowOrigin=*

Request parameters

HTTP method of the real request, normalized like the Request constructor. Forbidden methods (CONNECT, TRACE, TRACK) are rejected.

Requesting origin as scheme://host[:port], or the literal 'null'. It is serialized first, exactly as a browser serializes the Origin header — scheme and domain lowercased, an IPv6 literal compressed, a default port dropped — and that serialization is what allowOrigin is compared against byte-for-byte. So origin=https://app.example.com:443 matches allowOrigin=https://app.example.com, but not the other way round: allowOrigin is never rewritten. A path, query, fragment or trailing slash is a 400, not a mismatch.

Comma-separated 'Name: value' pairs the page sets on the request (max 32 entries, max 2048 characters per value). An entry with no colon means that name with an empty value, and a repeated name models the repeated header-list entries a browser really sends. Because the list itself is comma-separated, a value containing a comma cannot be expressed.

Credentials mode of the request. 'include' is the mode that disables every Access-Control-* wildcard: allow-origin, allow-methods, allow-headers and expose-headers. It cannot change whether a preflight is required — nothing in the Fetch Standard makes it — so on /preflight it only echoes back and adds a note, and every wildcard rule it controls is evaluated on /check.

Access-Control-Allow-Origin value your server returns, taken literally — it is the byte string on the wire, so it is never normalized the way 'origin' is. Omit it to model a server that sends no CORS headers at all.

Access-Control-Allow-Methods value: up to 32 entries, each an HTTP method token or the literal '*' (anything else is a 400). Only consulted when a preflight happens; '*' is ignored when the credentials mode is 'include'.

Access-Control-Allow-Headers value: up to 32 entries, each an HTTP field name or the literal '*' (anything else is a 400), matched case-insensitively. '*' never covers Authorization, and is ignored entirely when the credentials mode is 'include'.

Whether the response carries the exact header value Access-Control-Allow-Credentials: true. Only read when the credentials mode is 'include'.

Access-Control-Expose-Headers value: up to 32 entries, each an HTTP field name or the literal '*' (anything else is a 400). The forbidden response-header names Set-Cookie and Set-Cookie2 are never exposed, and '*' is not a wildcard when the credentials mode is 'include'.

Access-Control-Max-Age in seconds. Omitted means 5 seconds, which is the Fetch Standard's default for a preflight response.

Advanced response options4 options

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

Return all fields except these (comma-separated).

Pretty-print the JSON response.

Drop the envelope: return the raw array/object without data/meta wrapper.

Response

Example parameters are ready. Send the request to inspect the live response.

Route reference

Code samples Ready-to-copy requests in 4 languages
Choose a code sample language
curl "https://randomapi.dev/api/cors-preflight/check?method=GET&origin=https%3A%2F%2Fapp.example.com&credentials=include&allowOrigin=*"
const res = await fetch("https://randomapi.dev/api/cors-preflight/check?method=GET&origin=https%3A%2F%2Fapp.example.com&credentials=include&allowOrigin=*");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/cors-preflight/check?method=GET&origin=https%3A%2F%2Fapp.example.com&credentials=include&allowOrigin=*").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/cors-preflight/check?method=GET&origin=https%3A%2F%2Fapp.example.com&credentials=include&allowOrigin=*"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 10
method string required

HTTP method of the real request, normalized like the Request constructor. Forbidden methods (CONNECT, TRACE, TRACK) are rejected.

allowed: 1 – 64
example: method=PUT
origin string required

Requesting origin as scheme://host[:port], or the literal 'null'. It is serialized first, exactly as a browser serializes the Origin header — scheme and domain lowercased, an IPv6 literal compressed, a default port dropped — and that serialization is what allowOrigin is compared against byte-for-byte. So origin=https://app.example.com:443 matches allowOrigin=https://app.example.com, but not the other way round: allowOrigin is never rewritten. A path, query, fragment or trailing slash is a 400, not a mismatch.

allowed: ≤ 256
example: origin=https://app.example.com
requestHeaders list

Comma-separated 'Name: value' pairs the page sets on the request (max 32 entries, max 2048 characters per value). An entry with no colon means that name with an empty value, and a repeated name models the repeated header-list entries a browser really sends. Because the list itself is comma-separated, a value containing a comma cannot be expressed.

example: requestHeaders=Content-Type:application/json
credentials enum

Credentials mode of the request. 'include' is the mode that disables every Access-Control-* wildcard: allow-origin, allow-methods, allow-headers and expose-headers. It cannot change whether a preflight is required — nothing in the Fetch Standard makes it — so on /preflight it only echoes back and adds a note, and every wildcard rule it controls is evaluated on /check.

default: same-origin
allowed: omit | same-origin | include
example: credentials=include
allowOrigin string

Access-Control-Allow-Origin value your server returns, taken literally — it is the byte string on the wire, so it is never normalized the way 'origin' is. Omit it to model a server that sends no CORS headers at all.

allowed: ≤ 256
example: allowOrigin=https://app.example.com
allowMethods list

Access-Control-Allow-Methods value: up to 32 entries, each an HTTP method token or the literal '*' (anything else is a 400). Only consulted when a preflight happens; '*' is ignored when the credentials mode is 'include'.

example: allowMethods=GET,PUT
allowHeaders list

Access-Control-Allow-Headers value: up to 32 entries, each an HTTP field name or the literal '*' (anything else is a 400), matched case-insensitively. '*' never covers Authorization, and is ignored entirely when the credentials mode is 'include'.

example: allowHeaders=Content-Type,Authorization
allowCredentials boolean

Whether the response carries the exact header value Access-Control-Allow-Credentials: true. Only read when the credentials mode is 'include'.

default: false
example: allowCredentials=true
exposeHeaders list

Access-Control-Expose-Headers value: up to 32 entries, each an HTTP field name or the literal '*' (anything else is a 400). The forbidden response-header names Set-Cookie and Set-Cookie2 are never exposed, and '*' is not a wildcard when the credentials mode is 'include'.

example: exposeHeaders=X-Request-Id
maxAge int

Access-Control-Max-Age in seconds. Omitted means 5 seconds, which is the Fetch Standard's default for a preflight response.

allowed: 0 – 31536000
example: maxAge=600
Universal parameters Shared response and formatting options 4
fields list

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

example: fields=method,origin
exclude list

Return all fields except these (comma-separated).

example: exclude=varyOriginAdvice
pretty boolean

Pretty-print the JSON response.

default: false
example: pretty=true
unwrap boolean

Drop the envelope: return the raw array/object without data/meta wrapper.

default: false
example: unwrap=true
Response schema Fields returned in each record 15
method string

Normalized method token used for the preflight method check.

example: PUT

origin string

Requesting origin after serialization (lowercased host, IPv6 literal compressed, default port dropped) — the exact bytes Access-Control-Allow-Origin is compared against. allowOrigin itself is never rewritten.

example: https://app.example.com

credentials string

Credentials mode of the request: omit, same-origin or include.

example: include

preflightRequired boolean

Whether the Access-Control-Allow-Methods and Access-Control-Allow-Headers rules apply at all.

example: true

allowed boolean

Whether every modeled step passes, so the browser would hand the response to JavaScript.

example: false

failedStep string nullable

The first failing step, or null when nothing fails: cors-check-allow-origin-missing, cors-check-allow-origin-mismatch, cors-check-allow-credentials-not-true, preflight-method-not-allowed, preflight-non-wildcard-header-not-allowed or preflight-unsafe-header-not-allowed.

example: cors-check-allow-origin-mismatch

failureDetail string nullable

Sentence explaining the failing step and how to fix it, or null when the request is allowed.

example: Access-Control-Allow-Origin: * is compared byte-for-byte with 'https://app.example.com' once the credentials mode is 'include', and the wildcard never matches. Echo the exact origin instead and add Access-Control-Allow-Credentials: true.

originAllowed boolean

Whether Access-Control-Allow-Origin covers this origin, honoring the rule that '*' stops matching once credentials are included.

example: false

credentialsAllowed boolean nullable

Whether credentials would be honored: Access-Control-Allow-Credentials is true AND the origin is named exactly. Null when the credentials mode attaches no credentials.

example: false

methodAllowed boolean

Whether the preflight method check passes. Always true without a preflight, and true for GET/HEAD/POST even when Access-Control-Allow-Methods omits them.

example: true

headersAllowed object[]

One verdict per CORS-unsafe request-header name: { name, allowed, nonWildcard, reason }. Empty without a preflight.

example: [{"name":"authorization","allowed":false,"nonWildcard":true,"reason":"'authorization' is a CORS-non-wildcard request-header name, so Access-Control-Allow-Headers must name it explicitly; '*' never covers it."}]

readableResponseHeaders string[]

Response header names JavaScript could read: the seven CORS-safelisted names plus Access-Control-Expose-Headers entries. Empty when allowed is false, because a blocked response exposes nothing.

example: ["cache-control","content-language","content-length","content-type","expires","last-modified","pragma"]

allResponseHeadersReadable boolean

True only when the response is allowed and Access-Control-Expose-Headers is '*' with a credentials mode other than 'include', which exposes every response header except the forbidden response-header names Set-Cookie and Set-Cookie2.

example: false

maxAge object nullable

Preflight cache lifetime { seconds, source, note }; source is 'header' or 'default'. Null unless a preflight both happens and succeeds.

example: {"seconds":5,"source":"default","note":"Browsers cap this at their own upper limit, which the Fetch Standard leaves unspecified, and may drop the entry sooner. Preflight cache entries are keyed by origin, URL, credentials mode and the individual method or header name."}

varyOriginAdvice string

Caching advice about Vary: Origin for this configuration. Vary is not part of the CORS check and never changes 'allowed'.

example: Access-Control-Allow-Origin: * is a constant, so Vary: Origin is not required — just make sure the header is sent on every response for this resource, including non-CORS ones.

Documented examples

Build-generated requests and complete responses
4
Wildcard origin with credentials is blocked
GET /api/cors-preflight/check?method=GET&origin=https%3A%2F%2Fapp.example.com&credentials=include&allowOrigin=*
{
  "data": {
    "method": "GET",
    "origin": "https://app.example.com",
    "credentials": "include",
    "preflightRequired": false,
    "allowed": false,
    "failedStep": "cors-check-allow-origin-mismatch",
    "failureDetail": "Access-Control-Allow-Origin: * is compared byte-for-byte with 'https://app.example.com' once the credentials mode is 'include', and the wildcard never matches. Echo the exact origin instead and add Access-Control-Allow-Credentials: true.",
    "originAllowed": false,
    "credentialsAllowed": false,
    "methodAllowed": true,
    "headersAllowed": [],
    "readableResponseHeaders": [],
    "allResponseHeadersReadable": false,
    "maxAge": null,
    "varyOriginAdvice": "Access-Control-Allow-Origin: * is a constant, so Vary: Origin is not required — just make sure the header is sent on every response for this resource, including non-CORS ones."
  },
  "meta": {
    "endpoint": "cors-preflight",
    "route": "/check",
    "params": {
      "method": "GET",
      "origin": "https://app.example.com",
      "credentials": "include",
      "allowOrigin": "*",
      "allowCredentials": false
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Preflighted PUT with a JSON body is allowed
GET /api/cors-preflight/check?method=PUT&origin=https%3A%2F%2Fapp.example.com&requestHeaders=Content-Type%3Aapplication%2Fjson&allowOrigin=https%3A%2F%2Fapp.example.com&allowMethods=GET%2CPUT&allowHeaders=Content-Type&maxAge=600
{
  "data": {
    "method": "PUT",
    "origin": "https://app.example.com",
    "credentials": "same-origin",
    "preflightRequired": true,
    "allowed": true,
    "failedStep": null,
    "failureDetail": null,
    "originAllowed": true,
    "credentialsAllowed": null,
    "methodAllowed": true,
    "headersAllowed": [
      {
        "name": "content-type",
        "allowed": true,
        "nonWildcard": false,
        "reason": "Listed by name in Access-Control-Allow-Headers."
      }
    ],
    "readableResponseHeaders": [
      "cache-control",
      "content-language",
      "content-length",
      "content-type",
      "expires",
      "last-modified",
      "pragma"
    ],
    "allResponseHeadersReadable": false,
    "maxAge": {
      "seconds": 600,
      "source": "header",
      "note": "Browsers cap this at their own upper limit, which the Fetch Standard leaves unspecified, and may drop the entry sooner. Preflight cache entries are keyed by origin, URL, credentials mode and the individual method or header name."
    },
    "varyOriginAdvice": "Access-Control-Allow-Origin names a single origin. If your server picks that value per request, send Vary: Origin so a shared cache cannot hand one origin's response to another. This is an HTTP caching concern: Vary is not part of the CORS check and never changes 'allowed'."
  },
  "meta": {
    "endpoint": "cors-preflight",
    "route": "/check",
    …
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Wildcard allow-headers never covers Authorization
GET /api/cors-preflight/check?method=GET&origin=https%3A%2F%2Fapp.example.com&requestHeaders=Authorization%3ABearer%20t0ken&allowOrigin=https%3A%2F%2Fapp.example.com&allowHeaders=*
{
  "data": {
    "method": "GET",
    "origin": "https://app.example.com",
    "credentials": "same-origin",
    "preflightRequired": true,
    "allowed": false,
    "failedStep": "preflight-non-wildcard-header-not-allowed",
    "failureDetail": "'authorization' is a CORS-non-wildcard request-header name, so Access-Control-Allow-Headers must name it explicitly; '*' never covers it.",
    "originAllowed": true,
    "credentialsAllowed": null,
    "methodAllowed": true,
    "headersAllowed": [
      {
        "name": "authorization",
        "allowed": false,
        "nonWildcard": true,
        "reason": "'authorization' is a CORS-non-wildcard request-header name, so Access-Control-Allow-Headers must name it explicitly; '*' never covers it."
      }
    ],
    "readableResponseHeaders": [],
    "allResponseHeadersReadable": false,
    "maxAge": null,
    "varyOriginAdvice": "Access-Control-Allow-Origin names a single origin. If your server picks that value per request, send Vary: Origin so a shared cache cannot hand one origin's response to another. This is an HTTP caching concern: Vary is not part of the CORS check and never changes 'allowed'."
  },
  "meta": {
    "endpoint": "cors-preflight",
    "route": "/check",
    "params": {
      "method": "GET",
      "origin": "https://app.example.com",
      "requestHeaders": [
        "Authorization:Bearer t0ken"
      ],
      "credentials": "same-origin",
      "allowOrigin": "https://app.example.com",
      "allowHeaders": [
        "*"
      ],
      "allowCredentials": false
    },
    "generatedAt": "2026-07-30T15:14:08.000Z"
  }
}
Expose a custom response header to JavaScript
GET /api/cors-preflight/check?method=GET&origin=https%3A%2F%2Fapp.example.com&allowOrigin=*&exposeHeaders=X-Request-Id%2CX-Rate-Limit-Remaining
{
  "data": {
    "method": "GET",
    "origin": "https://app.example.com",
    "credentials": "same-origin",
    "preflightRequired": false,
    "allowed": true,
    "failedStep": null,
    "failureDetail": null,
    "originAllowed": true,
    "credentialsAllowed": null,
    "methodAllowed": true,
    "headersAllowed": [],
    "readableResponseHeaders": [
      "cache-control",
      "content-language",
      "content-length",
      "content-type",
      "expires",
      "last-modified",
      "pragma",
      "x-rate-limit-remaining",
      "x-request-id"
    ],
    "allResponseHeadersReadable": false,
    "maxAge": null,
    "varyOriginAdvice": "Access-Control-Allow-Origin: * is a constant, so Vary: Origin is not required — just make sure the header is sent on every response for this resource, including non-CORS ones."
  },
  "meta": {
    "endpoint": "cors-preflight",
    "route": "/check",
    "params": {
      "method": "GET",
      "origin": "https://app.example.com",
      "credentials": "same-origin",
      "allowOrigin": "*",
      "allowCredentials": false,
      "exposeHeaders": [
        "X-Request-Id",
        "X-Rate-Limit-Remaining""generatedAt": "2026-07-30T15:14:08.000Z"
  }
}

About this API

Coverage & behavior

Evaluates the CORS configuration you paste into the query string. This endpoint never contacts your origin, opens a socket or reads a live response — it is pure set and string logic over the values you supply, so it works from CI, from behind a firewall and against a server that does not exist yet. Every other CORS tester answers by issuing a real request, which is exactly what makes them unusable in a pipeline.

/preflight answers "does this request send an OPTIONS first, and why?". In mode: cors a preflight is required when the method is not a CORS-safelisted method (only GET, HEAD and POST are) or when at least one header is CORS-unsafe. A header is CORS-unsafe when its name is outside the safelist (Accept, Accept-Language, Content-Language, Content-Type, Range), when its value is longer than 128 bytes, when a Content-Type essence is anything other than application/x-www-form-urlencoded, multipart/form-data or text/plain, when the value carries a byte the safelist forbids for that name, or when the safelisted values together exceed 1024 bytes, which promotes every safelisted name to the unsafe set. Since each safelisted value is already capped at 128 bytes, that budget only bites when a name repeats — list a name more than once to model the repeated header-list entries a browser really sends. The route also reconstructs the preflight: OPTIONS with Access-Control-Request-Method and a lowercased, sorted, comma-joined Access-Control-Request-Headers. That preflight is always sent without cookies or HTTP authentication, so do not put auth in front of your OPTIONS handler. In mode: no-cors there is no preflight to reconstruct: the request-no-cors Headers guard keeps only Accept, Accept-Language, Content-Language and Content-Type, and only while their values stay CORS-safelisted. Everything else is dropped silently before the request leaves — including Range, which is CORS-safelisted and so never forces a preflight, but is not a no-CORS-safelisted request-header name. The route lists exactly which names you would lose.

/check answers "would the browser hand this response to my JavaScript?". It runs the CORS check (Access-Control-Allow-Origin compared byte-for-byte, then Access-Control-Allow-Credentials), then the preflight's method and header rules, and reports the first failing step by name. It encodes the two rules that break most configurations: Access-Control-Allow-Origin: * stops matching the moment the credentials mode is include, and Access-Control-Allow-Headers: * never covers Authorization, which is the sole CORS-non-wildcard request-header name. It also returns which response headers JavaScript could read and the preflight cache lifetime, where an absent Access-Control-Max-Age means five seconds.

The comparison is deliberately asymmetric, because the two sides come from different places. Your origin is serialized first — lowercased host, IPv6 literal compressed to its shortest form, default port dropped — because that is what the browser actually puts in the Origin header. Your allowOrigin is taken exactly as you typed it, because that is the byte string your server sends. So origin=https://app.example.com:443 matches allowOrigin=https://app.example.com, while allowOrigin=https://app.example.com:443 does not match origin=https://app.example.com — which is precisely the bug an explicit default port causes in production. An origin with a path, query, fragment or trailing slash is not a serialized origin at all and is rejected with a 400 rather than reported as a mismatch.

Scope and honesty. We model the WHATWG Fetch Standard, not any individual browser: console wording, legacy quirks and the browser-specific cap on Access-Control-Max-Age are all out of scope, and browsers differ. Header names are matched case-insensitively; method tokens are compared byte-for-byte, as the specification requires. Private Network Access and the Access-Control-Allow-Private-Network family are deliberately excluded because that specification is still moving. Forbidden request-headers such as Origin, Cookie and Content-Length are reported and then excluded, because the browser sets them itself. Both routes assume a cross-origin URL and, on /check, mode: cors; a same-origin URL never involves CORS at all.

We also assume the request's use-CORS-preflight flag is unset, and we do not model it. That flag is set by an XMLHttpRequest with a listener on its upload object, or by a streaming request body, and it changes two answers: such a request preflights even when it is a plain POST with text/plain (so /preflight would say preflightRequired: false where the browser preflights), and in that path an absent Access-Control-Allow-Methods still admits the request's own method (so /check would say methodAllowed: false where the browser allows it). For everything fetch() does without a stream body, the flag is unset and these routes are exact.

Four further limits are worth stating plainly. preflightRequired describes a cold cache — a live preflight cache entry suppresses the OPTIONS request. We do not model the preflight response's status code, and the Fetch Standard also requires it to be an ok status, so an OPTIONS handler that answers 401 or 404 fails the preflight no matter how good its headers are. /check applies the one set of Access-Control-* values you supply to both the preflight response and the real response; a server that answers them differently needs two calls. And because requestHeaders is a comma-separated parameter, a header value containing a comma cannot be expressed, and values are limited to visible ASCII, SP and HT. Vary: Origin is returned as caching advice only; it is not part of the CORS check.

An allowed: true verdict means the headers you declared would satisfy the CORS check. It is not evidence that your server actually sends them, and it is not a security audit: CORS restricts what a page's JavaScript may read, it is not an authorization mechanism, and it never protects a server from a non-browser client.

Use it for

  • Decide whether adding a request header will cost you an extra OPTIONS round trip
  • Explain why Access-Control-Allow-Origin: * stops working the moment cookies are involved
  • Assert an API gateway's CORS configuration in CI without issuing a live cross-origin request
  • Work out which response headers a frontend can read through Access-Control-Expose-Headers

Frequently asked questions

Does a PUT request always trigger a CORS preflight?

Yes, in mode: cors. Only GET, HEAD and POST are CORS-safelisted methods, so PUT, PATCH and DELETE always preflight — /preflight?method=PUT reconstructs the OPTIONS request the browser sends.

Can I use Access-Control-Allow-Origin: * with credentials?

No. Once the credentials mode is include, the CORS check compares the header byte-for-byte with your origin and * never matches, so the response is blocked. /check reports failedStep: cors-check-allow-origin-mismatch.

Does Access-Control-Allow-Headers: * cover Authorization?

No. Authorization is the CORS-non-wildcard request-header name, so it must be listed by name. /check reports failedStep: preflight-non-wildcard-header-not-allowed.

Why does Content-Type: application/json cause a preflight?

Only the essences application/x-www-form-urlencoded, multipart/form-data and text/plain are safelisted, and the parser is strict rather than forgiving. Any other essence makes Content-Type a CORS-unsafe request-header, which forces the preflight.

Why does mode: no-cors drop my request headers?

The request-no-cors Headers guard keeps only Accept, Accept-Language, Content-Language and Content-Type, and only with CORS-safelisted values. Everything else is dropped silently — including Range, which never forces a preflight but is not a no-CORS-safelisted request-header name. /preflight?mode=no-cors lists the names you lose.

Does this API send a request to my server?

Never. It evaluates only the values in your query string, so it runs from CI and offline. It cannot confirm that your server really sends the headers you declared, and it is not a security audit.

Standards & references