DEV Community

Daniel Pertu
Daniel Pertu

Posted on

A stranger cannot reach our 404 page, and three of our four error screens report to nobody

Try this against our site. Two URLs that do not exist, two different answers:

curl -sI https://pub-trivia.app/guides/not-a-real-guide | head -1
# HTTP/2 404

curl -sI https://pub-trivia.app/not-a-real-guide | grep -iE "^(HTTP|location)"
# location: /login?next=%2Fnot-a-real-guide
Enter fullscreen mode Exit fullscreen mode

Same site, same build, same nonsense path on the end. One gets our 404 page. The other gets a 307 to a login screen it has no business seeing.

Why the second one never reaches the framework

Every request to the app passes an auth gate before Next's router sees it. The gate works off a prefix allowlist: a path is public if it equals an entry or begins with an entry plus a slash, which is how one entry covers a cluster of pages and its three legal documents live under one line.

/guides/not-a-real-guide begins with a public prefix, so the gate hands it on. Next looks for a matching route, finds none, and renders app/not-found.tsx with a 404 status. That is the ordinary path, and you can see it on any real page under that prefix if you mangle the slug.

/not-a-real-guide matches nothing in the allowlist. The gate has no idea whether Next has a route for it, because the gate holds a list of what is public and not a copy of the route table. An unknown path is therefore treated as a private path, and private paths get a redirect to the login screen with a next parameter so the user lands where they were going after signing in.

The result is that our own 404 page is unreachable from the root of the site. Anonymous traffic hitting a stale root-level link does not get told the page is gone, it gets told to log in. For a person that is mildly confusing. For a crawler holding a link to a URL we deleted, it is worse than confusing: a redirect to a login page is the textbook soft 404, and it keeps the dead URL alive in the index instead of retiring it.

The honest reason it has not been fixed is that there is no good place to fix it. The gate cannot answer 404 for unknown paths without knowing the full route table, which would mean a second list that drifts from the first. The real fix is a short list of retired root-level URLs that are allowed to reach the framework and get a proper 404, which is a list with one job and no second source of truth.

One consolation: the 404 that does work is cheap. It comes off the CDN like everything else.

curl -sI https://pub-trivia.app/guides/not-a-real-guide | grep -iE "x-vercel-cache|age"
# age: 309
# x-vercel-cache: HIT
Enter fullscreen mode Exit fullscreen mode

Four error screens, four different readers

There are four files in the app whose only job is to apologise. They are not variations on a template, and the differences are the interesting part.

File Lines Who sees it Reports? Ways out
app/not-found.tsx 20 anyone, on a public page no "Go home"
app/global-error.tsx 60 anyone, when the root layout itself failed yes "Try again"
app/dashboard/error.tsx 46 the venue, signed in console only "Try again", "Back to Dashboard"
app/play/error.tsx 37 a player's phone, mid-quiz console only "Try again"

not-found.tsx is twenty lines and has no error in it. There is nothing to report: the user asked for a page that does not exist, which is information, not a fault. It gets a heading, one sentence and a link home.

global-error.tsx is the only one that cannot trust the app. It renders its own <html> and <body>, because the root layout is the thing that failed, and every single style in it is an inline style object:

<h1 style={{ fontSize: "1.5rem", fontWeight: "bold", marginBottom: "0.5rem" }}>
  Something went wrong
</h1>
Enter fullscreen mode Exit fullscreen mode

That looks like sloppiness next to a codebase that is Tailwind classes everywhere. It is the opposite. The stylesheet link is injected by the layout that just threw, so a className here is a bet that the CSS arrived, and the one screen whose job is to work when nothing else did should not be making bets. Inline styles are the only styling that cannot fail to load.

It is also the only one of the four that calls the error reporter.

dashboard/error.tsx has two exits instead of one. "Try again" calls reset(), which re-renders the segment, and that genuinely fixes a transient failure. If it does not, a second button goes back to the dashboard root. A signed-in user looking at a broken page inside an app they are paying for should never have to use the browser's back button to escape.

play/error.tsx is the one I would defend hardest. It is the screen a player sees on their phone, standing in a pub, in the middle of a round. Its heading is not "Something went wrong":

<h2 className="text-xl font-bold">Connection Error</h2>
<p>We couldn&apos;t load the game. Check your connection and try again.</p>
Enter fullscreen mode Exit fullscreen mode

The code may well be at fault. But the base rate on a phone in a busy venue says otherwise, and "Check your connection" points at the thing the player can actually do something about. It also has exactly one button and no link anywhere else, not even home. A player has no use for our marketing site. Their context is the table they are sitting at, and the only useful action is to try the thing again.

Every one of them shows an Error ID that three of them do not log

All four files carry the same four lines:

{error.digest && (
  <p className="text-xs text-muted-foreground font-mono">
    Error ID: {error.digest}
  </p>
)}
Enter fullscreen mode Exit fullscreen mode

Two things about this. First, the guard is not decoration. Next populates digest for errors that come from the server, and an exception thrown in the browser arrives with nothing to put in that paragraph, so the whole block vanishes. The screen is at its most informative in exactly the case where the platform log already has the error, and at its least informative when the log has nothing.

Second, three of the four files display that id without sending it anywhere themselves. A host reading an Error ID down the phone to me is a good experience right up until the moment I go looking for it, and for a browser-side failure in the dashboard or on a player's phone there is nothing on our side to match it against beyond a console.error that lives and dies in a device I will never see.

That is the actual finding from writing four of these files instead of one. The styling differences are justified, the copy differences are justified, and the reporting differences are not a decision at all, they are what happens when you copy a file three times and only think hard about the first one.

If you want to see the end of this chain without an error in it, the four free tools run entirely in the browser and have no boundary of their own, which is its own kind of answer: nothing to report because nothing on our side is involved.

Top comments (0)