HTTP 405 is Method Not Allowed: the URL exists, but it doesn’t accept this method. RFC 9110 section 15.5.6 requires an Allow header listing the methods it does accept. An empty Allow is legal and means none at the moment (section 10.2.1), for instance when the resource is switched off by configuration.
The exchange
DELETE /api/invoices/88 HTTP/1.1
HTTP/1.1 405 Method Not Allowed
Allow: GET, HEAD
Content-Type: application/problem+json
{"title": "Method Not Allowed", "status": 405, "detail": "Invoices can't be deleted; void them instead"}
The client should switch to a listed method, not retry.
Who sends it for you, and who doesn’t
- Flask (through Werkzeug), Starlette and FastAPI, Django’s class-based views and
@require_http_methods, and Go’sServeMuxwith Go 1.22 method patterns like"GET /items/{id}"all answer 405 withAllowwhen the path matches and the method doesn’t. - nginx answers
405 Not Allowedto aPOSTon a static file. - Express doesn’t. An unmatched method falls through to the 404 handler as
Cannot DELETE /api/invoices/88. - nginx’s
limit_exceptdoesn’t either. Itsdeny allanswers 403.
405, 404 or 501
path unknown 404
path known, method not supported there 405 + Allow
method the server doesn't implement for any URL 501 Not Implemented
RFC 9110 section 15.6.2 assigns 501 to a method the server doesn’t recognize at all. A 404 for a wrong method sends developers hunting for a typo in a URL that is correct.
CORS preflights
A browser’s preflight is an OPTIONS request, and it fails unless the answer is a 2xx. A route that answers OPTIONS with 405 shows up in the console as a CORS error, never as a 405. Handle OPTIONS before method routing, or let a CORS middleware answer it.
Caching
405 is heuristically cacheable (RFC 9110 section 15.1). That only bites for a 405 to GET or HEAD, since caches don’t store responses to PUT or DELETE: a GET endpoint you enable later can keep answering 405 from a cache until it expires. Send Cache-Control with it if that matters.
In your stack
router.route('/api/invoices/:id')
.get(showInvoice) // Express routes GET also answer HEAD
.all((req, res) => res.set('Allow', 'GET, HEAD').sendStatus(405));
@require_http_methods(["GET", "HEAD"]) # Django: 405 with Allow for anything else
def invoice(request, pk): ...
mux.HandleFunc("GET /api/invoices/{id}", showInvoice) // Go 1.22+: a DELETE here gets 405 and Allow