CrawlCheck

Findings · 2026-09-17 · By · 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.

HTTP 200 · /adminheader, nav, shell — renderedcontrol panelone sentence where the page wasReferenceError, caughtby the route that renders itstatus never changesHTTP 200 · /adminheader, nav, shell — renderedcontrol panelone sentence where the page wasReferenceError, caught by the routethat renders itstatus never changes
Uptime is a property of the response. It is not a property of the page.

The cause is four words long #

A function that renders the panel called a block with an argument it never received. 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.

CheckWhat it assertedResult while the panel was dead
Uptime pingThe route answersPass
Status code200, not 5xxPass
Syntax checkThe code parsesPass
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 code parsed and the deploy succeeded. Neither fact has 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 missing argument is now passed through. That part is not interesting. The interesting part is the second change: the block now tolerates the missing input 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 have not established the date it started, and we have not claimed one here. What we can say is that the call is present in every earlier version 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 on its own with stand-in inputs, catching the throw, and reading the first frame of the stack.

Then the control that mattered: run the same function from the previous version. 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 version 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.

Scan a domain — 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 code parses and that routes answer. A variable that is undefined only when one authenticated route runs is invisible to both.

What stops this recurring?

The block now tolerates a missing input 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

How anything measured in this article was measured15client identitiesone second, one address5machine filesapex and www114named agentsresolved from robots.txt24sections scoredreach, read, quoteHow anything measured here was measured15 client identities5 machine files114 named agents24 sections scoredone second, one addressapex and wwwresolved from robots.txtreach, read, quote
No account, nothing installed, and the same sequence on every domain — which is what makes one scan comparable to another. Run it on your own site.

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.

All findings · The dataset · How the dataset works