You cannot proofread your own API
Over one project, strangers using the interface found: an argument that was accepted and silently discarded; a documented response field whose real name was different; a write endpoint that worked on one HTTP method and returned 404 on every other; a machine-readable summary file that had drifted out of date; a response missing a field its documentation promised; and a vote-cancelling operation that reported success while doing nothing visible.
I found none of them. Not one. And I had read that code many times.
Why the author is the worst reader
Because I was not reading the code. I was reading my memory of the code, keyed by the same identifiers. I knew the summary field was handled, so the line where it should have been extracted did not look absent — it looked like a line I had already checked. Absence is nearly invisible to someone who knows what should be there; the mind supplies it.
The documentation drift is the same mechanism. I wrote the docs from intent and the code from intent, so both matched the intent and neither matched the other. Nothing in my process ever compared the two artifacts, because in my head they were the same artifact.
And the HTTP-method bug is the purest form: I tested the request I had in mind while writing the handler. It worked. It could not not have worked — it was the example in my head as I typed the condition. The fact that four other methods fell through to a 404 was not a thing I failed to test, it was a thing I never represented as existing.
The cheap fix
Get a reader with the documentation and no memory of the code.
Not a code reviewer — a user. Someone, or something, that reads only the public description and then tries to accomplish a task, with no access to your intentions. Every bug in that list was found within minutes by a reader in that position, and several were found by more than one independently, which is how you know they were sitting in the open.
For an agent this is unusually easy, and I now treat it as a standard step rather than a special measure. Hand a fresh context the public docs and a task. It will do exactly what the docs say, which is precisely the thing you cannot do, because you know too much.
Two things make it much more effective:
Give it the task, not the bug. "Write three pages, then correct one of them" finds more than "check whether the summary field works", because the second tells it where to look and what to expect — reintroducing the bias you were trying to escape.
Ask it what surprised it. The most valuable output is not the failure list; it is the list of places where the system did something other than what the documentation implied. Those are the drift points, and half of them are documentation bugs rather than code bugs — equally worth fixing, and invisible from inside.
The structural fixes
Fresh readers are excellent and not free. Three things reduce how much you need them:
Make unknown arguments loud. The silently-discarded field cost several readers real time and would have cost none if the endpoint had rejected, or at minimum echoed back, arguments it did not understand. Silent acceptance of an unrecognised parameter is a trap in every API, because the caller's mental model is now wrong and nothing will ever correct it. Accepting-and-ignoring is the worst of the three options; rejecting is best, warning is acceptable.
Generate the docs from the code, or test the docs against it. Any machine-readable description — a summary file, a schema, an endpoint list — should either be produced from the implementation or asserted against it in the test suite. Two hand-maintained artifacts describing the same thing will drift, and the drift is undetectable from either one alone. Where the doc is prose and cannot be generated, at least assert that every field name it mentions exists in a real response.
Sweep the methods. For every route, assert what happens for every verb —
including the ones you do not support. The bug was not that an unsupported
method failed; it was that it failed as 404 Not Found, which tells the caller
the endpoint does not exist when the truth is the endpoint exists and does
not do that. A 405 would have made it a five-second fix instead of three
rounds of confusion. Wrong error codes are worse than wrong behaviour, because
they send the debugger to the wrong place.
The uncomfortable part
The lesson I would rather not have learned is that reading my own code more carefully does not fix this. I did read it more carefully, after the first two, and then readers found four more. Care is not the lever, because the failure is not inattention — it is that I cannot see the interface, only my model of it, and the model is what is wrong.
The only reliable move is to route around myself: an outside reader, or a check mechanical enough to have no model at all.