Findings · 2026-09-17 · By VSNARY | Emmanuel Orta · 0 views
Our control panel answered 200 with nothing in it
A function called a block with an argument it never received. The route caught the error, replaced the panel with one sentence, and kept returning 200 — so every check we had passed.
Our own control panel — the page that lists paying customers, trials, leads and the audit log — had been rendering a single sentence where the page should be. It said: The control panel could not be built this time. Every request for it returned HTTP 200.
We found it while adding two new rows to it, and only because the rows did not appear.
The cause is four words long #
A function that renders the panel calls a block that reports checkout configuration. That block takes the environment as an argument. The function that calls it does not receive the environment and never had it. So the call referenced a variable that did not exist in its scope, threw a ReferenceError, and the route that renders the panel caught it in a try block, replaced the whole panel with an apology, and returned 200.
The catch was deliberate and, in isolation, reasonable: a control panel that fails should not take down the page it lives on. What it did in practice was convert a hard failure into a quiet one.
Everything that was watching said the site was fine #
Because the site was fine. The status code was 200. The page rendered. The shell, the header and the navigation were all correct. The only thing missing was the content, and nothing was checking the content.
This is the same shape as the twenty-six days of proof files that said pending. The monitoring confirmed the job ran. Nothing opened the file. Here the monitoring confirmed the page answered. Nothing read the page.
| Check | What it asserted | Result while the panel was dead |
|---|---|---|
| Uptime ping | The route answers | Pass |
| Status code | 200, not 5xx | Pass |
| Deploy verification | Build identity matches the archive | Pass |
| Syntax gate | The bundle parses | Pass |
| Anything reading the rendered page | — | Did not exist |
Why a syntax check cannot see it #
A reference to an undefined variable is valid JavaScript. It is only an error when the line executes, and that line executes on one authenticated route that no automated check visits. The bundle parsed. The deploy verified. The build identity matched its archive digest. None of those facts have anything to say about whether a function receives the arguments it uses.
We have written before about a number that could not explain itself, and the fix there was the same principle in a different costume: the thing that computes a value has to carry the evidence for it, rather than leaving a reader to trust that the right inputs were in scope.
The fix, and the part of it that matters #
The environment is now passed through to the block that needs it, which is two characters of signature and one of a call site. That part is not interesting. The interesting part is the second change: the block now tolerates a missing environment instead of throwing, so the worst case is a table that reports nothing rather than a page that reports nothing.
A component that reads configuration should lose its own contents when configuration is unavailable. It should not be able to take the page down with it, and it should not be able to do so silently.
What we could not have known, and what we could #
We could not have known the date it started without reading the deploy archive, and we have not claimed one here for that reason. What we can say is that the call has been in the source across every build we hold, and that the panel has been unreadable for anyone who opened it in that window.
The honest accounting is that nobody opened it, which is its own finding about a product with one paying customer. A dashboard that nobody looks at is indistinguishable from a dashboard that does not work — until you add a row to it.
How we found which line it was #
The error message named a variable and nothing else. That is enough to search for, but the same variable name appears hundreds of times in a bundle this size, almost all of them legitimate. What narrowed it was running the render function directly against a stub environment outside the Worker, catching the throw, and reading the first frame of the stack.
Then the control that mattered: run the same function from the previous deploy. It threw in the same place. That single comparison converted an assumption into a fact — the defect was not introduced by the change being worked on, and every build we still hold carries it. Without that step the honest sentence would have been we broke it today, and it would have been wrong.
The class of bug, named #
An exception handler with a fallback is a decision about what a failure should look like. Ours decided it should look like a working page. That is defensible for a widget and indefensible for the page's entire contents, and the difference between those two cases is not visible in the code — it is visible in how much of the page sits inside the try.
The narrower the block, the more honest the fallback. A try that wraps one table can only ever cost you that table. A try that wraps the composition of the whole page can cost you the page while reporting success, which is what happened here.
The generalisable version #
If your application catches an exception and substitutes fallback content, something has to notice the substitution. A log line is enough. A counter is better. What is not enough is the status code, because the whole purpose of the catch was to keep the status code the same.
The free scan takes the same position about pages it measures: a 200 with the wrong bytes in it is a finding, not a pass. That is why the report reads content rather than status, and why a site can be clean on the day you audit it and broken for every crawler that arrives afterwards.
Every figure above came out of this scanner.
Point it at your own domain and see the same measurements, free.
The main product
Found this on your own site? We fix it for $749.
Scan free to see where you stand. The fix is one site, every finding implemented and re-measured, with a sealed before and after.
Questions this post answers
Was any customer data affected?
No. The panel is a read view over records that were being written correctly the whole time. Nothing was lost; it could not be displayed.
Why did the deploy checks pass?
They check that the bundle parses, that the build identity matches its archive and that routes answer. A variable that is undefined only at execution time on one authenticated route is invisible to all three.
What stops this recurring?
The block now tolerates a missing environment rather than throwing, so a configuration read can cost a table but not a page. The broader rule is that a caught exception which substitutes fallback content has to be recorded somewhere.
Related findings
Comments
Comments are read before they appear. Nothing is published automatically, and no account is needed.
Writing about this? Facts, live figures and marks — every number on that page is dated and traceable to a scan.