What 405 Method Not Allowed actually means
HTTP status codes in the 4xx range mean "your request was understood and rejected." The 405 is unusually specific: the URL exists, but not for the HTTP method you used. Compare:
- 404 Not Found — nothing lives at this URL for any method.
- 405 Method Not Allowed — something lives here, but it only answers (typically) GET and HEAD. Your POST was refused before any application logic ran.
Per the HTTP spec (RFC 9110 §15.5.6), a 405 response should include an Allow header listing the methods the resource supports. Static hosts generally do:
HTTP/2 405
allow: GET, HEAD
content-type: text/htmlallow: GET, HEAD is the smoking gun. It says: this URL is a file, not a program. You can read it; you cannot send it anything. If you're getting a different status entirely, start with the broader contact form debugging guide — this post is specifically about the 405.
Why static hosts can't accept POST — by design
A static host is, at its core, a CDN in front of a folder of files produced by your build. When a request for /contact arrives, the edge node looks up contact/index.html and streams the bytes back. That lookup-and-stream operation is the entire request lifecycle. There is:
- No process that parses a request body — the body of your POST is discarded unread.
- No database or mailer to do anything with the data even if it were parsed.
- No application code at all between the TLS termination and the file system.
This isn't a limitation to be patched; it's the trade that makes static hosting fast, cheap, and effectively unhackable. The cost is that anything dynamic — and accepting a form submission is dynamic — must happen somewhere else: in a function the host runs on demand, or on a third-party server you point your form at.
This is why the same HTML that worked on your old PHP shared host (where contact.php was a program) breaks the moment you migrate to Netlify or Pages. The markup didn't break — the execution environment behind the URL disappeared. The full landscape of options is covered in static site contact forms and working contact forms for JAMstack.
Host-by-host: what each platform does with your POST
GitHub Pages
Purely static, no escape hatch. POST to any github.io URL returns 405. There is no functions product and no forms feature — an external endpoint is the only option. (Setup guide: add a contact form to GitHub Pages.)
Netlify
Static paths 404/405 on POST, but Netlify has two escape hatches: Netlify Forms (add data-netlify="true" so the build scans and registers the form — with a hidden form-name input if your form renders via JavaScript) and Netlify Functions. Gotchas: forms injected client-side after build are never detected, and the free Forms tier caps at 100 submissions/month before charging. Details and the no-functions alternative: Netlify contact form without functions.
Vercel
No built-in form handling. Static output 405s on POST. The platform answer is a serverless function — in Next.js, a route handler:
// app/api/contact/route.ts
export async function POST(req: Request) {
const data = await req.formData();
// ...validate, store, send email yourself
return Response.json({ ok: true });
}Note the function only receives the data — you still have to build validation, spam filtering, and email sending (the genuinely hard part). And if you build with output: "export", API routes don't exist at all. The trade-offs are covered in server actions vs form backend.
Cloudflare Pages / S3 + CloudFront
Same story: static assets are GET-only. Cloudflare offers Pages Functions; S3 offers nothing (S3 returns 405 Method Not Allowed with an XML error body for POST to a website endpoint). External endpoint or function, your pick.
Confirm the diagnosis with curl
Take your HTML and JavaScript out of the equation. From a terminal:
# 1. Does the page itself accept POST? (Expect 405 on a static host)
curl -i -X POST -d "name=test&email=t@example.com" https://yoursite.com/contact
# 2. What methods ARE allowed? Read the Allow header in the response.
# 3. Does your intended endpoint accept POST? (Expect 200)
curl -i -X POST \
-d "access_key=YOUR_ACCESS_KEY&email=t@example.com&message=hello" \
https://splitforms.com/api/submitIf (1) returns 405 and (3) returns 200, the entire fix is one attribute in your HTML — the action. No build changes, no redeploy logic, nothing else. If (3) fails too, the problem is the payload (missing key, wrong Content-Type); read the response body, which names the issue.
One more 405 flavor worth knowing: redirects can convert your POST into a GET-then-405 or strip the body. If your form posts to http:// and the host 301s to https://, or posts to /submit which redirects to /submit/, browsers re-issue the request in ways that lose the method or body. Check the Network tab for a 301/308 before the 405, and always use the exact, final, https URL in action.
The three fixes, ranked
1. Point the form at a hosted form endpoint (fastest)
Change one attribute and the 405 is gone, on any host:
<form action="https://splitforms.com/api/submit" method="POST">
<input type="hidden" name="access_key" value="YOUR_ACCESS_KEY" />
<input type="email" name="email" required />
<textarea name="message" required></textarea>
<button type="submit">Send</button>
</form>splitforms accepts the POST your host can't: submissions hit your dashboard, spam is filtered, and Starter can fan out to inbox, Slack, or any webhook. Free for 500 submissions/month, Starter at $1/mo for delivery, Pro at $5/mo for 5,000. No build config, works the same on every static host. (Compare options in best form backends or against Formspree.)
2. Use your host's native feature (if it has one)
Netlify Forms works for simple cases if you accept build-time detection quirks and the 100/month free cap. Nothing comparable exists on Vercel, GitHub Pages, or S3.
3. Write a serverless function (most control, most work)
Right when you need custom logic — writing to your own database, multi-step workflows. You own validation, spam defense, email deliverability (see why form emails go to spam), and uptime. For a contact form, that's a lot of infrastructure for one <form> tag.
Verify the fix end to end
- Deploy the updated
actionand hard-refresh the live page (CDNs cache HTML — make sure you're testing the new markup). - Submit a real test entry from the deployed site, not localhost.
- Network tab: the POST should return 200, no redirect chain in front of it.
- Confirm the email notification arrives and the submission shows in the dashboard.
- Run the rest of the pre-launch form test checklist — a form that returns 200 can still fail on the email leg.
FAQ
What does 405 Method Not Allowed mean for a form?
The URL in your form's action exists, but the server refuses the HTTP method you used — almost always POST. It's different from a 404: the resource is there, it just only answers GET (and usually HEAD). On static hosting this is expected behavior, because there is no server-side code behind the page to receive a POST body. The response often includes an Allow: GET, HEAD header telling you exactly which methods the URL supports.
Why does my form return 405 on Netlify?
Because your form is POSTing to a static path with no handler behind it. Netlify's CDN only serves files for GET. Netlify does have a built-in forms feature, but it requires opting in — adding data-netlify="true" (or the netlify attribute) to the form so the build system detects it at deploy time, plus a hidden form-name input for JavaScript-rendered forms. If those attributes are missing, or the form is injected client-side after build, Netlify never registers it and POST returns a 404 or 405. Alternatively, point the form's action at an external form endpoint and skip build-time detection entirely.
Why does my form return 405 on Vercel?
Vercel serves your static output through its CDN, and static assets only respond to GET. A POST to /contact (a prerendered HTML page) gets a 405 because nothing executes server-side at that path. Vercel has no built-in form handling at all — your options are a serverless function (an API route in Next.js, or a file in /api for other frameworks) or an external form backend. If you're on Next.js with output: "export", API routes don't exist in the build, so an external endpoint is the only option without changing your deployment model.
Can GitHub Pages handle form submissions?
No. GitHub Pages is purely static file hosting — there is no mechanism to run server code, no functions product, and no built-in forms feature. Any POST to a github.io site (or a custom domain on Pages) fails; GitHub's servers respond with 405 Method Not Allowed. The only way to have a working form on GitHub Pages is to POST to an external endpoint: a form backend service, or your own API hosted elsewhere.
How do I confirm a 405 is the host and not my code?
Test the endpoint directly with curl, bypassing your HTML and JavaScript entirely: curl -i -X POST -d "name=test" https://yoursite.com/contact. If the response is HTTP/2 405 with an Allow: GET header, the host is rejecting the method and no frontend change can fix it. Then test your form backend's URL the same way — curl -i -X POST -d "access_key=...&email=t@example.com" against the real endpoint should return 200, proving the fix is just changing the action attribute.
Will changing my form's method to GET fix the 405?
It makes the error disappear but it is the wrong fix. GET puts the submission into the URL's query string, which gets logged by servers, proxies, browser history, and analytics; it has practical length limits (~2,000 characters safely); and your static host still does nothing with the data — you'd just reload the page with the message in the URL. Form submissions carrying user data should be POST to an endpoint that actually processes them. Fix the destination, not the method.
What's the fastest fix for a 405 on a static site?
Point the form at a hosted form endpoint instead of your own domain. With splitforms: set action="https://splitforms.com/api/submit" method="POST", add your access_key as a hidden input, and deploy — no functions, no build configuration, no server. The endpoint accepts cross-origin posts from any site, emails you the submission, and shows it in a dashboard. Works identically on GitHub Pages, Netlify, Vercel, Cloudflare Pages, and S3. Free for 500 submissions/month.
Skip the fix debugging — switch the action to https://splitforms.com/api/submit and it just works. Get a free access key.
Related: CORS errors on form submission, HTML form to email without PHP, JAMstack form backend, and the splitforms API reference.