How HTTP Works

Request header

> Access-Control-Request-Method: PUT

Access-Control-Request-Method Header

Learn how Access-Control-Request-Method tells servers which HTTP method will be used in the actual CORS request. Essential for preflight request handling.

Reviewed 5 min readintermediate2 sourcesTry itMarkdown
Direction
Request
Category
CORS
Spec
Fetch Standard
JS can set it
No: forbidden header name
On this page

TL;DR: It’s the browser asking, during a CORS preflight, “may I send a DELETE (or PATCH, or PUT) here?” The preflight itself is an OPTIONS request, and the server answers with Access-Control-Allow-Methods. The browser adds this header on its own; your JavaScript can’t set it.

What is Access-Control-Request-Method?

Before a cross-origin request that isn’t “simple”, the browser sends an OPTIONS preflight. This header names the one method the real request will use, and the server replies with Access-Control-Allow-Methods. It only appears on the preflight. It isn’t a method override, and the actual DELETE, PUT, or POST doesn’t carry it. MDN lists it as a forbidden request header, which is why page scripts can’t set it.

How Access-Control-Request-Method Works

A preflight and its response, trimmed to the relevant headers (all examples on this page are trimmed):

OPTIONS /api/users/123 HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: authorization
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, DELETE
Access-Control-Allow-Headers: Authorization

The browser can now send the DELETE with an Authorization header, as long as the rest of the CORS checks pass. The preflight goes to the exact URL of the real request, so permission granted for /api/users says nothing about /api/users/123.

Syntax

Access-Control-Request-Method: PATCH

It holds exactly one method, never a comma-separated list. Method names are case-sensitive, so stick to the usual uppercase names on both sides.

When Preflight is Triggered

GET, HEAD, and POST are the CORS-safelisted methods. Anything else, including PUT, PATCH, DELETE, and a real OPTIONS request, gets a preflight. Safelisted methods need one too when the request carries non-safelisted headers, and a JSON POST is the case everyone hits. CONNECT, TRACE, and TRACK aren’t preflighted at all; Fetch refuses to send them. See Fetch’s method definitions.

Browser operationIntended method in preflightReason
JSON POSTPOSTapplication/json Content-Type
GET with bearer tokenGETAuthorization header
DELETE without custom headersDELETEMethod is not safelisted
Plain GET without custom headersNo preflight needed on these groundsSafelisted method and headers

Other details of the request can trigger a preflight too. When debugging, look at the headers your fetch wrapper actually sent, not just the options you passed it.

Common Examples

A POST with Content-Type: application/json gets a preflight with Access-Control-Request-Method: POST. So seeing this header doesn’t mean the real request is a PUT or DELETE.

After a successful preflight, the real request goes out with its own method, here PUT:

PUT /api/users/123 HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Content-Type: application/json

{"theme":"dark"}

That request has no Access-Control-Request-Method header. Your route sees req.method === 'PUT', and that’s the method to act on. The preflight header only matters while answering OPTIONS.

Real-World Scenarios

Called from a different origin, this request triggers a preflight unless the browser already has a matching permission cached:

await fetch('https://api.example.com/api/users/123', {
  method: 'PATCH',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ theme: 'dark' })
})

In the Network panel you’ll see the OPTIONS request with Access-Control-Request-Method: PATCH and Access-Control-Request-Headers: content-type. The browser sends it for you; there’s no need to write a second fetch.

Server-Side Handling

For an existing Express app with cors installed:

const cors = require('cors')
app.use('/api/users', cors({
  origin: 'https://app.example.com',
  methods: ['GET', 'PATCH'],
  allowedHeaders: ['Content-Type']
}))

Register it before your authentication and route handlers. The middleware answers preflights and adds the CORS headers to the real responses that follow.

If you write the preflight handler yourself, check the Access-Control-Request-Method header. req.method is always OPTIONS there, so comparing it with PATCH gets you nowhere. This handler allows only PATCH with Content-Type on the record route:

app.options('/api/users/:id', (req, res) => {
  res.vary('Origin')
  if (req.get('Origin') !== 'https://app.example.com') {
    return res.status(403).end()
  }
  if (req.get('Access-Control-Request-Method') !== 'PATCH') {
    return res.status(403).end()
  }
  const names = (req.get('Access-Control-Request-Headers') || '')
    .split(',').map((name) => name.trim().toLowerCase()).filter(Boolean)
  if (names.some((name) => name !== 'content-type')) {
    return res.status(403).end()
  }
  res.set({
    'Access-Control-Allow-Origin': 'https://app.example.com',
    'Access-Control-Allow-Methods': 'PATCH',
    'Access-Control-Allow-Headers': 'Content-Type'
  }).status(204).end()
})

Use this instead of the cors middleware, not alongside it, or the two OPTIONS handlers will fight. Your PATCH handler still has to set Access-Control-Allow-Origin on its own response. This policy leaves out cookies and Authorization on purpose; if your route needs them, update the whole policy, not just one header.

Getting Access-Control-Request-Method right

Preflights never carry cookies or the bearer token, even when the real request will. So if OPTIONS requires a logged-in session, every preflight fails. Answer OPTIONS without user authentication, and check the user’s permissions on the real request. Treat Origin as which site is calling, not as a user’s role.

Collection and record routes usually allow different methods: /api/users might take POST while /api/users/123 takes PATCH or DELETE. Match the route first, then pick its method list. And remember two users on the same frontend share an Origin but not permissions, so the Origin never decides who can delete a record.

Common Errors and Solutions

If your logs show OPTIONS but no PATCH, the preflight failed. Check its status, allowed origin, method list, and allowed headers. If the PATCH does show up but JavaScript still rejects, look at the PATCH response. It’s probably missing Access-Control-Allow-Origin, which happens a lot on error responses.

Watch for redirects on OPTIONS, like a trailing-slash fix or a bounce to the login page. Look at the first response, not just where it ended up; the preflight should get its answer from the requested URL directly. A 200 that returns your SPA’s HTML without CORS headers fails too.

Testing

curl -i -X OPTIONS https://api.example.com/api/users/123 \
  -H 'Origin: https://app.example.com' \
  -H 'Access-Control-Request-Method: PATCH' \
  -H 'Access-Control-Request-Headers: content-type'

curl will happily send this header, but it doesn’t enforce CORS, so confirm with a real browser request. A good OPTIONS response also tells you nothing about whether the PATCH handler exists or works.

To compare how the server answers for each method, without making any real writes:

for method in GET POST PATCH DELETE; do
  curl -sS -D - -o /dev/null -X OPTIONS \
    https://api.example.com/api/users/123 \
    -H 'Origin: https://app.example.com' \
    -H "Access-Control-Request-Method: $method" \
    -H 'Access-Control-Request-Headers: content-type'
done

Every request in that loop is OPTIONS, so nothing gets deleted or changed. One more thing to know: browsers cache preflight results separately. If you don’t see an OPTIONS request in DevTools, the permission may already be cached. Try a fresh URL or wait out the max-age before deciding the browser skipped the check.

Method-Specific Patterns

Switching GET to POST won’t avoid a preflight if you still send JSON or Authorization. In the other direction, a short Access-Control-Allow-Methods list won’t stop a plain form POST, because that never gets preflighted. Enforce allowed methods and authentication in the endpoint itself.

Frequently asked questions

What is Access-Control-Request-Method?

It is the header in a CORS preflight that tells the server which method the real request will use. The browser adds it to the OPTIONS request, and the server says yes through Access-Control-Allow-Methods and the other CORS response headers.

When is this header sent?

On every CORS preflight. That covers PUT, DELETE and PATCH, and also a POST that needs preflight because of its headers, such as a JSON Content-Type.

How should servers handle this header?

If you allow it, answer the OPTIONS request with a 2xx that names the allowed origin, method and headers. Then authenticate and authorize the real request when it arrives.

What is the difference from Access-Control-Allow-Methods?

Access-Control-Request-Method is one method, sent by the browser in the preflight request. Access-Control-Allow-Methods is the list the server sends back. Only the browser can set the request header; your JavaScript cannot.

Sources

  1. MDN Web Docs: Access-Control-Request-Methoddeveloper.mozilla.org
  2. Fetch Standard: CORSfetch.spec.whatwg.org

Keep going

Browse /search