1. Library
  2. HTTP and the Web
  3. HTTP Methods

Updated 1 day ago

The HTTP OPTIONS Method

The OPTIONS method asks servers a simple question: "What can I do with this resource?" But that's the boring answer.

The interesting truth: OPTIONS exists because browsers don't trust the web pages they're running. When JavaScript on one website tries to talk to a different server, the browser steps in and asks that server: "Hey, some code wants to send you a request. Is that okay?"

This is the preflight request—and it's OPTIONS doing the asking.

What OPTIONS Actually Returns

OPTIONS requests information about what's allowed:

OPTIONS /api/articles/12345 HTTP/1.1
Host: api.example.com
→
HTTP/1.1 204 No Content
Allow: GET, HEAD, PUT, DELETE, OPTIONS
Access-Control-Allow-Methods: GET, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400

The Allow header lists every method the server supports for this resource. The Access-Control-* headers are for CORS—they tell browsers what cross-origin requests are permitted.

The Preflight Dance

Here's where OPTIONS earns its keep.

When JavaScript on https://app.example.com wants to make this request:

fetch('https://api.example.com/articles/123', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token123'
  },
  body: JSON.stringify({ title: 'Updated Title' })
});

The browser doesn't just send it. It can't. This is a cross-origin request that could modify data. The browser is protecting the server from JavaScript it doesn't trust—including JavaScript the user chose to run.

So the browser automatically sends a preflight:

OPTIONS /articles/123 HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization

Translation: "JavaScript from app.example.com wants to PUT with these headers. Allow it?"

The server responds:

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

"Yes, that origin can PUT with those headers. And you can remember this answer for 24 hours."

Only then does the browser send the actual PUT request.

Why Some Requests Skip Preflight

Not every cross-origin request triggers OPTIONS. "Simple requests" go through directly:

  • Method: GET, HEAD, or POST
  • Headers: Only basics like Accept, Accept-Language, Content-Type
  • Content-Type: Only application/x-www-form-urlencoded, multipart/form-data, or text/plain

These mirror what HTML forms could always do—before JavaScript, before fetch, before APIs. The browser treats them as safe because they've always been possible.

Anything else triggers preflight:

// No preflight (simple GET)
fetch('https://api.example.com/data');

// Preflight (DELETE isn't simple)
fetch('https://api.example.com/data', { method: 'DELETE' });

// Preflight (Authorization header isn't simple)
fetch('https://api.example.com/data', {
  headers: { 'Authorization': 'Bearer token' }
});

// Preflight (JSON content type isn't simple)
fetch('https://api.example.com/data', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ data: 'value' })
});

Caching Preflight Responses

Access-Control-Max-Age: 86400 tells browsers to cache the preflight response. Without this, every PUT or DELETE would require two round trips—OPTIONS then the actual request.

But browsers impose their own limits1:

BrowserMaximum Cache
Firefox24 hours
Chrome2 hours
Safari5 minutes

Set your max-age to 86400 (24 hours) and let browsers apply their limits. The default is just 5 seconds.

Implementing OPTIONS

Most frameworks handle OPTIONS automatically:

// Express with CORS middleware
const cors = require('cors');

app.use(cors({
  origin: 'https://app.example.com',
  methods: ['GET', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  maxAge: 86400
}));

If you're implementing it manually:

app.options('/api/articles/:id', (req, res) => {
  res.set('Allow', 'GET, HEAD, PUT, DELETE, OPTIONS');
  res.set('Access-Control-Allow-Origin', req.get('Origin'));
  res.set('Access-Control-Allow-Methods', 'GET, PUT, DELETE');
  res.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.set('Access-Control-Max-Age', '86400');
  res.status(204).end();
});

OPTIONS for Server-Wide Discovery

You can send OPTIONS to * to query the entire server:

OPTIONS * HTTP/1.1
Host: api.example.com
→
HTTP/1.1 200 OK
Allow: GET, POST, PUT, DELETE, HEAD, OPTIONS

This asks "what methods does this server support in general?" rather than "what can I do with this specific resource?"

Security: The Real Dangers

OPTIONS should respect authentication. Don't reveal that secret resources exist:

OPTIONS /admin/secret-data HTTP/1.1
→ 401 Unauthorized (if not authenticated)
→ 403 Forbidden (if not authorized)

Never return Allow: GET, PUT, DELETE for resources the user shouldn't know about.

The most dangerous CORS mistake isn't using a wildcard—browsers block Access-Control-Allow-Origin: * combined with Access-Control-Allow-Credentials: true by design2. The real danger is origin reflection: dynamically echoing back whatever Origin header you receive.

// DANGEROUS: Origin reflection
res.set('Access-Control-Allow-Origin', req.get('Origin'));
res.set('Access-Control-Allow-Credentials', 'true');

This lets any website make authenticated requests to your API. Always validate origins against an allowlist:

const allowedOrigins = [
  'https://app.example.com',
  'https://mobile.example.com'
];

const origin = req.get('Origin');
if (allowedOrigins.includes(origin)) {
  res.set('Access-Control-Allow-Origin', origin);
  res.set('Access-Control-Allow-Credentials', 'true');
}

Debugging CORS with OPTIONS

When CORS fails mysteriously, OPTIONS is your diagnostic tool:

curl -X OPTIONS https://api.example.com/resource \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: PUT" \
  -H "Access-Control-Request-Headers: Content-Type" \
  -i

This shows you exactly what the server allows—or why it's rejecting your preflight.

Key Takeaways

  • OPTIONS asks "what can I do with this resource?" and returns allowed methods in the Allow header.
  • Its real purpose is CORS preflight: the browser asking servers for permission before letting JavaScript make cross-origin requests.
  • Simple requests (basic GET, HEAD, POST with simple headers) skip preflight. Everything else triggers it.
  • Cache preflight responses with Access-Control-Max-Age to reduce round trips. Browsers cap this at 5 minutes (Safari) to 24 hours (Firefox).
  • Don't leak information—respect authentication in OPTIONS responses just like any other method.
  • The browser is the gatekeeper. OPTIONS is how servers tell the browser what to allow through.

Frequently Asked Questions About HTTP OPTIONS

Sources

Sources

  1. Access-Control-Max-Age - HTTP | MDN

  2. CORS and the Access-Control-Allow-Origin response header | PortSwigger

Was this page helpful?

😔
🤨
😃