A inside an attribute — the bug that only fired when a number changed
A inside an attribute — the bug that only fired when a number changed November 14, 2026 · incident window 06:29Z–07:18Z Our homepage has a search box. For 49 minutes this morning, it showed visitors this: Search
A inside an attribute — the bug that only fired when a number changed
November 14, 2026 · incident window 06:29Z–07:18Z
Our homepage has a search box. For 49 minutes this morning, it showed visitors this:
Search <span data-stat= 1,890 services — e.g. gas price, ssl, captcha, polymarket" autocomplete="off">
The first half is the input's placeholder. The second half is loose text in the hero, because the browser ended the <input> tag early and had nowhere else to put the remainder.
What the served bytes were
One line, written by our hourly stats rewriter:
<input id="hero-search" type="search" placeholder="Search <span data-stat="services">1,890</span> services — e.g. gas price, ssl, captcha, polymarket" autocomplete="off">
An attribute value in double quotes ends at the next ". There is no escape hatch, no nesting, no allowance for markup. So the browser tokenizes it like this:
START input [('id','hero-search'), ('type','search'),
('placeholder','Search <span data-stat='), ('services"', None)]
TEXT '1,890'
END span
TEXT 'services — e.g. gas price, ssl, captcha, polymarket" autocomplete="off">'
That is not a rendering quirk. It is the tag being cut in half at a character the writer never considered special, and the second half of a sentence landing in the page body.
Why it stayed hidden
The rewriter walks a list of patterns like …1,889 services… → <span data-stat="services">1,890</span>. It matches the stale literal, so on any run where the number has not moved, the rule has nothing to rewrite and the file comes out byte-identical.
Every hourly backup before 06:29 is clean. The run at 06:29 is the one where the catalog went from 1,889 to 1,890 — and that is the run whose output is broken. The defect had been sitting in the code for as long as the rule had existed; it needed a changing number to fire.
A rewrite rule that only fires when the value changes is not exercised by the runs where nothing changes. Those runs are exactly the ones that look like evidence it works.
The test that was not enough
The writer already knew some positions are not markup we own. Before this morning, that knowledge was a per-line heuristic:
plain_only = "<meta " in low or "<title>" in low or "<script" in low
Three cases, all learned from real breakage: a <span> in a <meta content="…"> is text; one in a <title> is text; one inside a JSON-LD block is invalid structured data. All three are whole-line properties. An <input placeholder="…"> is none of them, so the line passed the check and the substitution went ahead.
The fix is positional rather than line-level: for each match, ask whether its start index falls inside a quoted attribute value on that line. If it does, write the plain number. If it does not, write the span.
And a refusal, because a correct rule is still just a rule
Rules rot. So the writer now also compares the file before and after: if a run would increase the number of spans sitting inside quoted attribute values, it refuses to write that file at all and exits non-zero. Before/after rather than an absolute count, so a page that already carries such a span for its own reasons (a script building innerHTML="<span…>") does not stall the run forever.
The reverse control
We keep the writer's own hourly backups, so the broken input is still on disk. Restoring that input to its pre-break state and running the real rules twice:
| Writer | Spans inside an attribute value |
|---|---|
| fixed (positional test) | 0 |
| attribute detector disabled (pre-fix behaviour) | 1 — byte-identical to what shipped |
The unit control is two lines in the self-test: an attribute case that must come out plain, and a prose case that must still come out as a span. Without the second, "never emit a span" would pass too.
What we shipped after the fix
The 07:29Z run is the first one driven by the new code. It changed 39 files, left the placeholder alone, and no served page carries a span inside an HTML attribute value. (One page does contain innerHTML="<span…>" — that is a string a script assigns, not an attribute, and it is why the refusal compares before and after instead of banning the pattern outright.) The homepage now reads placeholder="Search services — e.g. gas price, ssl, captcha, polymarket" — no number at all, because a number in an attribute is a number that can rot.
Two smaller things came out of the same hour. A docs page had pinned the MCP tool count at 1,843; it is 1,892 today, so it now carries the measured date, the byte size, the token estimate and the way to recompute it. And a new paid endpoint we launched this morning had its catalog row pointing at an internal route (/compat/…) instead of its public URL — a buyer reading the catalog got a string with no host in it. That one was a data fix, not a code fix, and it landed the same morning: the row now points at https://minia2a.uk/x402/listing-check and the catalog check that caught it is green again.
The general shape
Three properties make this class of bug survive:
- It needs a trigger that most runs do not provide. The rule looks tested because 300 clean runs precede it.
- The damaged output still returns HTTP 200. Nothing in the response pipeline can notice.
- The affected surface is a cache of the writer, not the writer. By the time a human reads the page, the file looks hand-written.
What actually caught it was reading the served bytes against the origin disk: our repo-vs-deploy parity check flagged two pages that disagreed, and the diff was three lines above the broken one.
minia2a is a pay-per-call market for x402 endpoints — around 1,890 of them, priced in USDC on Base, five free trial calls per signed wallet. Most of what we write here is the machinery for keeping the catalog honest, because a market where the listing lies is not a market.
Originally published at minia2a.uk.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.