D
P
0
← All articles Baca dalam Bahasa Indonesia

Next.js & React in Production

`?email=name@domain.com` in the URL Bar: A Newsletter Form That Shipped Without a Handler

· · 6 min read
`?email=name@domain.com` in the URL Bar: A Newsletter Form That Shipped Without a Handler

This bug did not surface in an error log. It surfaced because a visitor idly clicked the time range buttons on a chart panel and mentioned that nothing happened. Small complaint, but it planted a bigger question: if those buttons are dead, how many others are?

The audit that followed turned up something considerably worse than a dead button.

The symptom: a visitor's email landed in the URL bar

The homepage had a newsletter signup form. Clean input, high contrast SUBSCRIBE button, every visual cue of a feature that works. I typed an email address and hit Enter.

The page reloaded. The URL bar now read something like this:

https://client-site.example/?email=name%40domain.com

The address I had just typed was sitting in the URL bar, saved into browser history, written whole into the server access log, and readable by any analytics script that reports the page URL as it finds it. No email was actually collected anywhere, of course, because nothing was there to collect it. All that happened was a one way leak.

Why it happens: a form with no action submits to itself

This is not a framework bug. It is baseline HTML behaviour and it has always worked this way. When a <form> has no action, the browser targets the current page URL. When it has no method, the default is GET. And GET means every field with a name attribute gets serialised into the query string.

So this perfectly innocent looking markup:

<form className="flex gap-2">
  <input type="email" name="email" placeholder="Your email" />
  <button className="bg-white text-black px-4 py-2">SUBSCRIBE</button>
</form>

effectively says: "send this form's contents to this same page, through the URL." A <button> inside a <form> with no type also defaults to type="submit", so any click in there fires a native submit.

No onSubmit, no action, no endpoint. Just a browser doing its job faithfully.

The root cause: mockup phase UI that shipped half wired

That form was born while the page was still a mockup. Its only purpose at the time was to show what a newsletter block would look like in the layout. The wiring was scheduled for a later phase, and so was the backend.

The later phase had not arrived. The page had.

This is a pattern I keep meeting on projects built in stages: elements created for visual purposes ride along into production without ever being marked as inactive. During that audit I swept the whole site and found 12 surfaces with the same illness, from the chart range filters that prompted the original complaint to the newsletter form leaking emails. Every one of them looked active. Not one was connected to anything.

What makes this class of bug expensive is that it never shows up in an error log, never turns a build red, never trips a test. From the machine's point of view nothing is broken. The only thing broken is the promise the UI is making to the visitor.

Two quick fixes that make it worse

The reflex when you meet a form like this is to patch the submit:

<form onSubmit={(e) => e.preventDefault()}>

That does stop the URL leak. But the end result is worse as an experience: now the button is clickable, looks active, and does absolutely nothing. No reload, no message, no feedback of any kind. The visitor assumes they did something wrong and clicks three more times.

The second reflex is equally misleading:

<a href="#">Coming soon</a>

An anchor pointing at # is still announced as a link by screen readers, still lands in the tab order, and still mutates the URL. Same lie, different costume.

Both of these hide the symptom without touching the actual problem, which is UI that misrepresents its own status.

The fix: an honest disabled state

What I applied across all 12 surfaces was one recipe, and the important part is not the CSS. It is the decision to stop pretending.

<button
  type="button"
  disabled
  title="Email signup launches with Phase 3"
  className="... opacity-60 cursor-not-allowed"
>
  SUBSCRIBE · soon
</button>

Four pieces, each with a job:

type="button" removes the element from default submit behaviour, so clicking it no longer fires a native submit even while it is still wrapped in a <form>.

The disabled attribute is the one doing the real work. It does more than grey things out: it pulls the button out of the tab order, blocks clicks at the browser level, and makes screen readers announce the control as unavailable. It is the only part of this recipe that works for keyboard users and assistive tech, not just for people looking at pixels.

opacity-60 cursor-not-allowed carries the visual signal. The cursor change on hover matters more than it sounds, because it answers the visitor's question before they spend a click on it.

The · soon label suffix is the part people skip. A title tooltip only appears if you hover with a mouse and wait, which makes it useless on touch and unreliable as the only explanation. Text inside the label is read by everyone, screen readers included. The tooltip is there for extra context, not as the load bearing part.

For the newsletter form specifically, the input got disabled too, and that part is not just tidiness. I triggered the original symptom by pressing Enter, not by clicking, so type="button" alone would not have been enough. HTML has implicit submission: a form holding a single text field still submits when you press Enter in that field, even with no submit button anywhere inside it. The disabled input is what actually closes that path, because a dead field cannot be focused and is never submitted.

The rules I keep now

That audit left me with two hard bans on any project built in stages:

  1. Never <form onSubmit={(e) => e.preventDefault()}> behind a button that looks active. If it does not work yet, say it does not work yet.
  2. Never <a href="#"> as a placeholder. If there is no destination, it is not a link.

Plus one habit: every time a page graduates from mockup to production, walk its interactive elements one by one and ask whether each is actually connected to something. A quick grep for href="#", <form without an onSubmit, and <button> without a type usually rounds up the suspects.

What I took away