Frontend
Your Flutter 404 Page Is Probably Crashing, and Your Server Is Probably Lying About It
K M Shahriar Hossain DEV Community
1 views
Someone sent me a screenshot of my own 404 page. Washed-out grey text on a light
background, barely readable, nothing like the dark theme every other page uses.
My first thought was a styling bug.
It was not a styling bug. The page was crashing before it could paint, and the
fallback I had built for exactly that case was doing its job. Underneath it was a
second bug that had been hiding the first one for months.
Bug one: the status code was a lie
The site is a Flutter web app with every route prerendered to a real HTML file.
Firebase Hosting served those files, and anything unmatched hit a catch-all
rewrite:
"rewrites": [
{ "source": "**", "destination": "/404.html" }
]
That looks right. It is not, and the reason is worth internalising: a Firebase
rewrite always responds 200. That is what a rewrite is — serve this other
content under the requested URL. So every nonexistent URL on the site returned
200 OK with a page that said "Page not found".
Browsers do not care. Crawlers care a great deal. A 200 means "this is a real
page, index it", so every typo, every dead inbound link, every scanner probing
for /wp-admin was eligible to be indexed as a real page. This is the soft 404,
and it is invisible from a browser because the page looks correct.
The fix is to delete the catch-all rather than repoint it. With every real route
prerendered as a file, Firebase serves those directly, and for anything with no
matching file it falls through to its own handling — which serves 404.html
with an actual 404 status.
One caveat that will bite you if your app has client-only routes. My admin panel
has no prerendered file, so removing the catch-all 404'd my own admin URL. It
needs a rewrite scoped to that path alone:
"rewrites": [
{ "source": "/your-client-only-path/**", "destination": "/index.html" }
]
Scoped rewrites for the routes that genuinely need them; no catch-all; real 404s
for everything else.
Bug two: the page was crashing the whole time
Fixing the status code is what made me actually look at the page, and that is
when the grey rendering stopped looking like a theme problem.
My prerendered HTML carries the page's text in the DOM, clipped to a single
pixel, with a boot guard that reveals it if Flutter never paints. The idea is
that a failed boot shows the content rather than a spinner turning forever. So
the washed-out text was the guard firing correctly, telling me Flutter had
died.
The console had it:
GoError: There is no GoRouterState above the current context.
This method should only be called under the sub tree of a RouteBase.builder.
Here is the chain. errorBuilder renders my NotFoundPage. That page uses the
same shell as every other page, and the shell contains the nav bar, and the nav
bar asks which route is current so it can highlight the right link:
final current = GoRouterState.of(context).uri.path;
On every real route that is fine. On the 404 page it throws, because
errorBuilder renders outside any RouteBase.builder — there is no route
subtree above it and therefore no GoRouterState to inherit.
The fix that does not exist, and the one that also fails
The instinct is maybeOf. In go_router 17 there is no GoRouterState.maybeOf.
of() throws unconditionally; there is no nullable variant to fall back to.
So the next instinct is to ask the router itself, which definitely sits above
everything:
current = GoRouter.of(context).state.uri.path; // also throws
I tried exactly this, and it fails too — differently, which is what makes it
interesting. GoRouter.state reads matches.last on the current match list, and
on an unmatched URL that list is empty, so you get a StateError: Bad state:
No element. The accessor fails for precisely the reason you are on the 404 page
at all.
Both router-side answers are dead ends. But the browser knows the path regardless
of what the router made of it:
String current;
try {
current = GoRouterState.of(context).uri.path;
} catch (_) {
current = Uri.base.path;
}
The broad catch is deliberate, and I would defend it specifically here.
Determining which nav item to highlight is decoration. This widget renders inside
every page on the site. Failing at decoration must never be able to take down the
page around it, and the two failure modes above are different exception types
from different call sites — narrowing the catch buys nothing except a chance to
miss the third one.
Nothing highlights on a 404, which is correct: no nav item corresponds to a page
that does not exist.
Why it survived so long
Because the two bugs concealed each other. The 404 page had been broken for
months and rendered identically whether it answered 200 or 404 — so nothing
looked wrong from a browser, and nothing in the logs distinguished it. Crawlers
saw 200 and were happy. I never visited my own 404 page, because why would I.
The status-code fix did not cause the crash. It made someone look at the page,
which is a different and more useful thing.
Two habits fall out of this. Check your 404 page's status code, not just its
appearance — curl -o /dev/null -w '%{http_code}' https://yoursite/nonsense
takes two seconds and is the only way to see a soft 404. And actually load your
error page in a browser after a routing change, because it is the one page in
your app that renders through a completely different code path from every other
page, and therefore the one page your testing never touches.
Originally published at devshakib.jumyn.com. I write about Flutter, Dart and the parts of shipping that are genuinely awkward — and publish the packages that came out of them at pub.dev/publishers/jumyn.com.
Read original: https://dev.to/devshakib/your-flutter-404-page-is-probably-crashing-and-your-server-is-probably-lying-about-it-k43
← Previous
A Squarified Treemap by Hand, Because Charting Packages Cannot Drill Down
Next →
What It Actually Takes to Run a Cross-Border Marketplace: Six Years of Shpper
Related
how I make my templates easy to reskin (probably overthought this)
Frontend
1
Dev.to (EN Zone)
StyleX won CSS-in-JS because AI agents can read it
Frontend
2
DEV Community
Why I Built a Lightweight Utility Styling Library for React Native (And How It Solves StyleSheet Fatigue)
Frontend
2
DEV Community
DOM in Angular: Understanding the Document Object Model with Practical Examples
Frontend
3
Dev.to (EN Zone)
Comments0
No comments yet — be the first