,

HTTP Status 405: A Developer’s Guide to Fixing It

Neel Das avatar
HTTP Status 405: A Developer’s Guide to Fixing It
  • A 405 means the URL exists, but the method is wrong. Think POST sent to a route that only accepts GET, not a missing page.
  • Start with the Allow header. It tells you which methods the resource currently accepts and is the fastest clue you’ll get from the server.
  • Debug layer by layer. Check the client, then CDN or WAF, then Nginx or Apache, then the framework router.
  • A lot of 405 incidents are really contract problems. The code, proxy rules, and docs drift apart, and production is where that mismatch shows up.

Table Of Contents

A feature works on localhost. Your form submits, your React app gets a clean response, your integration test passes. Then the same request hits staging through a CDN, a reverse proxy, and a production app server, and suddenly you get 405 Method Not Allowed.

That failure is maddening because it looks simple and often isn’t. The route exists. DNS is fine. The app is up. You’re not staring at a 500 or a dead service. You’re looking at a request that made it far enough for some layer to say, “I know this resource. I just won’t accept that verb here.”

Introduction The Frustrating 405 Error

I’ve seen http status 405 show up in exactly the kind of release that feels safe. A small frontend change. A route rename that looked harmless. A proxy rule added for security. Everything appears normal until someone tries to POST from the live environment.

A hand-drawn illustration showing a developer frustrated by a 405 error occurring in production but not localhost.

HTTP 405, formally Method Not Allowed, is a long-established client error in the HTTP family. One useful detail makes it different from many other errors: the response should include an Allow header listing the permitted methods, which makes the error intentionally self-describing according to Abstract API’s HTTP 405 reference.

That matters in practice. When a response says Allow: GET, HEAD, OPTIONS, the server is giving you a direct hint instead of forcing you to guess whether the path is wrong, the app is down, or the resource doesn’t exist.

Practical rule: If you see a 405, stop treating it like a generic failure. It’s a method mismatch until proven otherwise.

Production stacks make this trickier than localhost. A browser sends one thing. A proxy rewrites it. A WAF blocks it. Nginx applies method rules. Then your framework router gets the request that survived all of that. If you start by blaming application code, you can burn a lot of time in the wrong layer.

What Exactly is a 405 Method Not Allowed Error

The cleanest way to think about a 405 is this: the key fits the door, but you’re turning it the wrong way. The URL is valid. The server knows the resource. It just refuses the HTTP method used on that resource.

A 404 says “I can’t find that path.” A 405 says “that path exists, but not for this verb.”

An infographic illustrating the difference between HTTP status code 404 Not Found and 405 Method Not Allowed.

The header that matters most

When you’re debugging, the Allow header is the first thing worth checking. RFC-derived guidance says a server must send an Allow header in a 405 response listing the methods supported by that resource. That makes 405 useful not just for humans, but also for clients, gateways, and observability tooling, as noted in MDN’s source content for status 405.

A response might look like this:

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

That single header tells you a lot:

  • GET is allowed for reading the resource
  • HEAD is allowed for metadata checks
  • OPTIONS is available for capability discovery
  • POST is not accepted here, at least right now

405 vs 501

This distinction matters in API design and debugging.

StatusMeaningScope
405Method recognized but refused for this resourceResource-specific
501Method not implemented by the serverServer-wide capability

If your route is read-only and a client tries POST, PUT, or DELETE, 405 is the right answer. If the server doesn’t support that method at all, 501 is the better fit.

Don’t treat 405 as a weaker 500. It’s usually a precise policy signal.

That precision is why 405 is so helpful when the stack preserves it properly. It narrows the search area fast.

A Debugging Checklist for Finding the Root Cause

The mistake I see most often is jumping straight into controller code. With modern stacks, a 405 can come from several places before your app ever sees the request. The fastest path is a layered check.

Start outside the application

First, inspect the actual response, not what your frontend logger summarized.

Look for:

  • The Allow header. That tells you what the responding layer thinks is valid.
  • Response headers that reveal ownership. CDN, gateway, reverse proxy, or framework-specific headers often tell you who generated the response.
  • Differences between local and production paths. A frontend app may call /api/items locally but hit /v1/items through a proxy in production.

MDN’s coverage points out an under-explained reality: 405 is often a routing-contract problem between frontend, API, and infrastructure layers, and the practical workflow is to inspect Allow, compare route definitions, then verify whether edge infrastructure is rewriting or blocking methods before the request reaches the app in MDN’s 405 guidance.

Move layer by layer

I use this sequence:

  1. Client request

    • Is the browser form defaulting to GET?
    • Is fetch() sending POST where the route only supports PUT?
    • Is an SDK wrapper changing the method under the hood?
  2. CDN, WAF, or reverse proxy

    • Are certain verbs blocked at the edge?
    • Is a preflight OPTIONS request failing before the main request happens?
    • Is method override middleware expected locally but missing in production?
  3. Web server

    • Nginx limit_except
    • Apache <Limit> rules
    • Route-specific blocks that allow reads but deny writes
  4. Application framework

    • Route exists, but only for GET
    • Resource is mounted under a namespace you didn’t deploy
    • Middleware short-circuits before controller logic

A good way to test the contract quickly is to hit the endpoint directly with curl or Postman and compare methods one by one. If you’re also reviewing endpoint behavior more broadly, this guide on testing RESTful APIs is a useful companion because it forces you to verify the method, path, headers, and expected response together.

If localhost works and production fails, assume there’s an extra layer in production enforcing method policy.

Watch for false leads

Some teams label any rejected API call as “405” in logs or UI messages, even when the actual issue is elsewhere. Don’t trust the frontend label. Trust the actual wire response.

A lot of 405s come from explicit method controls in web servers. Apache and Nginx can allow or deny methods by configuration, which is why a valid URL can still fail if the verb isn’t permitted, as described in Sitechecker’s explanation of 405 behavior.

A hand-drawn diagram illustrating a server configuration update to explicitly allow additional HTTP methods.

Nginx

This is a common source of production-only surprises.

location /api/ {
limit_except GET POST {
deny all;
}
}

That block allows GET and POST, but rejects PUT, PATCH, or DELETE.

If your app supports updates, adjust the allowed set:

location /api/ {
limit_except GET POST PUT PATCH DELETE {
deny all;
}
}

Check for a second trap too. A different location block may catch the route you think you’re hitting.

Apache

Apache can enforce method rules with <Limit> or related directives.

<Limit GET POST>
Require all granted
</Limit>

That looks harmless until your client tries PUT.

If the endpoint should allow more methods, update the rule to match the route contract:

<Limit GET POST PUT DELETE>
Require all granted
</Limit>

When teams run into request-size issues alongside method issues, they often debug the wrong thing first. If you’re sorting out adjacent upload failures, this write-up on status code 413 is worth keeping nearby because the symptoms can get mixed together in API clients.

Express.js

In Express, the route may exist but only for one verb.

app.get('/users', (req, res) => {
res.json(users);
});

If the frontend sends:

fetch('/users', { method: 'POST' })

you’ll get a method mismatch unless you define the handler:

app.post('/users', (req, res) => {
const user = createUser(req.body);
res.status(201).json(user);
});

If you want clearer diagnostics, add an explicit method handler for unsupported verbs and return a 405 with Allow.

A short walkthrough can help if you want to see the mechanics visually:

Django

Django views often fail when the allowed methods aren’t declared or the wrong class-based view is used.

Function-based example:

from django.views.decorators.http import require_http_methods
@require_http_methods(["GET", "POST"])
def users(request):
...

If your endpoint needs DELETE, that decorator has to reflect reality.

For class-based views, check which methods are implemented:

from django.views import View
class UserView(View):
def get(self, request):
...
def post(self, request):
...

No put() method means no PUT support.

Ruby on Rails

Rails usually keeps method intent in routes.rb.

resources :users, only: [:index, :show]

That route set won’t accept create, update, or destroy.

To support creation:

resources :users, only: [:index, :show, :create]

Then make sure the controller action exists:

def create
# create user
end

Worth checking: The route can be correct in code but missing in the deployed environment because an old build or stale container is still serving traffic.

Client-Side Checks and Why Your API Docs Are Key

Some 405s are embarrassingly small. An HTML form defaults to GET because nobody set method="POST". A frontend helper switched from POST to GET during a refactor. A generated client kept using an old endpoint signature after the backend changed.

A hand-drawn illustration depicting a client application using the wrong HTTP GET method instead of required POST.

Client checks that catch real bugs

Before touching the server, verify these:

  • HTML forms

    • Did the form declare method=\"post\"?
    • Is the action URL the same path the backend exposes?
  • JavaScript requests

    • Does fetch() or Axios send the intended method?
    • Are redirects changing request behavior in a way your client library hides?
  • Browser behavior

    • Is an OPTIONS preflight happening before the actual call?
    • Does the browser hit a different origin in production than in dev?

If your team is building automation-heavy workflows, resources on connecting apps without custom code are useful because they force you to think in terms of API contracts and method compatibility, not just code snippets.

A lot of 405s are documentation failures

This is the part most engineering teams underweight. A backend route changes from POST /sessions/refresh to PUT /sessions/refresh, but the public docs still show POST. Every consumer follows the docs, gets a 405, and opens a bug against the server.

That’s not really a server failure. It’s contract drift.

A strong contrarian take from Hostwinds’ 405 discussion is that many 405 incidents are documentation failures, not only server failures. The endpoint may be correct, but the docs or examples omit method constraints, and teams end up patching the wrong layer.

Here’s what healthy API documentation should make unambiguous:

Doc elementWhat it must show
Endpoint exampleExact method and path
Reference blockAllowed methods for the resource
Sample client codeMatching verb in every language example
Change historyWhen method support changed

If you maintain API references in GitHub, it’s worth tightening your process around API documentation best practices so method changes don’t land in code without landing in docs.

A 405 often means the request is wrong. It can also mean the docs taught the client to be wrong.

Adopting a Proactive Stance on API Errors

The teams that handle http status 405 well don’t just fix the incident. They reduce the chance of seeing the same class of bug again.

That starts with better visibility. Log the request method, path, response status, and the component that generated the response. If your edge layer can return a 405, make that obvious in headers or logs so app engineers don’t waste time debugging routes that never received the request.

It also means reviewing method policy as part of change management. When someone edits routing, proxy rules, or server config, the API contract changed whether they meant to or not.

For teams improving their broader toolchain, directories of curated AI agents and developer workflow tools can be handy for evaluating what belongs in a modern documentation and delivery workflow, especially if you’re trying to tighten the connection between code, docs, and operational checks.

The practical standard is simple:

  • Check Allow first
  • Trace ownership layer by layer
  • Keep route definitions, edge rules, and docs aligned
  • Treat method support as part of the API contract, not an implementation detail

A 405 is rarely mysterious once you stop treating it as a generic web error. It’s usually a precise signal that one part of your stack believes the contract is different from another.

If your API docs keep drifting from your routes, DeepDocs is worth a look. It keeps documentation in sync with code changes inside GitHub, which helps teams avoid the kind of stale method examples and route mismatches that turn into avoidable 405s.

Leave a Reply

Discover more from DeepDocs

Subscribe now to keep reading and get access to the full archive.

Continue reading