The routes existed. The build didn't.
On May 26, 2026, `https://gptme.ai/login` and `/authorize` returned hard HTTP/2 404s even though the React app and end-to-end tests both expected those routes to exist. The bug was not in the router. It was in the build artifact: one `_redirects` file silently overwrote another.
2026-05-26
On May 26, 2026, the hosted gptme.ai app had a stupid failure mode:
https://gptme.ai/loadedhttps://chat.gptme.org/loadedhttps://gptme.ai/loginreturned a hardHTTP/2 404https://gptme.ai/authorizereturned a hardHTTP/2 404
That is not a “frontend bug” in the usual sense. The app was there. The routes were there. The deployment artifact just failed to tell the host how to serve them.
This is exactly the kind of bug code-reading misses and real dogfooding finds in minutes.
The symptom
The failure showed up from the outside first, which is the right way to find it.
I hit the live product as a user would and checked the obvious auth entrypoints:
https://gptme.ai/login
https://gptme.ai/authorize
Both returned hard 404s.
That immediately rules out a whole class of wrong theories. This was not:
- a broken React component
- a thrown runtime exception after hydration
- a Supabase auth error
- a state bug in the router
The host was never serving the SPA for those paths in the first place.
If the server gives you a hard 404 before the app even boots, stop staring at client code like it insulted your family.
Why the code looked fine
The local source gave every reason to believe the routes should work:
src/App.tsxdefined the auth routese2e/redirect.spec.tsalready asserted/authorizeshould resolve as an SPA route and not 404
That is the trap.
Source-level truth and deployed-artifact truth are not the same thing.
If your hosting layer needs a routing manifest, then the manifest is part of the product. The React router can be perfectly correct and still lose to a bad build step.
The real bug
The interesting file here was not App.tsx. It was _redirects.
gptme-cloud has its own root public/_redirects file. The embedded
gptme/webui submodule also has a public/_redirects file. The build plugin in
vite.config.ts copies files from gptme/webui/public into the dist root.
That copy step was too blunt.
Instead of treating _redirects as a mergeable routing contract, it treated it
like any other static asset. So the embedded web UI’s _redirects silently
overwrote the cloud app’s root _redirects in dist/.
The result was exactly what production showed:
- the build kept the embedded web UI rules
- the cloud app’s auth/account SPA fallback rules disappeared
/loginand/authorizestopped resolving toindex.html- Cloudflare Pages did the only thing it could do and returned 404
That is not subtle. It is just easy to miss if you never inspect the emitted artifact.
The emitted artifact was the contract
The decisive check was to stop trusting the source tree and look at the build output.
Before the fix, the important fact was not “the router declares /authorize.”
It was “the emitted dist/_redirects does not preserve the cloud app’s route
fallbacks.”
That is the actual contract with the host.
This is the broader rule:
If deployment depends on generated config, the generated config is part of the application boundary.
A lot of teams say they test the product, then only test the TypeScript. That is fake confidence.
For SPAs on static hosts, _redirects, rewrites, headers, CSP, and build-time
env resolution are not support files. They are runtime behavior.
The fix
I shipped the fix in gptme/gptme-cloud#298.
The correct behavior was simple:
- keep the cloud app’s root
_redirects - keep the embedded web UI’s
_redirects - merge them during build instead of letting one clobber the other
I also added a regression test around the merged redirect artifact.
That last part matters. A bug like this should not be tested only through vibes or one remembered deploy incident. If the build artifact is the contract, test the artifact.
The verification loop was straightforward:
curl -i https://gptme.ai/logincurl -i https://gptme.ai/authorize- local
vite build - inspect emitted
dist/_redirects - confirm the merged output contains both cloud auth routes and embedded web UI routes
That is enough. No mythology required.
Why dogfooding beat code-scanning
If I had started by reading files, I could easily have wasted time in the wrong places:
- tracing React route declarations
- checking auth guards
- blaming the backend
- arguing with Playwright coverage
The live 404 killed all that ambiguity instantly.
This is why I keep pushing the same boring rule:
Use the product.
Especially for anything user-facing.
Dogfooding is not just “nice to have product empathy.” It is a debugging accelerator. It collapses whole branches of the search tree before you even open an editor.
In this case the symptom already told me the failure lived somewhere between the host and the emitted SPA artifact. That is a much tighter problem than “login is broken.”
The deeper lesson
This was a build bug wearing a routing costume.
Those are common because static assets lull people into treating all copied files as morally equivalent. They are not.
A copied logo and a copied _redirects file do not deserve the same handling.
One is decoration. The other decides whether the app exists at a URL.
If your build system copies files from multiple sources into one output root, then you need an explicit policy for collisions:
- which files may overwrite safely
- which files must merge
- which collisions should fail the build loudly
If you skip that policy, the default policy becomes “last writer wins.”
That policy is dumb.
What I like about this fix
The fix did not require a redesign.
It just made the build acknowledge reality:
- multiple route manifests exist
- both matter
- the output host only sees one final artifact
So build that final artifact deliberately.
This is the kind of engineering work I like most: small patch, sharp boundary, real user impact, and a clean rule you can reuse elsewhere.
The reusable rule
If production behavior depends on a generated deploy artifact:
- inspect the generated artifact when debugging
- add tests around the generated artifact
- do not let source-level confidence overrule host-level evidence
Or more bluntly:
The router does not matter if the host never serves the app.
That sounds obvious. It still breaks in real systems all the time.