Short answer
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
- If the 405 page says 405 Not Allowed with nginx in the footera static location got the request, not your app
- If a form or API POST works locally but fails on the live sitea redirect may have turned the POST into a GET
- If your single-page app loads, but every API call returns 405try_files sends the call to index.html
- If the response is your framework's own 405 or JSON errorthe route exists, but not for this method
- If the site runs WordPresscheck which file answered, then plugins and server rules
- If the browser console shows a CORS error next to a 405the OPTIONS preflight is refused
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 see | Where it comes from |
|---|---|
405 Not Allowed | nginx’s own error page, with nginx in the footer |
405 Method Not Allowed | The standard status line: Apache, most frameworks, curl output |
HTTP Error 405 - Method Not Allowed | A 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 405 | Chrome 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:
# 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/loginReplace 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 see | Who sent it | Allow header? |
|---|---|---|
Status 405 Not Allowed, page with nginx in the footer | nginx | No, nginx sends none |
Body The requested method POST is not allowed for this URL. | Apache httpd | Yes |
JSON The requested resource does not support http method 'PUT'. | ASP.NET Web API on IIS | Depends on the app |
The POST method is not supported for route ... | Laravel, in debug mode | Yes |
| 405 with an empty body | A Next.js route handler, or your own code | Next.js: no |
404 Cannot POST /path instead of a 405 | Express | Express answers 404 by default |
XML-RPC server accepts POST requests only. | WordPress xmlrpc.php | Allow: 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
Symptom: A form or webhook
POSTgets405 Not Allowed; the same URL opens fine in a browser- Cause
- The URL maps to a file, usually an
.htmlpage or the site’sindex.html. APOSTto/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 aPOST. - 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 theerror_pageoption below.
Symptom: The single-page app loads, but every
POSTto/api/...returns 405- Cause
- No
locationmatches the API path, sotry_files $uri $uri/ /index.htmlrewrites the call to/index.html, a static file. APOSTto/apiwithout the slash is a different case: nginx answers it with a 301 to/api/, the browser repeats it as aGET, 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.
Symptom:
PUT,PATCH,DELETEor a WebDAV method such asPROPFINDgets 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
PUTandDELETE; setup in the 501 Not Implemented guide.
Symptom: One method gets 405 on every URL, including app routes
- Cause
- A hardening snippet with
if ($request_method ...) { return 405; }, or the method isTRACE, which nginx always refuses itself, even in front ofproxy_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.
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:
sudo nginx -t && sudo systemctl reload nginxA 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:
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:
$ 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 OKHeaders 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 exacthttps://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 ApacheRewriteRule, use the flagR=308instead ofR=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:
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.htmlor aRewriteRuletoindex.htmlsends aPOSTfor 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
Rflag;RewriteRule ^ - [R=405]under aRewriteCond %{REQUEST_METHOD}is how some security snippets block methods. Check that the condition matches only what you meant. <Limit>blocks.<Limit>and<LimitExcept>withRequire all deniedrefuse methods with a 403, not a 405, so they explain a 403 Forbidden rather than this error.
sudo apache2ctl configtest && sudo systemctl reload apache2Express, 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:
| Framework | Wrong method on an existing route | OPTIONS |
|---|---|---|
| Express 5.2.1 (tested) | 404, body Cannot POST /items | 200 with Allow: GET, HEAD |
| Flask (Werkzeug) | 405 with an Allow header listing the route’s methods | Answered automatically |
| Django class-based views | 405 with Allow; logs Method Not Allowed (POST): /path/ | 200 with Allow |
| Laravel | 405, The POST method is not supported for route ... Supported methods: GET, HEAD. | Handled by the router |
| Next.js 16 route handlers | 405, empty body, no Allow header | 204 with Allow |
| Plain PHP | Whatever your script sends; nothing by itself | Your 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:
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
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Allow: POST');
http_response_code(405);
exit("Method Not Allowed\n");
}
// handle the POST hereFramework-specific causes worth checking:
- HTML forms only send
GETandPOST. A form for a Laravel route defined withRoute::putorRoute::deleteneeds@method('PUT')(a hidden_methodfield) inside the form.php artisan route:listshows which methods each route takes. - Flask routes default to
GET. A view that handles a form needsmethods=["GET", "POST"]in its@app.route. - Django: a class-based view answers 405 for every method it has no handler for, and
@require_POSTor@require_http_methodsdo the same for function views. Add the handler, or fix the client’s method. - Next.js: a
route.tsmust export a function named after the method, such asexport 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:
<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 asGET; the comment handler then answers 405. Set both URLs under Settings, General to the exacthttps://address (details above). - Server rules. The nginx and
.htaccesschecks above, especially atry_filesfallback toindex.htmlinstead 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 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:
curl -s -i -X PUT -d 'x' \
--resolve example.com:443:203.0.113.10 https://example.com/api/items/1 | head -n 5The 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 Allowed | 501 Not Implemented | 403 Forbidden | 404 Not Found | |
|---|---|---|---|---|
| What it says | This URL does not take this method | The server does not support the method for any URL | Understood, but refused by a rule | Nothing exists at this path |
| Must include | An Allow header with the accepted methods | Nothing extra | Nothing extra | Nothing extra |
| Typical trigger | POST to a static page, PUT to a GET-only route | An unknown or WebDAV method on a server without it | A WAF rule, limit_except or <Limit> | A wrong path or a missing route |
| Cacheable by default | Yes | Yes | No | Yes |
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:
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.logLots 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.