405 Method Not Allowed (tested)
405 Method Not Allowed means the URL exists, but not for the method you used. A good server also sends an Allow header listing the methods it accepts. In our tests, many servers do not send 405 at all for this mistake.
1. What 405 means
The server found the resource. It does not accept your method there. Example: you sent POST to a URL that only answers GET.
The spec says a 405 response must include an Allow header. Read it first. It tells you which methods will work.
2. Not every server says 405
We sent the same wrong method to three servers. Only one replied 405.
| Server | Route | POST response | Allow header |
|---|---|---|---|
| Express 5.2.1 | app.get('/x') | 404, body Cannot POST /x | none |
| Flask 3.1.3 | methods=['GET'] | 405 METHOD NOT ALLOWED | HEAD, OPTIONS, GET |
| python3 -m http.server | static file | 501 Unsupported method ('POST') | none |
So if you search only for "405", you may miss the real problem. A 404 Cannot POST from Express is the same mistake.
3. Express: 404, not 405
app.get('/x', (req, res) => res.send('hello'));
$ curl -si -X POST localhost:8993/x
HTTP/1.1 404 Not Found
Content-Type: text/html; charset=utf-8
Content-Length: 141
<pre>Cannot POST /x</pre>
Express does answer HEAD and OPTIONS for a GET route by itself:
$ curl -sI localhost:8993/x
HTTP/1.1 200 OK
Content-Length: 5
$ curl -si -X OPTIONS localhost:8993/x
HTTP/1.1 200 OK
Allow: GET, HEAD
Content-Type: text/plain
GET, HEAD
To send a proper 405, add a catch-all after the real handlers:
app.route('/y')
.get((req, res) => res.send('y'))
.all((req, res) => res.set('Allow', 'GET, HEAD').sendStatus(405));
$ curl -si -X POST localhost:8993/y
HTTP/1.1 405 Method Not Allowed
Allow: GET, HEAD
Content-Type: text/plain; charset=utf-8
Method Not Allowed
Note: .all() also catches OPTIONS. In our run, OPTIONS /y returned 405 too. If you need CORS preflight on that path, handle OPTIONS before the catch-all.
4. Flask: a real 405 with Allow
@app.route('/x', methods=['GET'])
def x(): return 'hello'
$ curl -si -X POST localhost:8994/x
HTTP/1.1 405 METHOD NOT ALLOWED
Allow: HEAD, OPTIONS, GET
Content-Type: text/html; charset=utf-8
<title>405 Method Not Allowed</title>
Flask adds HEAD and OPTIONS for you. OPTIONS /x returned 200 with the same Allow value and an empty body. Fix: add the method, e.g. methods=['GET', 'POST'], or send to the right route.
5. Static file servers: POST to a file
A common cause: a form or fetch() posts to an .html file or a static host. Static servers do not run code, so they refuse POST.
$ curl -si -X POST localhost:8995/index.html
HTTP/1.0 501 Unsupported method ('POST')
<p>Error code explanation: 501 - Server does not support this operation.</p>
Python's http.server says 501, not 405. OPTIONS also got 501. From the nginx docs (not measured): nginx answers 405 for POST to a static file. Fix: point the form action or fetch URL at your API, not at the page.
6. Redirects that turn POST into GET
If your POST hits a redirect, the client may follow it with GET. Then the target can reply 405 (or your handler never sees the body). We posted form data with curl -L -d a=1 to a redirect that lands on an echo route:
| Redirect | Method at the target |
|---|---|
| 301 | GET |
| 302 | GET |
| 307 | POST |
| 308 | POST |
$ curl -sL -d a=1 localhost:8993/old # 301
method=GET
$ curl -sL -d a=1 localhost:8993/old308 # 308
method=POST
Careful: with -X POST, curl forced POST on every hop, even 301 and 302. That hides the bug. Test with -d and no -X.
A missing or extra trailing slash is a classic trigger (from framework docs, not measured here). /api/users redirects to /api/users/, and the POST becomes a GET. Fix: call the exact final URL, or use 307/308 for redirects. See how to check a redirect chain.
7. CORS preflight getting 404 or 405
For a cross-origin PUT, DELETE or JSON POST, the browser first sends OPTIONS. If that fails, the console says "Response to preflight request doesn't pass access control check". We sent a preflight with curl:
$ curl -si -X OPTIONS -H 'Origin: http://localhost:3000' \
-H 'Access-Control-Request-Method: PUT' localhost:8993/y
HTTP/1.1 405 Method Not Allowed
Allow: GET, HEAD
Express /x and Flask /x replied 200 to the same preflight, but with no Access-Control-Allow-Origin. The browser would still block it. http.server replied 501. Fix: answer OPTIONS with 204 and the CORS headers. See Access-Control-Max-Age and allowing multiple origins.
8. Other causes (from docs, not measured)
- IIS: the WebDAV module can grab PUT and DELETE and return 405. Remove WebDAV for that site.
- Hosting for static sites often has no POST support at all. Use a function or API endpoint.
- A wrong
Content-Typegives a different error. See 415 Unsupported Media Type.
9. Quick checklist
- Run
curl -si -X METHOD URLand read the status andAllow. - Run
curl -si -X OPTIONS URLto see what the route accepts. - Check for a 301/302 before the error with
curl -sIL. - Confirm the URL points at your API, not a static file.
- For browser-only failures, check the OPTIONS preflight.
Frequently asked questions
Why does Express return 404 instead of 405?
Express 5.2.1 does not check other methods on the same path. POST to a GET-only route fell through to the default handler: 404 with Cannot POST /x. Add .all() on app.route() to send 405 with an Allow header.
What should the Allow header contain?
The methods the URL accepts, comma-separated. Flask sent HEAD, OPTIONS, GET for a GET-only route. Express's automatic OPTIONS reply sent GET, HEAD.
Why does my POST arrive as GET?
A 301 or 302 redirect. In our test, curl -L -d followed both with GET. 307 and 308 kept POST. Fix the URL, often a trailing slash, or use 307/308.
Why does python -m http.server give 501 instead of 405?
It only implements GET and HEAD. Any other method, including POST and OPTIONS, got 501 Unsupported method in our run.
Is 405 a client or server problem?
Usually a client sending the wrong method or URL. It is a server problem when a route is missing a method, or a module like WebDAV on IIS blocks it.
Written with AI assistance. Measured on 2026-10-02 with curl 8.14.1, Node.js 24.21.0 with Express 5.2.1 on 127.0.0.1:8993, Flask 3.1.3 / Werkzeug 3.1.9 on Python 3.13.5 on port 8994, and python3 -m http.server on port 8995. Every status, header and body below is copied from those runs. The nginx, IIS and trailing-slash notes come from their docs and were not measured. PlainHeaders is our extension; curl, Express, Flask and Python are free.