Understanding HTTP 405 Method Not Allowed: The Required Allow Header and Why It's Not a 404
405 Method Not Allowed is one of the few status codes where the RFC doesn't just define the meaning — it mandates a specific header you have to send along with it. Most implementations skip that requirement, which turns a genuinely useful error ("here's what you can do at this URL") into a dead end that just says "no." This post covers what 405 actually promises, the Allow header it's required to carry, how it differs from 404 and 501, and how to return it correctly — including an NGINX gotcha that trips people up more than you'd expect.
What Is a 405?
A 405 tells the client that the URL exists and the server understood the HTTP method — it just doesn't support that method for this particular resource.
The 405 (Method Not Allowed) status code indicates that the method received in the request-line is known by the origin server but not supported by the target resource. The origin server MUST generate an Allow header field in a 405 response containing a list of the target resource's currently supported methods. — RFC 9110, Section 15.5.6
In plain English: you used the wrong HTTP method for this endpoint. The resource exists, but it doesn't support the method you tried — and the RFC doesn't leave the Allow header as a nice-to-have. It's a MUST: the server is required to tell you which methods are supported (e.g., Allow: GET, HEAD) in the same response.
Don't forget the Allow header — it's required by the RFC and helps clients understand what methods they should use. For example, if a resource is read-only, return 405 for
POST/PUT/DELETEwithAllow: GET, HEAD, OPTIONS.
The Allow Header Isn't Optional
This is the detail that separates a correct 405 from a merely functional one. A 405 without an Allow header technically violates the spec — it tells the client "wrong method" without telling it the right one, which defeats the entire point of the code existing separately from a generic 400.
The mental model: 405 is supposed to be self-correcting. A client (or a developer debugging in the browser) that hits a 405 should be able to read the response and immediately know what to try instead, without guessing or checking documentation. Skip the header and you've turned a helpful, actionable error into a plain "no."
405 vs 404 vs 501
Three codes can all show up when a request doesn't land where the client expected, and they answer different questions:
| Code | Meaning | Use when |
|---|---|---|
| 404 Not Found | Resource doesn't exist at this URL | No route or record matches, regardless of method |
| 405 Method Not Allowed | Resource exists, method doesn't | The URL is valid, but this particular resource doesn't support the method used |
| 501 Not Implemented | Server doesn't support the method at all | The method isn't implemented anywhere on the server, independent of which resource was targeted |
404 and 405 are the pair that's easy to conflate, because both can be triggered by hitting POST /users/42 when only GET /users/42 is defined — but they mean different things about what exists. 404 says the URL itself is unknown; 405 says the URL is known but this method isn't one of its supported operations. 501 is rarer and describes the server, not the resource: it means the method itself (say, a custom or exotic verb) isn't implemented anywhere, so no Allow header listing per-resource methods would even make sense.
Common Causes
The recurring shapes of a genuine 405:
- Using
POSTon a read-only endpoint — a resource that only supportsGET/HEADgets a write attempt. - Trying
DELETEwhen onlyGETis allowed — the client assumes a REST-y method exists that the server never implemented for that resource. - Wrong HTTP verb for the resource — a general mismatch between what a client expects a resource to support (often based on convention) and what the server actually wired up.
How Frameworks Produce 405s Automatically
Some frameworks generate 405s for you, which is worth knowing so you don't duplicate the logic — or worse, silently disable it.
In the Next.js App Router, a Route Handler file only defines the HTTP methods it exports (GET, POST, PUT, etc.); if a request arrives using a method the file doesn't export, Next.js responds with 405 automatically, without any code in the handler. Next.js also auto-implements OPTIONS and sets a correct Allow header based on whichever methods you did export, unless you define OPTIONS yourself — so the RFC's Allow requirement is already satisfied for free in the App Router, as long as you don't override it with something that forgets the header.
Returning 405 Correctly
Express / Node.js
Express doesn't generate 405s automatically — an undefined method on a defined path just falls through to your 404 handler unless you add an explicit method guard:
app
.route("/articles/:id")
.get((req, res) => {
res.json(getArticle(req.params.id));
})
.all((req, res) => {
// Any method other than GET lands here
res.set("Allow", "GET, HEAD").status(405).json({
error: "method_not_allowed",
message: `${req.method} is not supported on this resource.`,
});
});app.route() scopes the .all() fallback to just this path, so it only catches methods you haven't already handled with .get(), .post(), etc. — it won't swallow requests to other routes.
Next.js App Router
// app/api/articles/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> },
) {
const { id } = await params;
return Response.json(await getArticle(id));
}
// No POST/PUT/DELETE exported — Next.js returns 405 automatically
// for any other method, with an Allow header derived from the
// methods this file does export.If you need a custom body on the 405 instead of Next's default, export the other methods explicitly and return the response yourself:
export async function POST() {
return Response.json(
{ error: "method_not_allowed", message: "This resource is read-only." },
{ status: 405, headers: { Allow: "GET, HEAD" } },
);
}NGINX
The instinctive approach — limit_except — is a common trap: it restricts which methods are allowed, but it returns 403 Forbidden on a rejected method, not 405, and it doesn't set an Allow header. To get a spec-correct 405 with the required header, check the method explicitly and route to a named error location:
server {
location @method_not_allowed {
add_header Allow "GET, HEAD" always;
return 405;
}
error_page 405 @method_not_allowed;
location /articles/ {
if ($request_method !~ ^(GET|HEAD)$) {
return 405;
}
# normal proxy_pass / handling for GET and HEAD
}
}The error_page redirect is what lets you attach the Allow header to the final 405 response — setting add_header directly inside the if block doesn't reliably survive NGINX's response-generation internals the way it does via error_page.
REST API JSON body
HTTP/1.1 405 Method Not Allowed
Allow: GET, HEAD
Content-Type: application/json
{
"error": "method_not_allowed",
"message": "DELETE is not supported on this resource. Supported methods: GET, HEAD."
}Common Pitfalls
- Omitting the Allow header. It's a
MUSTin the RFC, not a nice-to-have — a 405 without it leaves the client guessing what it should have sent instead. - Using NGINX's
limit_exceptand expecting a 405. It returns 403, not 405, and adds noAllowheader — use an explicit method check witherror_pageif you need a spec-correct response. - Returning 404 instead of 405 when the resource exists. If the URL matches a real resource but the method doesn't fit, that's a 405 — collapsing it into a generic 404 hides useful information from the client.
- Letting the Allow header drift from what's actually implemented. If you hardcode
Allow: GET, POSTin one place while the handler logic changes elsewhere, the two can fall out of sync — derive the header from the same source of truth as the routing, where the framework allows it. - Reaching for 501 instead of 405. 501 says the server doesn't implement the method anywhere; 405 says this resource doesn't support it. A resource that simply doesn't allow
DELETEis a 405, not a 501.
Wrapping Up
405 is a status code that's supposed to hand the client the answer, not just the problem. The rules of thumb:
- 405 means the resource exists, but this method isn't one of its supported operations
- The
Allowheader listing supported methods is aMUST, not optional — skipping it violates the RFC - 404 is for unknown URLs; 405 is for known URLs with an unsupported method; 501 is for methods the server doesn't implement at all
- Watch for framework/proxy shortcuts (like NGINX's
limit_except) that don't actually produce a spec-correct 405
For more, see our pages on 405 Method Not Allowed, 404 Not Found, and 501 Not Implemented.