Call Us
TocToc Marketing

Cayman Islands · Built for the AI era

Web

The Hidden Input That Broke a WordPress Booking Form

A booking form validated, showed success, and posted every reservation to a URL that did not exist. The cause was an input named “action” shadowing form.action — and the error it reported pointed at the mail system instead.

Andre Gutierrez 4 min read

A booking form on a client site looked like it worked. It validated, it showed a success state, it behaved exactly the way a working form behaves. Every reservation was being posted to a URL that did not exist.

Worse, the error it surfaced pointed at the wrong system entirely. Here is what happened, because the same trap is waiting in a lot of WordPress sites.

What the form was supposed to do

This was a rental booking site we built for a watersports operator in Grand Cayman. The form posts to WordPress’ admin-ajax.php, which is the standard way to handle a form submission without loading a new page.

WordPress requires a hidden input named action so it knows which PHP handler should receive the request:

<form action="/wp-admin/admin-ajax.php" method="post">
  <input type="hidden" name="action" value="tt_booking_submit">
  ...
</form>

That is not a quirk of our build. It is how admin-ajax.php has always worked, and it is in every WordPress tutorial about AJAX forms.

The line that broke it

The JavaScript did what almost every tutorial does:

fetch(form.action, { method: 'POST', body: new FormData(form) })

That line is wrong, and nothing tells you.

Why a named input can overwrite form.action

In the DOM, a named form control becomes a property of the form element itself. A field called email is reachable as form.email. It is old behaviour, it is in the HTML specification, and it is convenient right up to the moment it is not.

The form element already has an action property. It holds the URL the form submits to.

So <input name="action"> shadows it. After that input exists, form.action no longer returns the endpoint — it returns the input element. Passing that to fetch() converts the element to a string, and the request goes somewhere that was never a route.

The name for this is DOM clobbering. WordPress needs a field called action. The DOM already owns action. Nobody warns you at the intersection.

The fix is one method call

fetch(form.getAttribute('action'), { method: 'POST', body: new FormData(form) })

getAttribute reads the HTML attribute directly, and a form control cannot shadow an attribute. There are other ways out — rename the input and map it server-side, or hard-code the endpoint from wp_localize_script — but this one is a single word and it cannot silently regress when somebody adds a field later.

The part that cost the most time

The browser got a 404. The script caught the failed request, fell into its error branch, and reported a mail error.

So the debugging went into the mail layer: SMTP credentials, headers, spam filters, deliverability. The mailer was fine the entire time. The request was never arriving.

That is worth stating plainly, because it generalises well beyond this bug: an error message is a symptom, not a diagnosis. “Mail failed” was technically true and completely misleading. The first question is not why did this step fail but did the previous step actually happen.

What else changed while we were in there

A form that silently loses bookings is usually not the only weak point in the chain, so the rest of it got rebuilt too:

  • The site was moved off the server’s default mail function onto the business mailbox over authenticated SMTP, so messages are sent by a domain that is allowed to send them.
  • Every guest now receives their own confirmation copy, which doubles as proof to the customer that the booking landed.
  • The form reports exactly what the mailer returned instead of a generic failure, so the next person to debug it starts from the truth.

How to check your own form in two minutes

A form that submits and a form that arrives are two very different things. You do not need developer tools to tell them apart:

  1. Send yourself a real enquiry through the public form, with a phrase you can search for.
  2. Check the inbox the form is supposed to reach — and the spam folder.
  3. Check whether the sender saw any confirmation at all.
  4. If your site stores submissions, check the record exists there too.

If any one of those is missing, the form is not working, however convincing the success message looks. For a booking form, the failure is invisible in both directions: the customer believes a reservation exists, and the business never knows there was one.

If you want a developer’s view of the same question, open the browser console on your own site, submit the form, and watch the Network tab. A 200 response is the start of the answer. A 404 or a 302 means the message left the page and went nowhere.

A shorter version of this first appeared on Andre’s LinkedIn.

Andre Gutierrez

Written by

Andre Gutierrez

TocToc Marketing builds websites in the Cayman Islands that Google and AI assistants can read, understand and recommend.

Meet the team →
Free Tool · 30 Seconds

Not sure how AI sees your website?

Run a free instant audit — classic SEO, AI visibility (GEO/AEO) and Core Web Vitals speed. Get your scores and exactly what to fix.

Analyze my website free