Repository navigation
fix(queen-web): every deploy built, started, and was refused by its own healthcheck - #1086
Merged
Merged
Conversation
…wn healthcheck Four merges on 2026-09-21 -- #1075, #1078, #1079, #1082 -- and then #1084 on top of them, all FAILED on the trinity Railway service, all at the same place: Attempt #1 failed with HTTP 403. Continuing to retry for 29s ... five attempts ... 1/1 replicas never became healthy! Healthcheck failed! The build succeeds every time and the container starts every time. The 403 is the server refusing the platform, not the app being broken. `npm start` is `vite preview`, and vite's preview server has answered an unrecognised Host with 403 since 6.0.9, when the DNS-rebinding guard was turned on by default. Railway's probe arrives as `healthcheck.railway.app`, which was in no list, so it was refused -- and the message that says exactly this is printed into the response body, which a healthcheck discards. Reproduced at the pinned version rather than guessed. The first attempt at this was run with `npx vite preview`, which quietly installed vite@8.3.0 and served the foreign Host with 200 -- a clean result from the wrong program. With the lockfile's 6.4.1: Host: healthcheck.railway.app -> 403 Blocked request. This host ... is not allowed. Host: 127.0.0.1 -> 200 and after this change: Host: healthcheck.railway.app -> 200 Host: trinity-production-6945.up.railway.app -> 200 Host: evil.example.com -> 403 A leading dot allows a domain and all of its subdomains, so one entry covers both the probe and the generated service domain, which can be reissued. `allowedHosts: true` would have switched the guard off for everything; the last line above is the reason not to. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Every deployment of the trinity service on Railway today built cleanly, started cleanly, and was then refused by its own healthcheck with HTTP 403, five attempts in thirty seconds. #1075, #1078, #1079, #1082 and #1084 all failed the same way. Nothing has rolled forward on that service since.
Cause
npm startisvite preview. Since vite 6.0.9 the preview server carries a DNS-rebinding guard that answers an unrecognisedHostwith 403. Railway's probe arrives ashealthcheck.railway.app, which appeared in no list. Vite prints the reason into the response body — where a healthcheck discards it.Reproduced at the pinned version, not guessed
The first attempt at this ran
npx vite preview, which quietly installed vite@8.3.0 and served the foreign Host with 200 — a clean result from the wrong program. Redone with the lockfile's 6.4.1:healthcheck.railway.apptrinity-production-6945.up.railway.appevil.example.com127.0.0.1The last row is why this is a list and not
allowedHosts: true: the guard stays on for everything that is not Railway. A leading dot allows a domain and all its subdomains, so one entry covers both the probe and the generated service domain, which can be reissued.{ "version": 1, "head_sha": "08685babd0827406a600cf31ae69b2547f4dfdb1", "summary": "Vite 6 preview refuses any Host it was not told about with 403, so Railway's own healthcheck probe was rejected and five consecutive deployments of the trinity service never reached a running replica.", "changes": [ "Add preview.allowedHosts to apps/queen-web/vite.config.ts with a single leading-dot entry", "Allow the Railway healthcheck probe host and the generated service subdomain", "Keep the DNS-rebinding guard active for every host that is not Railway", "Record in the config comment why the deploys failed and why a list beats a blanket true" ], "tests": [ { "command": "PORT=8793 npm start (vite 6.4.1 from the lockfile), curl -H 'Host: healthcheck.railway.app'", "result": "403 Blocked request, exactly the failure Railway reported five times", "status": "passed", "evidence": "Response body reads: Blocked request. This host is not allowed. Add it to preview.allowedHosts." }, { "command": "PORT=8794 npm start with the fix applied, curl -H 'Host: healthcheck.railway.app'", "result": "200, the probe is now served", "status": "passed", "evidence": "Same binary and same dist as the failing run, only vite.config.ts differs." }, { "command": "curl -H 'Host: trinity-production-6945.up.railway.app' against the fixed preview server", "result": "200, the generated service domain is covered by the same leading-dot entry", "status": "passed", "evidence": "One entry of .railway.app matches both the probe host and the up.railway.app subdomain." }, { "command": "curl -H 'Host: evil.example.com' against the fixed preview server", "result": "403, the guard is still on for anything that is not Railway", "status": "passed", "evidence": "This is the check that distinguishes the fix from setting allowedHosts to true." } ], "limitations": [ "A custom domain added to this service later would need its own entry here", "Vite documents preview as not intended to serve production traffic at all", "The healthcheck path remains the site root rather than a dedicated endpoint", "Whether apps/queen-web is still wanted at all is a separate question for the owner" ], "tags": ["railway", "vite", "healthcheck", "deploy"], "blog": { "title": "A server that refused its own healthcheck for a whole day", "summary": "Five deployments in a row built, started, and were then turned away by the program they had just launched, because a security default introduced in vite 6.0.9 does not know what a Railway probe is.", "outline": [ "The failure looked like a broken application and was not: every build succeeded, every container started, and the only error in the log was an HTTP status code from the server itself.", "Vite added a DNS-rebinding guard to the preview server in 6.0.9, and it refuses any Host header it has not been given, which is the correct default for a tool meant for local use.", "Railway probes a new replica as healthcheck.railway.app, a name that appears in no configuration file, so the refusal landed on the one request that decides whether a deploy lives.", "The reason was printed in the response body the whole time, and a healthcheck reads only the status code, which is how a self-describing error stayed invisible for four merges.", "The first reproduction attempt used npx and silently ran vite 8.3.0, which allowed the host and returned 200: a green result from a program that was not the one deployed.", "Allowing a leading-dot domain covers the probe and the generated subdomain together while leaving the guard in force for every other host, which a blanket true would not do." ] } }