HTTP OPTIONS Method: CORS, Preflight & API Discovery

Learn how the OPTIONS method works, why browsers use it for CORS preflight, and how to implement it correctly in your REST API.

Last Updated:

What is the HTTP OPTIONS Method?

OPTIONS
Safe Idempotent

The HTTP OPTIONS method describes the communication options for a target resource. It asks the server: "What can I do with this resource, and how?" The server responds with headers that list the allowed methods, accepted headers, and CORS permissions — but takes no action on the resource itself.

OPTIONS is defined in RFC 9110 (HTTP Semantics) as a safe and idempotent method — it does not modify server state, and calling it multiple times always produces the same response.

Two Primary Use Cases

  • CORS Preflight: Browsers automatically send OPTIONS before certain cross-origin requests to check whether the server permits the request. This is by far the most common use of OPTIONS in modern web APIs.
  • API Discovery / Introspection: Clients can send OPTIONS to discover what HTTP methods a resource supports, using the Allow response header.

In practice, most developers encounter OPTIONS exclusively through CORS preflight errors. Understanding exactly what OPTIONS does — and what your server must return — is the key to resolving those errors permanently.

CORS Preflight Requests Explained

What is CORS?

CORS (Cross-Origin Resource Sharing) is a browser security mechanism that restricts web pages from making HTTP requests to a different origin (domain, protocol, or port) than the one that served the page. Without CORS, a malicious website could silently make authenticated requests to your API using the user's credentials.

When a web application at https://app.example.com wants to call an API at https://api.example.com, the browser treats this as a cross-origin request and enforces CORS rules.

When Does the Browser Send a Preflight?

Not every cross-origin request triggers a preflight. The browser only sends an OPTIONS preflight for non-simple requests. A request is non-simple if it meets any of these conditions:

  • Uses a method other than GET, HEAD, or POST
  • Uses POST with a Content-Type other than application/x-www-form-urlencoded, multipart/form-data, or text/plain
  • Includes custom request headers (e.g., Authorization, X-Custom-Header)
  • Uses credentials (cookies, HTTP authentication)

In practice, virtually every REST API call from a frontend application triggers a preflight, because they use Content-Type: application/json and/or an Authorization header.

Step-by-Step Preflight Flow

STEP 1: Browser wants to make this actual request
  POST https://api.example.com/api/v1/users
  Origin: https://app.example.com
  Content-Type: application/json
  Authorization: Bearer token123

STEP 2: Before sending it, browser sends preflight
  OPTIONS https://api.example.com/api/v1/users
  Origin: https://app.example.com
  Access-Control-Request-Method: POST
  Access-Control-Request-Headers: Content-Type, Authorization

STEP 3: Server responds with CORS permissions
  HTTP/1.1 204 No Content
  Access-Control-Allow-Origin: https://app.example.com
  Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
  Access-Control-Allow-Headers: Content-Type, Authorization
  Access-Control-Max-Age: 86400

STEP 4: Browser checks: is POST allowed? Yes.
         Are Content-Type and Authorization allowed? Yes.
         Origin is approved? Yes.

STEP 5: Browser proceeds with the actual POST request.

Full Preflight Request and Response Example

Preflight Request (sent by browser automatically)

OPTIONS /api/v1/users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
Connection: keep-alive

Preflight Response (your server must return this)

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Vary: Origin
Content-Length: 0

OPTIONS Response Headers Reference

The following headers control what cross-origin requests are allowed. Understanding each one is essential for correctly implementing CORS in your API.

Header Direction Description
Origin Request The origin of the requesting page. Set by the browser automatically — never by JavaScript.
Access-Control-Request-Method Preflight Request The HTTP method that will be used in the actual request. Sent in the preflight only.
Access-Control-Request-Headers Preflight Request Comma-separated list of request headers that will be sent in the actual request.
Access-Control-Allow-Origin Response The origin(s) permitted to access the resource. Use * for public APIs, or echo the specific Origin value for credentialed requests.
Access-Control-Allow-Methods Preflight Response Comma-separated list of HTTP methods the resource allows for cross-origin requests. Example: GET, POST, PUT, DELETE, OPTIONS.
Access-Control-Allow-Headers Preflight Response Comma-separated list of headers that may be used in the actual request. Must include any custom headers the client wants to send.
Access-Control-Max-Age Preflight Response How long (in seconds) the browser can cache this preflight response. Reduces preflight overhead. Maximum is browser-dependent (Chrome: 7200s, Firefox: 86400s).
Access-Control-Allow-Credentials Response Set to true to allow the browser to expose the response when the request includes credentials (cookies, HTTP auth). Cannot be combined with Access-Control-Allow-Origin: *.
Access-Control-Expose-Headers Response Lists response headers that are safe to expose to JavaScript. By default, only CORS-safelisted headers (Cache-Control, Content-Type, etc.) are accessible.
Allow Response Lists all HTTP methods supported by the resource. Used for API discovery, not CORS specifically.
Vary: Origin Response Tells CDNs and proxies that the response varies based on the Origin header. Essential when returning different Access-Control-Allow-Origin values per request.

Implementing OPTIONS in Your API

Wildcard Origin vs Specific Origins

The Access-Control-Allow-Origin header has two modes:

  • Wildcard (*): Allows any origin. Suitable for truly public APIs with no authentication. Simple to implement but cannot be used with Access-Control-Allow-Credentials: true.
  • Specific origin: Echo the exact value of the request's Origin header (after validating it against your allowlist). Required for credentialed requests. Must also include Vary: Origin to prevent cache poisoning.

Public API (wildcard — no credentials)

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: 86400

Private API (specific origin — with credentials)

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 3600
Vary: Origin

Using Access-Control-Max-Age for Performance

Without caching, the browser sends an OPTIONS preflight before every non-simple cross-origin request. On a page that makes 20 API calls, that means 40 HTTP requests instead of 20. The Access-Control-Max-Age header tells the browser to cache the preflight result for a specified number of seconds, eliminating redundant preflight requests.

  • Value of 86400 (24 hours) is a good default for stable APIs
  • Use lower values (e.g., 300) during development or when CORS config changes frequently
  • Browser maximums: Chrome enforces 7200s, Firefox 86400s, Safari varies

Security: Never Use Wildcard with Credentials

The combination of Access-Control-Allow-Origin: * and Access-Control-Allow-Credentials: true is explicitly invalid per the CORS specification. Browsers will reject such responses. This restriction exists by design: if you allow credentials (cookies, auth tokens), you must be specific about which origins you trust. Always validate the request's Origin against an allowlist and echo the approved origin in the response.

Wrong — invalid combination that browsers reject:

Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true    <-- INVALID with wildcard

Correct — specific origin with credentials:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Vary: Origin

OPTIONS for API Discovery

Beyond CORS, OPTIONS serves a second purpose: API introspection. A client can send OPTIONS to any URL and receive the Allow header listing all HTTP methods the resource accepts.

API Discovery Request

OPTIONS /api/v1/users/123

Response:

HTTP/1.1 204 No Content
Allow: GET, PUT, PATCH, DELETE, OPTIONS
Content-Length: 0

Collection Resource Discovery

OPTIONS /api/v1/users

Response:

HTTP/1.1 204 No Content
Allow: GET, POST, OPTIONS
Content-Length: 0

The Allow header is also returned automatically by the server in a 405 Method Not Allowed response — telling clients which methods the resource does support. This makes it easy for API clients to recover from incorrect method choices without consulting documentation.

More advanced API introspection systems (like those built on OpenAPI or JSON:API) may return richer metadata in the OPTIONS response body, but the Allow header is the standard, universally-supported mechanism.

Common OPTIONS / CORS Mistakes

Mistake 1: Returning 404 or 405 for OPTIONS Requests

If your router does not explicitly handle OPTIONS, it may return a 404 Not Found or 405 Method Not Allowed response. The browser receives this before it can check CORS headers, causing the preflight to fail and the actual request to be blocked. Every route that accepts cross-origin requests must handle OPTIONS — either explicitly or via middleware that intercepts OPTIONS before routing.

Mistake 2: Wildcard Origin with Credentials

As covered above, Access-Control-Allow-Origin: * and Access-Control-Allow-Credentials: true cannot be combined. Browsers reject this combination silently (from the JavaScript perspective) — the fetch or XMLHttpRequest fails with a CORS error, and the network tab shows the response was received but blocked. Always use a specific origin when credentials are involved.

Mistake 3: Missing Vary: Origin Header

When your server returns different values of Access-Control-Allow-Origin depending on the request's Origin, you must also return Vary: Origin. Without it, a CDN or proxy might cache a response with one origin's CORS headers and serve it to a different origin — causing that second origin to see incorrect or absent CORS headers. This produces intermittent CORS failures that are very hard to debug.

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Vary: Origin    <-- Required whenever Access-Control-Allow-Origin varies per request

Mistake 4: Access-Control-Max-Age Too Low or Too High

  • Too low (e.g., 0 or 1): The browser preflights every single request, doubling your API call count and adding latency. This is a hidden performance problem that only shows up under realistic traffic.
  • Too high: If you change your CORS policy (add a new allowed header, change allowed origins), users' browsers may cache the old preflight response for hours. During incident response, this means users are stuck with the old policy until their cache expires.
  • Recommendation: Use 86400 (24 hours) for stable production APIs, 300-3600 for APIs with changing CORS policies.

Mistake 5: Not Including Required Headers in Access-Control-Allow-Headers

If your client sends a custom header — like Authorization, X-Request-ID, or X-API-Version — that header must appear in Access-Control-Allow-Headers. If it is missing, the browser blocks the actual request even after a successful preflight. Check the Access-Control-Request-Headers value in the preflight request to see exactly what the client is asking to send.

Frequently Asked Questions

What is a CORS preflight request?

A CORS preflight request is an HTTP OPTIONS request that the browser automatically sends before certain cross-origin requests. It asks the server whether the actual request — with its method and headers — is allowed from the requesting origin. The server responds with CORS headers indicating what is permitted. If the server approves, the browser proceeds with the actual request. If not, the browser blocks the request and the JavaScript code receives a CORS error.

Does every API request trigger an OPTIONS preflight?

No. The browser only sends a preflight OPTIONS request for non-simple requests. Simple requests — GET or POST with only basic headers and a Content-Type of application/x-www-form-urlencoded, multipart/form-data, or text/plain — do not trigger a preflight. In practice, the vast majority of REST API calls from frontend applications do trigger preflights, because they use Content-Type: application/json and/or an Authorization header. The Access-Control-Max-Age header lets you cache the preflight result to avoid repeated OPTIONS calls.

What should my server return for an OPTIONS request?

For a CORS preflight, return: HTTP 204 No Content (or 200 OK), Access-Control-Allow-Origin with the allowed origin, Access-Control-Allow-Methods listing permitted methods, Access-Control-Allow-Headers listing permitted request headers, Access-Control-Max-Age to cache the preflight, and Vary: Origin if you echo specific origins. For API discovery (non-CORS OPTIONS), return the Allow header listing all methods supported by the resource.

Why am I getting CORS errors even though I handle OPTIONS?

The most common causes are: (1) You return Access-Control-Allow-Origin: * but also set Access-Control-Allow-Credentials: true — this combination is invalid. (2) The Origin in the request does not match the value in Access-Control-Allow-Origin. (3) The request uses a header or method not listed in your CORS response headers. (4) You are missing Vary: Origin, causing a CDN to serve a cached response with the wrong origin. (5) Your OPTIONS handler returns the wrong status code, or CORS middleware runs after routing and never reaches OPTIONS requests that 404 first.