Skip to content

traefik: stop the splash page from claiming an app's client errors - #237

Merged
max-tet merged 1 commit into
mainfrom
fix/clayde/splash-status-range
Sep 17, 2026
Merged

max-tet merged 1 commit into
mainfrom
fix/clayde/splash-status-range

Conversation

@ClaydeCode

Copy link
Copy Markdown
Contributor

Closes #235.

The app-error middleware claimed 400-499 alongside 500-599, so every 4xx an app's own backend returned was replaced by the splash page on the way out: status preserved, body discarded. A frontend reading its API's error contract got an HTML string where it expected JSON.

Found from the controller app. A settings write rejected with 422 and the message min_nr_of_standby_shards: Input should be greater than or equal to 0 surfaced in the UI as axios's own Request failed with status code 422, because response.data was the splash HTML and response.data.detail was therefore undefined. Calling the same API directly, not through a shard, returns proper JSON — which is why this is invisible from outside a shard and survived this long.

The change

-                    status=["500-599", "400-499"],
+                    status=["500-599", "401"],

The new range is exactly what get_splash_behaviour renders something meaningful for:

  • 5xx — Error for 500, and Starting... / Initializing... while a container is not up, since Traefik answers 502 or 503 in that case. This is the splash's reason to exist and is untouched.
  • 401 — Access Denied with do_reload=False, the forwardAuth rejection, a real end-user-facing page.

403 is deliberately excluded, confirmed with Max. Nothing branches on it today, so it currently renders as Starting..., which is wrong regardless of the range.

Everything else from 400 to 499 now reaches the client exactly as the app sent it.

What this costs

A browser hitting a genuine 404 on an app gets an unstyled page instead of the splash. That is the accepted trade: silently destroying every machine-readable error on the platform is worse than an unstyled 404.

Status is the wrong axis for this decision, and narrowing it does not make it the right one. The complete answer is to key on what the client asked for via the Accept header, so a 404 can be a styled page for a human and a JSON body for a fetch call at the same time. That is #236 and is explicitly not this PR.

Verification

  • New test test_app_error_leaves_client_errors_to_the_app parses the rendered range spec and asserts 500, 502, 503 and 401 are covered while 400, 403, 404, 409 and 422 are not. It reads the actual generated traefik_dyn.yml, not the Python literal, so it covers the rendering too.
  • Proved it can fail before trusting it: restoring 400-499 makes it fail on assert not _covers(status_spec, code) for 400; restoring the fix makes it pass.
  • Full suite green: 392 passed in 23:32.
  • just cleanup touched nothing beyond the two files in this diff.

Not covered by tests

That Traefik itself honours a single status alongside a range in the same list. The generated config is asserted; the proxy's parsing of it is not exercised anywhere in this suite, and a shard is the only place to see it. Worth one look at a real shard after deploy: a 404 from an app should arrive as the app's own body, and an app whose container is down should still show the splash.

Related

#91 described this same swallowing and was closed as too complex after #92 proposed routing all app traffic through shard_core as a reverse proxy. This is deliberately not that.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VWEj4ivtnwGLef4hb5eFTm

The app-error middleware took 400-499 alongside 500-599, so every 4xx an
app's backend returned reached the browser as splash HTML with the status
preserved and the body gone. A frontend reading its own API's error contract
got an HTML string where it expected JSON. Found from the controller app,
where a 422 carrying "min_nr_of_standby_shards: Input should be greater than
or equal to 0" surfaced as axios's own "Request failed with status code 422".

The range now covers only what get_splash_behaviour renders something
meaningful for: 5xx, which is Error for 500 and Starting... or Initializing...
while a container is not up, since Traefik answers 502 or 503 then; and 401,
the forwardAuth rejection, which is a real end-user page. 403 is deliberately
not included.

Deciding this by status is still the wrong axis, because a browser hitting a
genuine 404 on an app now gets an unstyled page. Keying on what the client
asked for is #236.

Closes #235

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWEj4ivtnwGLef4hb5eFTm
@ClaydeCode
ClaydeCode force-pushed the fix/clayde/splash-status-range branch from 60e3477 to 8f9b4c3 Compare September 17, 2026 11:13
@max-tet
max-tet merged commit df7ac38 into main Sep 17, 2026
7 checks passed
@max-tet
max-tet deleted the fix/clayde/splash-status-range branch September 17, 2026 11:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Splash page swallows every 4xx an app returns: narrow the app-error status range

3 participants