What 405 is telling you

405 Method Not Allowed means the server found the resource at that URL, understood the HTTP method you sent, and refuses to apply that method to that resource. The classic case is a POST to something that only serves GET.

That makes 405 more specific than it looks. It’s not “page not found” (404), not “you aren’t allowed” (403), and not “this server has never heard of that method” (501). The address is right; the verb is wrong for that address.

RFC 9110 (§15.5.6) adds one rule that makes 405 easy to debug: the server must send an Allow header listing the methods the resource does accept. A well-behaved 405 hands you the answer:

HTTP/1.1 405 Method Not Allowed
Allow: GET, HEAD
Content-Type: text/html

Not every server honours that. Some error paths send 405 with no Allow at all. When the header is missing, you have to work out the accepted methods yourself, which is what the commands below are for.

If you’re just using the site

You almost certainly can’t fix this from your side. The usual visitor scenario is submitting a form, clicking a button or using an app, and getting a page that says “405 Not Allowed” or “Method Not Allowed”.

A few things are worth trying once: go back to the page that contains the form, reload it, and submit again. A form page that was open for hours, or restored from a bookmark, can post to an address the site no longer handles. If the error repeats, the site is broken for that action. Tell the owner which page and which button, and when.

Everything below is for the people running the site or calling the API.

Reproduce it with curl

Browsers hide too much here. Send the exact method yourself and look at the status line and headers:

curl -i https://example.com/contact -d 'name=test'
HTTP/2 405
allow: GET, HEAD
server: nginx
content-type: text/html

Then ask the resource what it accepts. Many servers and frameworks answer OPTIONS with the method list:

curl -i -X OPTIONS https://example.com/contact

And compare with a plain GET, which tells you whether the URL itself is fine:

curl -sS -D - -o /dev/null https://example.com/contact

-I sends HEAD, not GET. The command above sends GET and discards only the response body. In Windows PowerShell, use curl.exe -sS -D - -o NUL https://example.com/contact.

Three headers do most of the work:

  • Allow: the methods the resource accepts. If your method isn’t there, you’re aimed at the wrong handler or the handler isn’t wired for that method.
  • Server and any CDN or WAF headers: clues to which layer produced or relayed the response. A proxy can replace an upstream error page or change headers, so an nginx-style page alone does not prove the request missed the app. Correlate the request ID and timestamp across proxy and application logs.
  • Location, if the first response to your POST is a 301 or 302 rather than 405. That’s the redirect trap described below.

The HTTP check shows the server banner, the redirect chain and response headers for a URL from outside your network. It sends ordinary GET requests, so use it to confirm the page itself works and to see which server and CDN sit in front of it; testing the failing method still needs curl or your API client.

The usual causes

1. A form or API call aimed at a static file

The form’s action points at contact.html, /thanks/ or another static path. Static file serving only makes sense for GET and HEAD, so nginx and most static hosts and CDNs answer a POST to a plain file with 405. nginx shows a bare “405 Not Allowed” page.

This happens a lot after moving a site to static hosting. The old server ran a PHP or CGI script behind that form; the new host has no code to run, so the POST has nowhere to go.

Fix: point the form at something that processes it: your own endpoint, a serverless function, or a form-handling service. You’ll find nginx snippets that “fix” this with error_page 405 =200 $uri;, which answers the POST with the static page. That hides the error and also throws away whatever the user submitted. Don’t do it for forms that matter.

2. The route exists but not for that method

The application has a handler for the path, just not for this method. Frameworks report this differently, and the difference is worth knowing:

  • Flask: a route without methods=["POST"] returns 405 with an Allow header.
  • Django: class-based views return 405 for methods the view doesn’t define, and so do @require_POST-style decorators.
  • Laravel: “The POST method is not supported for route contact. Supported methods: GET, HEAD.”
  • Spring: “Request method ‘POST’ is not supported.”
  • FastAPI: {"detail":"Method Not Allowed"}.
  • Next.js App Router: a route.ts that only exports GET answers other methods with 405.
  • Express and Rails don’t use 405 for this. Express answers Cannot POST /contact with a 404, and Rails raises a routing error (“No route matches [POST]”). If you’re on those stacks and see 405, it came from somewhere else in the chain.

Fix: add the handler for the method, or call the method the route was built for. Check the route list rather than guessing: php artisan route:list, flask routes, or your framework’s equivalent.

3. A redirect turned your POST into a GET

This one wastes the most time. You POST to http://example.com/api/orders. The server redirects to https://… (or adds a trailing slash) with a 301 or 302. For historical reasons, browsers and most HTTP clients change POST to GET when following 301 and 302, and drop the body. The GET arrives at a handler that only accepts POST, and you get 405 on a URL you never sent a GET to.

With curl, let -d select POST; run without -L first to inspect the initial response:

curl -i http://example.com/api/orders -d '{}'

If that returns 301 or 302, compare the redirect chain on a test endpoint you control:

curl -i -L http://example.com/api/orders -d '{}'

Here 301/302 change POST to GET and drop its body; 307/308 preserve POST and the body. Do not add -X POST to this example: it explicitly pins the method name, so -L -X POST -d '{}' continues with POST even after 301/302 and does not reproduce the browser’s method switch. Check the actual methods in the test server logs. Do not send test POSTs to an unrelated production service. See the curl manual.

Fix: call the canonical URL directly (https, the right host, the exact slash form). If clients you don’t control post to the old address, use 307 or 308 for that redirect; both require the client to repeat the same method and body. The differences between the redirect codes are covered in 301 vs 302.

4. CORS preflight (OPTIONS) isn’t handled

Your request works from curl or Postman and fails in the browser. Before a cross-origin request that uses PUT, DELETE or PATCH, a custom header, or Content-Type: application/json, the browser sends a preflight:

OPTIONS /api/orders
Origin: https://app.example.com
Access-Control-Request-Method: PUT

The preflight must get a 2xx response with the right Access-Control-Allow-* headers. If the route has no OPTIONS handler, the server says 405, and Chrome reports something like “Response to preflight request doesn’t pass access control check: It does not have HTTP ok status.” The real PUT is never sent, which is why your server logs show only an OPTIONS line.

Reproduce it the way the browser does:

curl -i -X OPTIONS https://api.example.com/api/orders \
  -H 'Origin: https://app.example.com' \
  -H 'Access-Control-Request-Method: PUT'

Fix: enable your framework’s CORS middleware, which answers OPTIONS for you, or answer it at the proxy. Make sure authentication middleware lets OPTIONS through: preflights carry no credentials, so an auth layer that rejects them breaks CORS in a way that looks like a method problem.

5. IIS: WebDAV or the handler mapping eats PUT and DELETE

On IIS, an ASP.NET API that works for GET and POST but returns “HTTP Error 405.0 - Method Not Allowed” for PUT and DELETE is a well-known pattern. The WebDAV module, if installed, grabs those verbs before your application sees them. Removing it for the site usually fixes it:

<system.webServer>
  <modules>
    <remove name="WebDAVModule" />
  </modules>
  <handlers>
    <remove name="WebDAV" />
  </handlers>
</system.webServer>

If WebDAV isn’t the cause, look at the handler mapping that serves your app (for classic ASP.NET usually ExtensionlessUrlHandler-Integrated-4.0). Its verb attribute decides which methods it handles; a list like GET,HEAD,POST,DEBUG leaves PUT and DELETE with no handler. Setting it to * or adding the verbs fixes that.

The same idea applies anywhere a server module sits in front of your app and claims some methods for itself.

6. HTML forms only send GET and POST

<form method="PUT"> or method="DELETE" isn’t valid HTML. The browser falls back to GET, and a route that only accepts DELETE answers 405. Frameworks work around this with a hidden field (_method in Rails and Laravel, @method('DELETE') in Blade) on a POST form. If the hidden field is missing, or a proxy or middleware order prevents the framework from reading it, you get 405.

When the method is blocked on purpose

Sometimes a 405 is a policy, not a bug: an API gateway that only exposes certain methods, or a WAF rule that rejects unusual verbs. You can tell by where the response comes from: the Server header and error page belong to the gateway, and your application logs show nothing.

Note that the common nginx and Apache method restrictions (limit_except and <LimitExcept> with deny rules) return 403, not 405. A 405 points more often at “no handler for this method” than at an access rule. If you’re seeing 403 instead, 403 Forbidden covers that side.

Make sure you’re hitting the right server

If the method works in staging and fails in production, check that production traffic reaches the code you think it does. A hostname that still points at an old server or a static host gives you a 405 for every write method. The DNS lookup shows where the name resolves now, and the Server header from the HTTP check shows what answers there.

After the fix

  • Repeat the exact failing request with curl -i -X <METHOD> and check you get the status you expect, not 405 and not a redirect.
  • For browser apps, open DevTools → Network, tick “Preserve log”, and confirm both the OPTIONS preflight and the real request succeed.
  • Check the Allow header on a deliberately wrong method. It should list exactly what the route supports, which keeps the next debugging session short.

Check your own domain now

Free, no sign-up. Runs the exact check this guide describes and shows what to fix.