Troubleshooting

405 Method Not Allowed: what it means and how to fix it on nginx, Apache and in your app

A 405 error means the URL exists but refuses the HTTP method. Check the Allow header with curl, then fix nginx, Apache, a redirect or your app route.

Published ~11 min read
On this page

Short answer

A 405 Method Not Allowed error means the server found the URL but refuses the HTTP method you used, most often a POST sent to something that only serves GET. Run curl -i -X OPTIONS on the URL, read the Allow header and the Server line, then fix the layer that answered: the web server’s routing, a redirect that turned POST into GET, or the app’s route.

Where to start

What does 405 Method Not Allowed mean?

Every HTTP request starts with a method: GET to read, POST to submit, PUT and PATCH to change, DELETE to remove, OPTIONS to ask what is allowed. RFC 9110, the current HTTP specification, defines 405 as a method that is “known by the origin server but not supported by the target resource”. The server is up, the path resolved to something, and that something does not take this method. That is the difference from a 404 Not Found, where nothing exists at the path at all.

The same section adds a rule that makes 405 the easiest 4xx error to debug: the server MUST send an Allow header listing the methods the URL does accept. Not every server follows it, and which ones skip it is itself a clue, as the table further down shows.

Key facts

Status line
405 Method Not Allowednginx says 405 Not Allowed
Defined in
RFC 9110, section 15.5.6
Required header
AllowLists the methods this URL accepts
Methods every general-purpose server must support
GET and HEADRFC 9110, section 9.1
Class
4xx client error
Cacheable
Yes, by heuristicUnless Cache-Control says otherwise

“Client error” describes the request, not who has to fix it. When your own form, script or app sends the method, the fix is usually on your server: the request went to the wrong handler, or the handler does not list the method.

How the 405 error looks

The same status shows up under different wordings, depending on the server and the browser:

What you seeWhere it comes from
405 Not Allowednginx’s own error page, with nginx in the footer
405 Method Not AllowedThe standard status line: Apache, most frameworks, curl output
HTTP Error 405 - Method Not AllowedA generic wording on custom and hosting error pages; same error
HTTP Error 405.0 - Method Not Allowed with The page you are looking for cannot be displayed because an invalid method (HTTP verb) is being used.IIS 7 and later, detailed error page
405 - HTTP verb used to access this page is not allowed.IIS, the shorter page that remote visitors get
HTTP ERROR 405Chrome and Edge, when the server sent the 405 with an empty body

Check the method and the Allow header with curl

Browsers hide most of what you need. Repeat the request with curl, which shows the status line, every header and the body, and keeps no cache:

Linux · macOS · WSL · find out what the URL accepts
# 1. Send the method that fails and read the status and headers
curl -s -i -X POST -d 'test=1' https://example.com/api/login | head -n 20

# 2. Ask the URL which methods it takes
curl -s -i -X OPTIONS https://example.com/api/login | grep -i -E '^(HTTP|allow|server)'

# 3. Compare with a plain GET
curl -s -I https://example.com/api/login

Replace the URL and the method with the ones from your browser’s network tab or your app’s log. Two curl details matter here. -X only changes the method word, so test HEAD with -I, never with -X HEAD. And when you add -L to follow redirects, -X POST forces POST on every hop, which a browser does not do; use -d without -X to reproduce what a browser form does. More request recipes are in the curl examples guide.

Who sent the 405?

The headers and the body name the software that refused the request. These fingerprints come from each project’s source code, read on November 2, 2026, and from our own test runs for Express, PHP and Python:

What you seeWho sent itAllow header?
Status 405 Not Allowed, page with nginx in the footernginxNo, nginx sends none
Body The requested method POST is not allowed for this URL.Apache httpdYes
JSON The requested resource does not support http method 'PUT'.ASP.NET Web API on IISDepends on the app
The POST method is not supported for route ...Laravel, in debug modeYes
405 with an empty bodyA Next.js route handler, or your own codeNext.js: no
404 Cannot POST /path instead of a 405ExpressExpress answers 404 by default
XML-RPC server accepts POST requests only.WordPress xmlrpc.phpAllow: POST

To see the answer from outside your network, run the URL through our HTTP header checker: it shows the status code, each redirect on the way and the response headers, including server.

nginx: causes and fixes

nginx’s static file module, the part that serves files from root, accepts GET, HEAD and POST at first and refuses every other method with a 405. A POST then goes through the normal lookup: a missing file still gets a 404 and a directory without a trailing slash a 301, but once the file exists, nginx refuses the POST with a 405. Neither refusal is written to the error log, and nginx sends no Allow header with it. The access log is the only trace.

Symptom, cause, fix

  1. Symptom: A form or webhook POST gets 405 Not Allowed; the same URL opens fine in a browser

    Cause
    The URL maps to a file, usually an .html page or the site’s index.html. A POST to / ends there too, through the index module. Typical senders: a contact form on a static site, or a payment or login provider that returns the visitor with a POST.
    Fix
    Point the form at something that processes it (a PHP script or an app behind proxy_pass). For a callback that only needs to show the page, see the error_page option below.
  2. Symptom: The single-page app loads, but every POST to /api/... returns 405

    Cause
    No location matches the API path, so try_files $uri $uri/ /index.html rewrites the call to /index.html, a static file. A POST to /api without the slash is a different case: nginx answers it with a 301 to /api/, the browser repeats it as a GET, and the app may answer that with 405 (see the redirect section).
    Fix
    Give the API its own location; nginx picks the longest matching prefix, so it wins over /. Config below.
  3. Symptom: PUT, PATCH, DELETE or a WebDAV method such as PROPFIND gets 405

    Cause
    The request reached a static location, which refuses every method except GET, HEAD and POST.
    Fix
    Route the path to the app that implements the method. For plain file uploads, nginx’s DAV module handles PUT and DELETE; setup in the 501 Not Implemented guide.
  4. Symptom: One method gets 405 on every URL, including app routes

    Cause
    A hardening snippet with if ($request_method ...) { return 405; }, or the method is TRACE, which nginx always refuses itself, even in front of proxy_pass.
    Fix
    Debian · Ubuntu · find explicit method rules
    sudo grep -rn -E 'return 405|request_method|limit_except|error_page' /etc/nginx/

Every other method passes through proxy_pass and fastcgi_pass unchanged, so a 405 from a location that proxies to an app came from the app (apart from TRACE), and the app’s log has the reason. A limit_except block that denies a method answers with a 403, not a 405.

nginx · single-page app with an API behind it
server {
    listen 80;
    server_name example.com;
    root /var/www/app/dist;

    # API calls go to the backend with every method intact
    location /api/ {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # Everything else is the app shell
    location / {
        try_files $uri $uri/ /index.html;
    }
}

For PHP the same rule applies: a location ~ \.php$ block with fastcgi_pass must exist, and the try_files fallback must be /index.php?$args, not /index.html. Test and reload after every change:

Debian · Ubuntu
sudo nginx -t && sudo systemctl reload nginx

A static page that must accept a POST. When a provider sends the visitor back with a POST and the page only needs to load, nginx can answer the 405 with the page itself. On an error_page redirect to a URI, nginx switches the method to GET (its source keeps only HEAD as it is), so the file is served with a 200:

nginx · serve the page for a POST, drop the body
location = /payment/return.html {
    error_page 405 =200 $uri;
}

A redirect turned the POST into a GET

This cause looks like a server bug and is not. The form posts to http://example.com/login, the server answers with a 301 to https://example.com/login, and the browser follows with a GET, because RFC 9110 lets user agents change POST to GET after a 301 or 302 for historical reasons. The GET lands on an endpoint that only takes POST, and that endpoint correctly says 405. The same happens with redirects for www, a trailing slash or an old path. We reproduced it with a small test server whose /login only accepts POST:

Example output · curl 7.88.1 against a local test server
$ curl -s -i -L -d 'user=a' http://127.0.0.1:18405/old-301
HTTP/1.0 301 Moved Permanently
Location: /login

HTTP/1.0 405 Method Not Allowed
Allow: POST

$ curl -s -i -L -d 'user=a' http://127.0.0.1:18405/old-308
HTTP/1.0 308 Permanent Redirect
Location: /login

HTTP/1.0 200 OK

Headers are shortened to the relevant lines. The server’s own log confirms what happened: after the 301 it received GET /login, after the 308 it received POST /login. curl converts like a browser does unless you add --post301 or --post302. Two fixes:

  • Send the request to the final URL. Update the form action, the API base URL or the webhook address to the exact https:// address, host and slash the server expects. This is the real fix.
  • Use 308 for redirects that may carry a POST. A 308 (or 307 for a temporary move) keeps the method and the body. In nginx: return 308 https://$host$request_uri;. In an Apache RewriteRule, use the flag R=308 instead of R=301.

Apache and .htaccess

Apache’s default handler, which serves plain files, behaves like nginx for POST: once the file exists, it refuses the method with a 405. Unlike nginx it logs the refusal and sends an Allow header, for a plain file usually GET, HEAD, POST and OPTIONS (plus TRACE unless TraceEnable Off is set). Yes, POST appears in the list of a URL that just refused it. Look for the line in the error log:

Debian · Ubuntu · Apache
sudo grep AH00131 /var/log/apache2/error.log | tail -n 5
# AH00131: This resource does not accept the POST method.

# rules in the config and in .htaccess files that act on methods
sudo grep -rn -E 'R=405|<Limit|<LimitExcept|REQUEST_METHOD|FallbackResource' /etc/apache2/ /var/www/
  • An SPA fallback catches the API. FallbackResource /index.html or a RewriteRule to index.html sends a POST for a missing path to the static shell, which refuses it. Exclude the API path, or proxy it to the backend before the fallback applies.
  • A deliberate 405. mod_rewrite accepts any status in the R flag; RewriteRule ^ - [R=405] under a RewriteCond %{REQUEST_METHOD} is how some security snippets block methods. Check that the condition matches only what you meant.
  • <Limit> blocks. <Limit> and <LimitExcept> with Require all denied refuse methods with a 403, not a 405, so they explain a 403 Forbidden rather than this error.
test and reload
sudo apache2ctl configtest && sudo systemctl reload apache2

Express, PHP, Flask, Django, Laravel and Next.js

If the request reached your application, the framework decides between 404 and 405, and they do not agree:

FrameworkWrong method on an existing routeOPTIONS
Express 5.2.1 (tested)404, body Cannot POST /items200 with Allow: GET, HEAD
Flask (Werkzeug)405 with an Allow header listing the route’s methodsAnswered automatically
Django class-based views405 with Allow; logs Method Not Allowed (POST): /path/200 with Allow
Laravel405, The POST method is not supported for route ... Supported methods: GET, HEAD.Handled by the router
Next.js 16 route handlers405, empty body, no Allow header204 with Allow
Plain PHPWhatever your script sends; nothing by itselfYour script decides

The Express row is the trap: a client that “gets 405” from an Express API is talking to something else, usually the web server or proxy in front of it. A missing method in Express looks like a missing route. If you want a correct 405, declare it. .all() also catches OPTIONS, so without its own handler the CORS preflight gets a 405 too (see below). This is the pattern we tested:

Express · answer wrong methods with 405 and Allow
app.route('/reports')
  .get((req, res) => res.json([]))
  .options((req, res) => res.set('Allow', 'GET, HEAD, OPTIONS').sendStatus(204))
  .all((req, res) => res.set('Allow', 'GET, HEAD, OPTIONS').status(405).send('Method Not Allowed'));

In our run with Express 5.2.1, GET and HEAD got 200, OPTIONS got 204 and POST and DELETE got 405, all with Allow: GET, HEAD, OPTIONS. Add your CORS headers to the .options() handler if other origins call the route.

PHP · an endpoint that only takes POST
<?php
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    header('Allow: POST');
    http_response_code(405);
    exit("Method Not Allowed\n");
}
// handle the POST here

Framework-specific causes worth checking:

  • HTML forms only send GET and POST. A form for a Laravel route defined with Route::put or Route::delete needs @method('PUT') (a hidden _method field) inside the form. php artisan route:list shows which methods each route takes.
  • Flask routes default to GET. A view that handles a form needs methods=["GET", "POST"] in its @app.route.
  • Django: a class-based view answers 405 for every method it has no handler for, and @require_POST or @require_http_methods do the same for function views. Add the handler, or fix the client’s method.
  • Next.js: a route.ts must export a function named after the method, such as export async function POST(). Without that export, or with a typo in its name, the method gets a 405.

IIS and ASP.NET

On IIS the classic case is PUT and DELETE working in Visual Studio and failing with 405 in production. Microsoft’s troubleshooting article explains why: the WebDAV handler is registered for those methods on the production server, gets the request first, finds WebDAV disabled and refuses it. Remove it for the app in web.config:

web.config · inside <configuration>
<system.webServer>
  <modules>
    <remove name="WebDAVModule" />
  </modules>
  <handlers>
    <remove name="WebDAV" />
  </handlers>
</system.webServer>

A 405 behind a CORS error

Before a cross-origin PUT, DELETE or JSON POST, the browser sends an OPTIONS preflight. If that hits a static location or a handler without OPTIONS, it gets a 405, and the browser reports a CORS failure instead of the 405. Check the preflight with curl -i -X OPTIONS and an Origin header; the CORS error guide shows what the answer must contain.

405 on a WordPress site

WordPress core sends a 405 on purpose in two places, and both are harmless: xmlrpc.php and wp-comments-post.php answer any method except POST with 405 and Allow: POST. Opening xmlrpc.php in a browser therefore shows XML-RPC server accepts POST requests only. and nothing is broken. A wrong method on the REST API is not a 405 at all: WordPress answers 404 with the code rest_no_route, “No route was found matching the URL and request method.”

So a 405 on normal pages or forms comes from around WordPress:

  • A redirect before the form handler. A comment or contact form posted to http:// or the wrong host is redirected and arrives as GET; the comment handler then answers 405. Set both URLs under Settings, General to the exact https:// address (details above).
  • Server rules. The nginx and .htaccess checks above, especially a try_files fallback to index.html instead of /index.php?$args.
  • A security plugin or firewall. Switch plugins off from the shell and test again; if the error is gone, turn them back on one at a time:
WP-CLI · in the WordPress directory
wp plugin list --status=active --field=name > active-plugins.txt
wp plugin deactivate --all
# test the URL again, then restore:
wp plugin activate $(cat active-plugins.txt)

WAFs, CDNs and cached 405s

Layers in front of the server can refuse methods too, but they rarely use 405. Cloudflare’s documentation states that it does not generate 405 errors itself, so a 405 through Cloudflare came from your origin or your Worker or Pages function. The OWASP Core Rule Set, the common rule set for ModSecurity and other WAFs, allows only GET HEAD POST OPTIONS by default (rule 911100, “Method is not allowed by policy”) and blocks the rest, normally with a 403. If the WAF is the suspect, ask your server directly and compare:

Linux · macOS · skip the CDN or proxy (203.0.113.10 = your server)
curl -s -i -X PUT -d 'x' \
  --resolve example.com:443:203.0.113.10 https://example.com/api/items/1 | head -n 5

The same command catches a DNS record that still points at an old server, which may serve only static files there. One more trap: RFC 9110 makes a 405 heuristically cacheable, so a browser or CDN may keep showing an old 405 after the fix. Test with curl, purge the URL at the CDN, and send Cache-Control: no-store on error responses you generate yourself.

405 vs 501 vs 403 vs 404

405 Method Not Allowed501 Not Implemented403 Forbidden404 Not Found
What it saysThis URL does not take this methodThe server does not support the method for any URLUnderstood, but refused by a ruleNothing exists at this path
Must includeAn Allow header with the accepted methodsNothing extraNothing extraNothing extra
Typical triggerPOST to a static page, PUT to a GET-only routeAn unknown or WebDAV method on a server without itA WAF rule, limit_except or <Limit>A wrong path or a missing route
Cacheable by defaultYesYesNoYes
Definitions and cacheability from RFC 9110, sections 15.5 and 15.6, read on November 2, 2026.

Real servers blur the lines. nginx answers an unknown method on a static file with 405 where Apache says 501, and Express answers a wrong method with 404. The 501 Not Implemented guide has the server-by-server table for unusual methods. Errors about the request itself rather than its method are a 400 Bad Request, or a 431 Request Header Fields Too Large when only the headers are too big.

Find every 405 in your logs

Since nginx does not log its 405s in the error log (except TRACE, at the info level), the access log is where you see them. In the default combined format the method is field 6 (with the opening quote of the request line, which substr strips), the path field 7 and the status field 9:

Debian · Ubuntu · which methods and paths got a 405
sudo awk '$9 == 405 {print substr($6, 2), $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
# Apache: the same with /var/log/apache2/access.log

Lots of 405s for PUT, PROPFIND or TRACE from addresses you do not know are scanners checking which methods your server allows; refusing them is the correct answer. A 405 for a POST path your own site uses is the one to fix.

Reading these logs and editing the web server config needs root access to the machine. On a VMHeaven Standard KVM you get full root, so nginx, Apache and the app behind them are yours to change, and the browser VNC console in the panel still reaches the server if a config change locks you out.

How we tested

OS image
Debian 12
Versions
curl 7.88.1Python 3.11.2Node.js 26.12.1Express 5.2.1PHP 8.2.35
Tested on
Where
Test servers bound to 127.0.0.1 on our own Linux host
Method
301 and 308 redirects to a POST-only endpoint followed with curl -L; Express and PHP endpoints hit with POST, DELETE, GET and OPTIONS
Read in source, not run
nginx (static, index, error page and request modules), Apache httpd 2.4 (core and protocol), Next.js 16.4.3, WordPress (xmlrpc, comments, REST server)

Frequently asked

What does error 405 Method Not Allowed mean?

The server found the URL but refuses the HTTP method of the request, for example a POST to a page that only serves GET. RFC 9110 requires the response to carry an Allow header that lists the methods the URL does accept, so curl -i -X OPTIONS on the URL usually shows what to send instead.

Why does my POST request return 405 Method Not Allowed?

Usually because the POST reached a static file instead of your app: nginx and Apache serve files to GET and HEAD and refuse POST with 405. Common reasons are a missing location or proxy rule for the API path, a try_files or FallbackResource fallback to index.html, or a 301 or 302 redirect after which the browser repeats the request as GET.

How do I fix 405 Not Allowed in nginx?

Find which location answered. If it is a static location, give the API or form handler its own location with proxy_pass or fastcgi_pass, make sure try_files does not fall back to index.html for API paths, and search /etc/nginx for return 405 rules. nginx's own 405 has no Allow header and is not written to the error log, so check the access log.

What is the difference between 405, 501 and 403?

405 means this URL does not accept the method and lists the allowed ones in an Allow header. 501 means the server does not support the method for any URL. 403 means the request was understood but a rule refused it; WAFs and Apache Limit blocks that deny a method usually answer 403, not 405.

Can a visitor fix a 405 error?

Rarely. Reloading the page sends the same request again and gets the same 405. If a form failed, go back to it, open it on the exact https address and submit it again; clear the browser cache only when an old 405 is cached. Otherwise the fix is on the server or in the app, so report the URL and the time to the site owner.

Related articles

All troubleshooting