All resources
Web Security9 min read

CORS and preflight requests explained for real APIs

Understand origins, credentials, preflight caching, and the server headers that make browser-to-API calls predictable.

A practical PingFlow guide for developers working at the boundary between systems.

At a glance

Key takeaways

  • Origin is not the same as site or host
  • Know when the browser sends a preflight
  • Credentials change the allowed-origin rules
In this guide

Origin is not the same as site or host

A browser origin is the combination of scheme, host, and port. https://app.example.com and https://api.example.com are different origins even though they share a registrable domain. http and https are different origins, as are two ports on the same host. CORS controls whether a browser script from one origin may read a response from another. It does not prevent the request from reaching the server, and it does not replace authentication or authorization.

Start by recording the exact page origin, request URL, method, headers, credentials mode, and response headers. A server that allows https://app.example.com does not automatically allow a local development origin or a preview deployment. Avoid reflecting the Origin header blindly. An attacker can send an origin of their choice, and a reflected value combined with credentials can expose private responses.

Know when the browser sends a preflight

A simple cross-origin request can be sent without a preflight when its method, headers, and content type fit the browser's simple request rules. Other requests trigger an OPTIONS preflight. The browser asks whether the target origin permits the method and requested headers, and the server responds with Access-Control-Allow-Origin, Access-Control-Allow-Methods, and Access-Control-Allow-Headers. The browser then decides whether to send the actual request or expose the response.

A preflight is a browser protocol exchange, not an application endpoint that should require a user session in the same way as the actual request. Handle OPTIONS consistently at the edge or API gateway, return the required headers, and avoid redirecting it to a login page. A 301 or HTML error response can make a valid API call look like a CORS failure because the browser hides the detailed response from script.

Credentials change the allowed-origin rules

When a browser request includes cookies or other credentials, the server must opt in with Access-Control-Allow-Credentials: true. The allowed origin cannot be the wildcard when credentials are included. Return the exact approved origin and add Vary: Origin so a shared cache does not serve a response authorized for one origin to another. The cookie's SameSite, Secure, and domain rules still apply; CORS does not make a cookie cross-site by itself.

Do not enable credentials just because a frontend uses cookies somewhere else. Public endpoints can usually use anonymous, non-credentialed requests and a wildcard policy if the response is truly public. Narrow credentialed routes to known origins and methods. Review preview environments, customer domains, and local development separately. A growing allowlist should have an owner and a removal process.

Allow only the methods and headers you need

A broad CORS policy is difficult to audit. List the methods the client uses, expose only response headers the client must read, and allow request headers intentionally. Authorization, content type, and tracing headers often need explicit permission. Do not copy every requested header into Access-Control-Allow-Headers. That makes a typo look like permission and expands the surface for future code that should have undergone review.

CORS headers are not a validation shortcut. The server must still authenticate the caller, validate input, enforce tenant boundaries, and protect state-changing actions. A non-browser client can ignore CORS entirely. If an API needs to be private from non-browser callers, use network controls and authentication, not a missing Access-Control-Allow-Origin header.

Cache preflight decisions carefully

Access-Control-Max-Age lets a browser cache a successful preflight for a period. A long value reduces latency but delays policy changes reaching clients. A short value increases OPTIONS traffic. Choose a value that fits the release and security model, and invalidate or tolerate the old policy during a rollout. Proxies may cache preflight responses too, so include Vary headers for request method and requested headers when required by the deployment path.

During debugging, inspect both OPTIONS and the actual request. A successful preflight does not guarantee that the response includes Access-Control-Allow-Origin, and a successful server request does not guarantee that the browser will expose the body. Use browser network diagnostics and server logs together. Never fix a CORS issue by disabling browser security or adding a public proxy without evaluating what data it exposes.

Make error responses observable

A browser often reports a generic CORS error when the real failure is a 401, 500, redirect, or missing header. Add a correlation ID to server logs and return a safe, consistent error body with the same CORS policy for expected API errors. Do not expose stack traces or credentials while trying to make debugging easier. A server-side request from the same origin can help isolate whether the problem is CORS or application logic.

Record the request origin, method, preflight result, allowed policy branch, and response status in structured logs. Avoid recording arbitrary Origin values as metric labels because an attacker can create unbounded cardinality. Alert on a sudden rise in disallowed origins or preflight failures, but distinguish a broken deployment from an unexpected client.

Test the browser contract

Test allowed and disallowed origins, simple and preflighted methods, requested headers, credentialed cookies, redirects, error statuses, caching, and a preview environment. Verify that a shared cache cannot reuse an origin-specific response. Test an OPTIONS request through the same load balancer, authentication middleware, and WAF as production. Include a route that must never be available cross-origin and confirm that server authorization still rejects it.

CORS becomes predictable when the team treats it as a precise browser contract. Define origins, credentials, methods, headers, and cache lifetimes explicitly. Keep the policy separate from authentication and authorization, then observe the preflight and actual request as two related but distinct events.

Keep an origin matrix in the repository

List production, staging, preview, local, customer, and administrative origins with their allowed methods, headers, credentials, and expiry. Review the matrix when a deployment domain changes. Automated tests should read the same intended policy and exercise one allowed and one disallowed origin. This prevents an emergency allowlist edit from becoming permanent configuration nobody can explain.

When a browser reports a CORS failure, capture the page origin, preflight request, response headers, actual response status, and proxy hop. Do not add a wildcard until you know whether the route is public and non-credentialed. A precise origin matrix is faster to debug and safer to operate than a permissive header that hides the real boundary.

Implementation example

Define an origin matrix for production, staging, preview, local, and administrative callers. Return an exact allowed origin when credentials are used, allow only required methods and headers, and handle OPTIONS with the same route policy as the actual request. CORS is a browser boundary, not an authorization layer.

http
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Content-Type, Authorization
Vary: Origin

Verify and troubleshoot

Test one allowed origin, one disallowed origin, a simple request, a credentialed request, an OPTIONS preflight, a custom header, and a redirect or error response. Capture Origin, Access-Control-Request-Method, response headers, and proxy behavior. A browser console error is incomplete evidence; inspect the preflight and actual response separately.

Operations and recovery

Keep the origin matrix reviewed with domain changes and remove preview hosts after their lifecycle. Alert on unexpected origin configuration and avoid wildcard credentials. If a CORS change is urgent, narrow it to the affected route and origin, record an expiry, and preserve server-side authorization checks unchanged.

References and further reading

Use the Fetch standard's CORS processing model, MDN preflight guidance, and the API gateway's header policy. Document whether the route is public, credentialed, or intentionally inaccessible from browsers.

Keep exploring