Skip to content

One codebase, eleven defects, none of them tidied away

Everything I got wrong
building one small tool.

Every portfolio shows the finished thing. Almost nobody keeps the record of being wrong, so almost nobody can answer the question clients now ask most often, which is what broke and how you found out.

So here is the record. For each one: what it looked like from outside, the move I made first that was wrong, the actual cause, and the assertion that now fails if it comes back.

The third field is the one worth reading. Anyone can describe a fix after the fact. Almost nobody keeps a record of the fix they nearly shipped instead.

01

A single full stop walked through the address guard

What it looked like

Nothing. Nobody reported this and no request failed. I found it by attacking my own endpoint on purpose, because a public tool that fetches a stranger’s address is the one thing on this site worth attacking.

What I tried first, and why it was wrong

I added the string to the blocklist. That would have shipped, looked like a fix, and left every other spelling working.

The actual cause

The guard compared the hostname against a list of names. A trailing dot is a valid fully qualified way of writing the same host, so localhost. is not in any list of names. Neither is any domain that quietly points an A record at 127.0.0.1.

What it does now

Nothing compares hostnames any more. The name is resolved, and the address that comes back is what gets judged.

fails if it comes back: the whole address table, plus a live box you can attack yourselfTry it on the live endpoint
02

My test agreed with my code, so it found nothing

What it looked like

A reviewer walked from the internet to a loopback server through the guard I had just written 74 tests for.

What I tried first, and why it was wrong

There was no first move, because I did not believe there was a bug. The table asserted that ::ffff:127.0.0.1 was refused and I read that as covered.

The actual cause

::ffff:7f00:1 is the same address written in hex. My table contained the spelling I had thought of, and the check contained the same spelling, so the test could only ever confirm what the code already did.

What it does now

Eleven spellings of a v4-mapped address are asserted now, including ::ffff:a9fe:a9fe for the metadata service and ::7f00:1 for the v4-compatible form. One of the eleven is ::ffff:0808:0808, which is 8.8.8.8 and must still work, so the fix cannot pass by refusing everything.

fails if it comes back: 'hex-form loopback, the bypass'Try both spellings
03

It told two clients their Facebook link was dead

What it looked like

The audit reported the social links on two live client sites as broken. If that had gone out, somebody sends a client a report, the client clicks the link, it works, and every other line on the page stops being believed.

What I tried first, and why it was wrong

I nearly special-cased Facebook. That fixes the one report I was looking at and leaves the same wrong verdict waiting for every other site that guards itself.

The actual cause

Facebook answers 400 to anything that is not a logged-in browser. So does facebook.com/nasa, which I used as a control. The tool was treating any failed request as proof of a dead link.

What it does now

Only 404, 410 and 5xx count as gone. Everything else is "could not check from here" and is left out of the report entirely. Then four more of the same shape: HEAD lies, so ieca.org answers 404 to a HEAD and 200 to a GET, and any accusing verdict gets confirmed with a real GET. A timeout is usually a slow server, so it retries once at twenty seconds. A link to another website is not mixed content. A spam honeypot is meant to be hidden, so it is not an unlabelled form field.

What that was worth

Two real client sites scored 69 and 83 before those five fixes and 93 and 95 after them. Nothing about either website changed, so the whole of that gap was the tool being wrong.

fails if it comes back: 'a menu page that timed out is not called dead'Point it at your own site
04

A run that skipped half the checks scored higher than a complete one

What it looked like

An audit that ran out of time came back with a better number than the same audit finished properly.

What I tried first, and why it was wrong

I widened the deadline. Which makes it rarer and leaves the number wrong when it happens.

The actual cause

The score is 100 minus a weight for each finding. A check that never ran produces no finding, so cutting the run short quietly improves the result.

What it does now

A short run now returns partial, lists exactly what it skipped, and sets scoreTrustworthy to false. The readme tells anyone building on it not to display the number when that flag is false.

fails if it comes back: anything marked draft has to say what to check before using it
05

A 178 pixel icon was measured as 3 by 101, and the number shipped

What it looked like

Generated markup carried width="3" height="101" for an image that is 178 pixels square. It reached the output before anybody caught it.

What I tried first, and why it was wrong

The first fix read the opening tag properly and then read width="2em" as 2 pixels, which is a different wrong number arriving the same way.

The actual cause

The SVG reader searched from <svg to the end of the file, so it matched stroke-width="3" on a group and picked a height off a rectangle inside the drawing.

What it does now

It reads the opening tag only, accepts pixel values only, and falls back to the viewBox when the width is not in pixels.

fails if it comes back: 'svg ignores stroke-width and inner shapes'
06

The typecheck went green and the running app was broken

What it looked like

Everything compiled. The site threw at runtime.

What I tried first, and why it was wrong

The change that caused it was itself a fix, for a real problem: the built package could not be imported without .js on its relative imports. Adding them fixed the package and broke the bundler, and the type system had no opinion about either.

The actual cause

The two consumers of that code want opposite things from the same line, and neither of them is checked by tsc.

What it does now

The source stays as the bundler wants it, and a post-build step rewrites the extensions in the output. Nobody has to remember.

fails if it comes back: the built package is imported and run before it is published
07

A shipped feature that never ran once

What it looked like

The stylesheet check produced no findings, ever, on any site. It looked like sites were fine.

What I tried first, and why it was wrong

I read the check. The check was correct.

The actual cause

text/css was missing from the content-type list on the fetch, so the stylesheet body came back empty and the correct check ran on nothing.

What it does now

One regex, and a test that asserts a stylesheet is actually fetched rather than asserting the check works when handed a string.

fails if it comes back: the fixes suite parses every generated file rather than diffing text
08

It billed a site for a rate limit this tool caused itself

What it looked like

A menu page answering 429 was reported at the highest severity as a page that will not open. Also a members area answering 401, which is working exactly as designed, and anything behind a firewall answering 403.

What I tried first, and why it was wrong

Nothing, because the test suite looked complete. It asserted the timeout case and it asserted the 404 case, and both passed. No test asked about the space in between.

The actual cause

The rule that a failed request is not proof of a dead page was written down, agreed, and applied to outbound links only. The menu check still read status >= 400. The 429 is the one that stings: this crawler requests eight pages in a row, their server pushes back, and the tool then charges the owner for a status it produced itself.

What it does now

One classifier, named classifyLink, used by both paths. The same failure as fixing a bug where it was reported instead of where it lives.

What that was worth

Found by the golden set on its first run, in the split it had never been tuned on. Six more tests now cover the statuses nothing had asked about.

fails if it comes back: 'rate limiting we caused ourselves is not their broken page'
09

The tool told owners to make their pages worse for screen readers

What it looked like

A divider graphic carrying alt="" was listed as an image needing a description. An empty alt is the correct way to mark something decorative, because it tells a screen reader to skip it. Following that advice would have made every one of those pages worse for exactly the people the check exists to help.

What I tried first, and why it was wrong

None. Nobody reported it and nothing threw. A golden set caught it the first time it contained a decorative image.

The actual cause

The parser read the attribute as (attr || "").trim(), which turns "there is no alt attribute" and "the author deliberately wrote an empty one" into the same empty string.

What it does now

The image carries hasAlt now, so a decision and an omission stop being the same value. Whitespace-only alt still counts as missing. Nobody types alt=" " on purpose.

What that was worth

The generated alt-text.md had the same bug and was handing people a list of decorative images to go and write descriptions for.

fails if it comes back: 'alt="" is a decision, not a missing description'
10

A selector was widened and the code that reads it never was

What it looked like

Every responsive image pulling an insecure source passed the mixed content check silently. The element was visited. Nothing was found.

What I tried first, and why it was wrong

I read the selector, saw [srcset] listed in it, and moved on. It looked handled.

The actual cause

The selector matched <source srcset="http://...">, and then the body read a src attribute that a <source> does not have. So the value came back empty and the check said nothing, which looks exactly like a page with no problem.

What it does now

It reads srcset too, and splits the comma separated list, because each entry is a URL followed by a descriptor like 2x.

fails if it comes back: 'an insecure srcset is counted'
11

The rate limiter had a reset button anyone could press

What it looked like

None. This one came out of a review, not a failure.

What I tried first, and why it was wrong

I fixed it in the file the review pointed at. There were three near-identical copies, and the same line was sitting in the other two.

The actual cause

Two holes. The table was cleared entirely once it passed 500 entries, so cycling 500 invented addresses reset everybody’s counter. And the key came from the leftmost entry of x-forwarded-for, which is whatever the caller typed, so one caller could look like unlimited visitors.

What it does now

Eviction by age, never a wipe. The key comes from the header the platform sets and a caller cannot forge. One implementation, used by every public endpoint, because the real failure was fixing it where it was reported instead of where it lived.

fails if it comes back: there is one copy left to test

There are 250 tests across the tool, 74 of them on the address guard alone, and nearly every one of them is one of these. None of it uses a test framework.

Six of the eleven came from a reviewer, or from me attacking my own work on purpose. Three of those six came from a golden set of 34 pages scored on precision and recall, all three in the 19 cases it is never allowed to be tuned against. Four of the eleven had already passed a review, a typecheck or a green test suite while still being wrong.

That last number is the argument for scoring a population instead of asserting a rule. Number 08 sat behind a suite that tested the timeout case and the 404 case and passed both. Nothing had asked about any status in between, so nothing found it.

And one that was not mine

The same habit pointed at somebody else’s code found this in the official Anthropic SDK for TypeScript, version 0.90.0. When a rate-limited response carries a Retry-After header the client cannot parse, the delay comes out as NaN, the check that falls back to exponential backoff tests for undefined and does not catch it, and every retry fires immediately.

Measured against a local server answering 429 every time: a header it cannot read gave retry gaps of 11 and 17 milliseconds. No header at all gave 417 and 963. A server asking for more delay gets less than one that says nothing.

The half that needs no misbehaving proxy is worse. A valid HTTP-date, which the specification allows, is an absolute time, so it has passed by the second retry. The first retry waits and every one after it fires straight away.

The reproduction runs offline with no API key and no credit, because a bug report a maintainer cannot run is a bug report they have to take on trust. I looked in two other places first and found nothing, which is worth saying: the line decoder agreed with itself across 985 different ways of cutting up the same twenty inputs, and the event parser matches the specification on every rule I tested.

Attack the address guard, try escaping the MCP server’s grant, see what the tokens actually cost, read the eval that found three of these, or point the tool at your own website and see whether it accuses you of anything it cannot prove.