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.
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
Reports preflightRequired plus the exact condition that forces it, and reconstructs the OPTIONS request the browser would send. Nothing is fetched.
| Parameter | Type | Default & allowed | Description |
|---|---|---|---|
method
required
| string | allowed: 1 – 64 example: PUT | 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. |
requestHeaders | list | example: Content-Type:application/json | 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. |
mode | enum | default: cors allowed: cors | no-cors | same-origin example: cors | Request mode. Only 'cors' can produce a preflight; the other two are explained in meta.warnings. |
credentials | enum | default: same-origin allowed: omit | same-origin | include example: include | 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. |
origin | string | allowed: ≤ 256 example: https://app.example.com | 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. |
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.
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.
mode enum Request mode. Only 'cors' can produce a preflight; the other two are explained in meta.warnings.
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.
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.
| Parameter | Type | Default & allowed | Description |
|---|---|---|---|
fields | list | example: method,mode | Return only these fields (comma-separated). Mutually exclusive with 'exclude'. |
exclude | list | example: preflightRequest | Return all fields except these (comma-separated). |
pretty | boolean | default: false example: true | Pretty-print the JSON response. |
unwrap | boolean | default: false example: true | Drop the envelope: return the raw array/object without data/meta wrapper. |
fields list Return only these fields (comma-separated). Mutually exclusive with 'exclude'.
exclude list Return all fields except these (comma-separated).
pretty boolean Pretty-print the JSON response.
unwrap boolean Drop the envelope: return the raw array/object without data/meta wrapper.
| Field | Type | Description | Example |
|---|---|---|---|
method | string | Method token after 'normalize a method', which uppercases only DELETE, GET, HEAD, OPTIONS, POST and PUT. | PUT |
mode | string | Request mode that was evaluated: cors, no-cors or same-origin. | cors |
credentials | string | Credentials mode echoed back. It does not affect whether a preflight is required. | same-origin |
origin nullable | string | Requesting origin after the URL Standard's host serializer — lowercased, IPv6 literal compressed, default port dropped — or null when none was supplied. | https://app.example.com |
preflightRequired | boolean | Whether a browser with a cold preflight cache would send an OPTIONS request first. | 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. | [{"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. | [{"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. | ["content-type"] |
safelistValueSize | integer | Combined byte length of the safelisted header values; above 1024 every safelisted name becomes CORS-unsafe too. | 16 |
preflightRequest nullable | object | The CORS headers of the OPTIONS request the browser would send, or null when there is no preflight. | {"method":"OPTIONS","headers":[{"name":"Accept","value":"*/*"},{"name":"Access-Control-Request-Method","value":"PUT"},{"name":"Access-Control-Request-Headers","value":"content-type"}]} |
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"}]}
/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"
}
} /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"
}
} /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"
}
} /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"
}
} 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.
| Parameter | Type | Default & allowed | Description |
|---|---|---|---|
method
required
| string | allowed: 1 – 64 example: PUT | HTTP method of the real request, normalized like the Request constructor. Forbidden methods (CONNECT, TRACE, TRACK) are rejected. |
origin
required
| string | allowed: ≤ 256 example: https://app.example.com | 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. |
requestHeaders | list | example: Content-Type:application/json | 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 | enum | default: same-origin allowed: omit | same-origin | include example: include | 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. |
allowOrigin | string | allowed: ≤ 256 example: https://app.example.com | 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. |
allowMethods | list | example: GET,PUT | 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'. |
allowHeaders | list | example: Content-Type,Authorization | 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'. |
allowCredentials | boolean | default: false example: true | Whether the response carries the exact header value Access-Control-Allow-Credentials: true. Only read when the credentials mode is 'include'. |
exposeHeaders | list | example: X-Request-Id | 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'. |
maxAge | int | allowed: 0 – 31536000 example: 600 | Access-Control-Max-Age in seconds. Omitted means 5 seconds, which is the Fetch Standard's default for a preflight response. |
method string required HTTP method of the real request, normalized like the Request constructor. Forbidden methods (CONNECT, TRACE, TRACK) are rejected.
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.
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.
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.
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.
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'.
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'.
allowCredentials boolean Whether the response carries the exact header value Access-Control-Allow-Credentials: true. Only read when the credentials mode is 'include'.
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'.
maxAge int Access-Control-Max-Age in seconds. Omitted means 5 seconds, which is the Fetch Standard's default for a preflight response.
| Parameter | Type | Default & allowed | Description |
|---|---|---|---|
fields | list | example: method,origin | Return only these fields (comma-separated). Mutually exclusive with 'exclude'. |
exclude | list | example: varyOriginAdvice | Return all fields except these (comma-separated). |
pretty | boolean | default: false example: true | Pretty-print the JSON response. |
unwrap | boolean | default: false example: true | Drop the envelope: return the raw array/object without data/meta wrapper. |
fields list Return only these fields (comma-separated). Mutually exclusive with 'exclude'.
exclude list Return all fields except these (comma-separated).
pretty boolean Pretty-print the JSON response.
unwrap boolean Drop the envelope: return the raw array/object without data/meta wrapper.
| Field | Type | Description | Example |
|---|---|---|---|
method | string | Normalized method token used for the preflight method check. | 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. | https://app.example.com |
credentials | string | Credentials mode of the request: omit, same-origin or include. | include |
preflightRequired | boolean | Whether the Access-Control-Allow-Methods and Access-Control-Allow-Headers rules apply at all. | true |
allowed | boolean | Whether every modeled step passes, so the browser would hand the response to JavaScript. | false |
failedStep nullable | string | 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. | cors-check-allow-origin-mismatch |
failureDetail nullable | string | Sentence explaining the failing step and how to fix it, or null when the request is allowed. | 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. | false |
credentialsAllowed nullable | boolean | 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. | 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. | true |
headersAllowed | object[] | One verdict per CORS-unsafe request-header name: { name, allowed, nonWildcard, reason }. Empty without a preflight. | [{"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. | ["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. | false |
maxAge nullable | object | Preflight cache lifetime { seconds, source, note }; source is 'header' or 'default'. Null unless a preflight both happens and succeeds. | {"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'. | 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. |
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.
/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"
}
} /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"
}
} /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"
}
} /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"
}
} 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.
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.
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.
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.
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.
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.
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.
Complete pinned IANA HTTP field-name registry with status, Structured Field type, references, search and case-insensitive lookup.
The complete current IANA HTTP Method Registry with safe and idempotent flags — filter the list or look up one canonical method token.
The complete IANA HTTP status code registry with practical descriptions, defining RFCs and a retryable flag — list by category or look up a single code.
Parse RFC 9110 Accept media ranges and deterministically select the best available representation using specificity, parameters and q weights.
Parse absolute URLs or resolve relative references with the WHATWG URL algorithm — canonical hosts, ordered query pairs and no credential leakage.
Parse a Content-Security-Policy and see whether a URL would be allowed or blocked, which directive decided, and the whole default-src fallback chain.