# 405 Method Not Allowed

> 405 Method Not Allowed means the URL exists but not for that method. Read the Allow header, then fix redirects turning POST into GET, static hosting and WAFs.

Source: https://howhttpworks.com/status-codes/405
Last reviewed: 2026-10-04

> **TL;DR:** 405 Method Not Allowed means the URL exists but does not accept the HTTP method you used, and the response must list the accepted methods in an `Allow` header. The usual real-world causes are a form or fetch using the wrong verb, a redirect that turned POST into GET, static hosting that only serves GET, and a proxy or CORS layer blocking PUT/DELETE.

RFC 9110 section 15.5.6: the method "is known by the origin server but not supported by the target resource", and the server "MUST generate an Allow header field". Compare [501](https://howhttpworks.com/status-codes/501), which means the server does not recognize the method at all, and [404](https://howhttpworks.com/status-codes/404), where the path itself is unknown.

```http
HTTP/1.1 405 Method Not Allowed
Allow: GET, HEAD, OPTIONS
Content-Type: application/json

{"error":"method_not_allowed","allowed":["GET","HEAD","OPTIONS"]}
```

## Who sent it?

| Evidence | Layer |
| --- | --- |
| `Allow:` header lists methods and body comes from your framework (`Method Not Allowed`, Flask: `The method is not allowed for the requested URL.`) | Application route does not define that method |
| nginx page `405 Not Allowed` | nginx serving a static file (nginx static handling accepts only GET/HEAD, and POST to a static file returns 405) |
| `405 Method Not Allowed` with `Server: AmazonS3` | S3 website endpoint or bucket policy blocks the verb |
| `Server: cloudflare`, `CF-Ray` | Cloudflare Pages/Workers or a WAF rule restricting methods |
| IIS: `HTTP Error 405.0 - Method Not Allowed` or `405.0 ... The page you are looking for cannot be displayed because an invalid method (HTTP verb) is being used` | IIS; frequently WebDAV module intercepting PUT/DELETE |
| Spring: `Request method 'POST' is not supported` (`HttpRequestMethodNotSupportedException`) | Spring MVC mapping |
| Django: `Method Not Allowed (POST): /path/` in the server log | View class lacks `post()` |

## Fix it, most common first

1. **Read the `Allow` header** and use one of those methods: `curl -si -X POST URL | grep -i '^allow'`, or ask with `curl -si -X OPTIONS URL`.
2. **A redirect changed your method.** [301](https://howhttpworks.com/status-codes/301) and [302](https://howhttpworks.com/status-codes/302) make most clients re-issue POST as GET, and the target (say a GET-only page) answers 405. Typical triggers: `http` to `https` redirect, `www` to apex, trailing slash redirect. Call the final URL directly, or have the server use [307/308](https://howhttpworks.com/status-codes/308), which preserve the method.
3. **Static hosting.** POSTing a form to a page on GitHub Pages, S3 static hosting, Netlify without a function, or nginx `try_files` serving a file returns 405. Point the form to a real endpoint.
4. **A framework route only handles some verbs.** Add the handler. Next.js App Router needs an exported function named exactly `POST`, `PUT` and so on in `route.ts`; Express needs `app.post(...)`, and a `GET`-only `app.get` yields `Cannot POST /path` (404 from Express, not 405).
5. **Proxy or WAF restrictions.** `limit_except GET { deny all; }` in nginx returns 403, not 405; but ModSecurity and CDN rules often return 405 for PUT, DELETE and PATCH. Allow the verbs for that path.
6. **CORS preflight.** An `OPTIONS` request that your server answers with 405 breaks cross-origin PUT/DELETE/JSON POST. Handle `OPTIONS` and return `Access-Control-Allow-Methods` (see [CORS preflight debugging](https://howhttpworks.com/debug/cors-preflight)).
7. **HTML forms only do GET and POST.** `<form method="put">` is treated as GET. Use `fetch`, or a hidden `_method` override your framework understands.
8. **HEAD or OPTIONS rejected.** Monitoring tools and crawlers send HEAD; a handler that omits it returns 405 and trips health checks. Most servers should accept HEAD wherever GET is accepted (RFC 9110 section 9.1).

## Fix by stack

### nginx

```nginx
# POST to a location that serves static files => 405. Route it to the app instead.
location /api/ {
    proxy_pass http://127.0.0.1:3000;
}

# Optional workaround for legacy POST-to-static
error_page 405 =200 $uri;
```

### Apache

```apache
# Restrict verbs: denied methods get 403, not 405, unless you return 405 yourself
<LimitExcept GET POST OPTIONS>
    Require all denied
</LimitExcept>
```

### Express / Node.js

```javascript
app.route('/api/items')
  .get(listItems)
  .post(createItem)
  .all((req, res) => {
    res.set('Allow', 'GET, POST, OPTIONS').status(405).json({ error: 'method_not_allowed' })
  })
```

### Next.js, Flask, Django

```typescript
// app/api/items/route.ts : Next.js App Router
export async function GET() { return Response.json([]) }
export async function POST(req: Request) { return Response.json(await req.json(), { status: 201 }) }
// Any other method gets 405 with an automatic Allow header.
```

Flask returns 405 with an `Allow` header automatically when `methods=[...]` does not include the verb. Django class-based views return `HttpResponseNotAllowed` using `http_method_not_allowed`; DRF returns `{"detail":"Method \"DELETE\" not allowed."}`.

## Reproduce and verify

```bash
curl -si -X DELETE https://api.example.com/items | sed -n '1p;/^[Aa]llow/p'
curl -si -X OPTIONS https://api.example.com/items | grep -i '^allow'
curl -sv -L -d 'a=1' http://example.com/form 2>&1 | grep -E '^> (POST|GET)'
```

The last command shows whether the second request in a redirect chain is still POST. Plain `curl -L -d` switches to GET after 301/302 unless you pass `--post301`/`--post302`, which is also what browsers do.

## Related

405 lives next to [404](https://howhttpworks.com/status-codes/404), [501](https://howhttpworks.com/status-codes/501), and [400](https://howhttpworks.com/status-codes/400). Method semantics: [GET](https://howhttpworks.com/methods/get), [POST](https://howhttpworks.com/methods/post), [PUT](https://howhttpworks.com/methods/put), [DELETE](https://howhttpworks.com/methods/delete), [OPTIONS](https://howhttpworks.com/methods/options). Try a bad verb in the [request builder](https://howhttpworks.com/tools/playground).
