traefik: stop the splash page from claiming an app's client errors - #237
Merged
Merged
Conversation
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
force-pushed
the
fix/clayde/splash-status-range
branch
from
September 17, 2026 11:13
60e3477 to
8f9b4c3
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #235.
The
app-errormiddleware claimed400-499alongside500-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
422and the messagemin_nr_of_standby_shards: Input should be greater than or equal to 0surfaced in the UI as axios's ownRequest failed with status code 422, becauseresponse.datawas the splash HTML andresponse.data.detailwas thereforeundefined. 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
The new range is exactly what
get_splash_behaviourrenders something meaningful for:Errorfor 500, andStarting.../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.Access Deniedwithdo_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
404on 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
Acceptheader, so a404can be a styled page for a human and a JSON body for afetchcall at the same time. That is #236 and is explicitly not this PR.Verification
test_app_error_leaves_client_errors_to_the_appparses 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 generatedtraefik_dyn.yml, not the Python literal, so it covers the rendering too.400-499makes it fail onassert not _covers(status_spec, code)for 400; restoring the fix makes it pass.just cleanuptouched 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
404from 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