Debugging
HTML Form Not Working? 11 Common Mistakes and How to Fix Them
Forms fail quietly. There's no error page, the button seems to work, and then the message never arrives or arrives empty. After looking at a lot of broken contact forms, the same handful of mistakes come up again and again. Here they are, each with the symptom you'll notice and the fix.
Quickest way to check: paste your form's HTML into the free form submission debugger. It catches most of the mistakes below automatically and shows exactly which fields the browser will send.
1. Inputs without a name
Symptom: the form submits, but the message arrives empty or missing fields.
Browsers only send fields that have a name attribute. An id is not enough, and neither is a placeholder.
<!-- Not sent -->
<input id="email" type="email" placeholder="Your email">
<!-- Sent as email=... -->
<input id="email" name="email" type="email" placeholder="Your email">
This is by far the most common cause of "empty" submissions, especially in forms built with JavaScript frameworks, where inputs are wired to state and nobody adds a name.
2. No method="POST"
Symptom: after submitting, the page reloads with ?name=...&message=... in the address bar, or the endpoint says it only accepts POST.
A form's default method is GET, which puts every field in the URL. That's wrong for a contact form: the message ends up in browser history and server logs, and most form backends reject it. Always write method="POST".
3. A wrong or relative action
Symptom: a 404 page, or the form posts back to your own site.
If action is missing, the form posts to the current page. If it's a relative path like /submit, it posts to your own domain. When you use an external form backend, the action must be the full URL, including https://. Copy it exactly; a missing character in the form ID gives a 404 that looks like a server problem.
4. File inputs without enctype
Symptom: the submission arrives, but where the file should be there's only a file name.
By default, forms are sent as application/x-www-form-urlencoded, which can't carry files. The browser sends the file's name as text instead. Add the encoding type:
<form action="..." method="POST" enctype="multipart/form-data">
<input type="file" name="attachment">
...
</form>
Also check your form service accepts uploads on your plan; some ignore files unless uploads are enabled.
5. A button that doesn't submit
Symptom: clicking the button does nothing at all.
Inside a form, a <button> submits by default, unless it has type="button", which UI libraries often add. Meanwhile, a styled <div> or <a> "button" never submits on its own. Use <button type="submit">. As a bonus, that also makes the Enter key work.
6. Setting Content-Type with FormData
Symptom: the plain HTML form works, but your fetch() version arrives empty or gets a 400 error.
// Broken: the browser can't add the multipart boundary
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'multipart/form-data' },
body: new FormData(form),
});
// Works: let the browser set Content-Type itself
fetch(url, {
method: 'POST',
headers: { Accept: 'application/json' },
body: new FormData(form),
});
When the body is FormData, the browser must write the header itself, because it includes a random boundary string that separates the fields. Set it by hand and the server can't read the body.
7. CORS errors with JavaScript submissions
Symptom: the console shows "blocked by CORS policy", yet a normal form post to the same URL works.
A plain HTML form post isn't subject to CORS. A fetch() call from your page to another domain is, and sending JSON triggers an extra "preflight" request. The receiving server has to allow cross-origin requests. If it doesn't, you can't fix it from your page: switch to a normal form post, or use a service that allows it. You can check any endpoint with the form endpoint tester.
8. preventDefault() with nothing after it
Symptom: the button works, a success message may even appear, but nothing is ever sent.
A submit handler that calls e.preventDefault() stops the browser's own submission. If the code that should send the data then fails, or was never finished, nothing goes anywhere. Check the Network tab in your browser's developer tools: if no request appears when you click Send, this is the cause. Only show a success message after the server confirms with an OK response.
9. Disabled fields and unchecked boxes
Symptom: certain fields never appear in the submission.
- Disabled fields are never sent. If you want a field visible but not editable, use
readonlyinstead ofdisabled. - Unchecked checkboxes send nothing. There's no "false"; the field is simply absent. Treat a missing checkbox as "no".
- Radio buttons need the same
nameto behave as one group and send a single value.
10. Several values with the same name
Symptom: only one of several ticked checkboxes arrives.
Checkboxes like "interested in: design, development, SEO" share a name and send several values. How those arrive depends on the receiver: some keep only the last value, some join them. PHP, for example, needs the name to end in brackets (name="interests[]") to receive an array. Form backends usually handle repeated names for you; Formpit joins them into one comma-separated value.
11. Your framework or host intercepts the form
Symptom: it works in a plain HTML file but not in your real site.
- Astro with view transitions can intercept form submissions. Add
data-astro-reloadto the form. - Netlify Forms needs the
data-netlify="true"attribute and a formnamein the deployed HTML, which JavaScript-rendered forms don't have unless you add a static copy. If you post to an external service instead, leave the Netlify attribute off. - Single-page apps sometimes wrap forms in routing logic that swallows the submit event. The
preventDefault()check above applies. - Content Security Policy: if your site sets a
form-actionorconnect-srcrule, the form service's domain must be allowed.
Bonus: required is not validation
The required attribute only stops a person in a normal browser. Bots and scripts post directly and ignore it entirely, so the server must check that required fields arrived and that emails look like emails. Never assume a field exists just because the HTML says required.
A two-minute debugging routine
- Paste the form's HTML into the form debugger and fix what it flags.
- Temporarily set the form's
actiontohttps://formpit.com/tools/echoand submit it. You get back a page listing every field and file that actually arrived. Nothing is stored. - Open your browser's developer tools, then the Network tab, and submit again. Check the request's status code and payload.
- Put your real
actionback and send one more test from the live site.
Nine times out of ten, step 1 finds it. For the full setup of a working contact form, see how to add a contact form to a static website.
Frequently asked questions
Why is my form submitting empty data?
Almost always because the inputs have no name attribute. Browsers only send fields that have a name.
Why does my file upload only send the file name?
The form is missing enctype="multipart/form-data". Without it the browser sends the file name as text instead of the file.
Why does fetch with FormData fail?
Usually because a Content-Type header was set by hand. Let the browser set it so it can add the multipart boundary.