A contact form was posting and leaving nothing behind. No message stored, no e-mail sent, no thread created — and no error either. The page just reloaded, empty and calm, as if the visitor had never pressed anything.
The cause is a one-line detail about how forms are submitted from JavaScript, and it is not specific to the framework we hit it in. If any part of your backend decides whether to act by testing for a submit button's name, you can hit this too.
What the code did
The form was protected by an invisible CAPTCHA. Tokens for that kind of CAPTCHA expire in about two minutes and are single-use, so the sensible design is to fetch a fresh token at the moment of submit rather than on page load:
$form.on('submit', function (e) {
e.preventDefault();
grecaptcha.execute(siteKey, { action: 'contact' }).then(function (token) {
$form.find('[name="g-recaptcha-response"]').val(token);
$form.trigger('submit'); // <- here
});
});
Intercept, get the token, put it in the hidden field, submit for real. That is the standard shape, and it appears in a lot of tutorials.
What actually goes over the wire
$form.trigger('submit') does not simulate a click. jQuery, finding no more handlers to run, calls the element's native form.submit().
A native submit has no submitter. The HTML spec is explicit that the entry list is built from the form's controls, and a submit button contributes its name/value pair only when it is the button that activated the submission. Nothing activated anything here, so:
- the clicked button's
nameandvalueare simply absent from the POST body; - every other field is present and correct.
So you get a POST that looks complete in the Network tab. Unless you know which key is missing, nothing jumps out.
Why that is fatal for some backends
Plenty of server code — PrestaShop, older WordPress plugins, hand-rolled PHP, anything with several forms posting to the same controller — decides what to do by asking which button was pressed:
if (Tools::isSubmit('submitMessage')) {
// validate, store, send mail
}
isSubmit('submitMessage') is a presence test on $_POST['submitMessage']. With the submitter dropped, that key is not there, the branch never runs, and the controller falls through to rendering the page again.
This is the worst possible failure shape:
- No error. The controller did not fail; it was never asked to do anything.
- No validation message. Validation lives inside the branch that did not run.
- No log line. Nothing threw.
- The CAPTCHA looks innocent. Our module's own "blocked submission" counter never moved, which was the clue that finally mattered: the request had not even reached the verification step. We had been tuning the score threshold for a week. The threshold was never involved.
We reproduced it on PrestaShop 9.1.4: the contact form posted, left no thread, no message and no mail, and displayed nothing.
The fix
Re-attach the activating button before submitting:
function submitPreservingButton($form, event) {
var form = $form.get(0);
if (!form || form.tagName !== 'FORM') {
form = $form.closest('form').get(0);
}
if (!form) { $form.trigger('submit'); return; }
var submitter = (event && event.originalEvent && event.originalEvent.submitter)
|| form.querySelector('button[type="submit"][name], input[type="submit"][name]');
if (submitter && submitter.name
&& !form.querySelector('input[type="hidden"][name="' + submitter.name + '"]')) {
var keep = document.createElement('input');
keep.type = 'hidden';
keep.name = submitter.name;
// Buttons often carry an empty value; a presence test is happy either
// way, but an empty string is easy to lose in transit.
keep.value = submitter.value || '1';
form.appendChild(keep);
}
form.submit();
}
Three details worth keeping:
-
event.submitterfirst. Modern browsers put the activating element on the submit event. Use it when it is there; it is the only source that is actually correct when a form has several named submit buttons. - Fall back to the first named submit control. Older browsers, and synthetic submits with no event, need something. It is a guess, but on a single-button form it is the right guess.
- Do not add the hidden input twice. A user who submits, fails validation and submits again would otherwise accumulate duplicates.
If you are not carrying jQuery for other reasons, the modern equivalent is form.requestSubmit(submitter), which does the right thing natively — it fires the submit event and includes the submitter. It is widely supported now. requestSubmit() is what form.submit() should always have been.
The part that generalises
Two APIs that look interchangeable are not:
fires submit event |
includes submitter | |
|---|---|---|
| user clicks the button | yes | yes |
form.requestSubmit(btn) |
yes | yes |
form.submit() |
no | no |
$(form).trigger('submit') with no handlers left |
ends in form.submit()
|
no |
And a backend convention that looks harmless — "check which button was pressed" — turns that difference into a silent no-op.
If you are debugging a form that posts and does nothing, before you touch validation, tokens or thresholds: open the Network tab, look at the actual form data, and check whether the submit button's own name is in there. It is a ten-second check that we did not do for an embarrassingly long time.
Found while maintaining PrestaShop modules at MEG Venture. The same shape affects account creation, newsletter sign-up and checkout on that platform, since all four are gated on a button name.
Top comments (0)