Skip to content

fix(queen-web): every deploy built, started, and was refused by its own healthcheck - #1086

Merged
gHashTag merged 1 commit into
mainfrom
fix/queen-web-healthcheck-host
Sep 21, 2026
Merged

gHashTag merged 1 commit into
mainfrom
fix/queen-web-healthcheck-host

Conversation

@gHashTag

Copy link
Copy Markdown
Owner

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 start is vite preview. Since vite 6.0.9 the preview server carries a DNS-rebinding guard that answers an unrecognised Host with 403. Railway's probe arrives as healthcheck.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:

Host before after
healthcheck.railway.app 403 200
trinity-production-6945.up.railway.app 403 200
evil.example.com 403 403
127.0.0.1 200 200

The 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."
    ]
  }
}

…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>
@github-actions github-actions Bot added the status:in-progress 🔵 Agent working label Sep 21, 2026
@gHashTag
gHashTag merged commit 59accb4 into main Sep 21, 2026
29 of 35 checks passed
@gHashTag
gHashTag deleted the fix/queen-web-healthcheck-host branch September 21, 2026 13:14
@github-actions github-actions Bot added status:completed Done and removed status:in-progress 🔵 Agent working labels Sep 21, 2026
@gHashTag
gHashTag restored the fix/queen-web-healthcheck-host branch September 28, 2026 10:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant