Skip to content

fix(astro-renderer): status-page fixes after RFC-0043 PR 1 (#167) - #171

Merged
Madraka merged 3 commits into
mainfrom
claude/charming-jennings-72b55a
Nov 8, 2026
Merged

Madraka merged 3 commits into
mainfrom
claude/charming-jennings-72b55a

Conversation

@Madraka

@Madraka Madraka commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #167: the follow-ups to RFC-0043 PR 1 (#162) from the adversarial review. Patch changeset for @cloudhal/astro-renderer.

The entry's status step and error page moved from cloudflare-entry.ts into a new Worker-safe module, packages/astro-renderer/src/status-entry.ts (statusStep, withErrorPage, errorPage). It has no cloudflare: or Astro imports, so unit tests can run it. cloudflare-entry.ts shrinks by 20 lines.

Issue items and how each was verified

# Item Change Verified by
1 withErrorPage turned the starter's not-published 503 into a 500 error page with a false error log Only Astro's own 500 HTML page is rewritten. A response with x-cloudhal-state (every renderer status page; the header is already public per RFC-0043 §1.2, so nothing new to strip), a theme's x-cloudhal-error-page: theme, and any other 5xx pass unchanged and log nothing. test/status-entry.test.ts: the starter's statusPage("not-published") 503 on a POST passes untouched with no log; a theme's 502/503, a themed 500 and a plain-text 500 pass; Astro's 500 HTML becomes the logged error page. On workerd, packages/cli/test/cloudflare-status.test.ts: POST / before the first publish now answers 503 not-published. Before this fix it answered a 500.
2 A store read failure before rendering answered 500 error statusStep catches a siteState failure, logs it once, and answers unavailable: 503 with Retry-After: 300, the same as /__cloudhal/health. Page paths get the page, machine paths statusPlain. status-entry.test.ts "a store read failure is unavailable": the page path gets 503, Retry-After: 300 and x-cloudhal-state: unavailable; /rss.xml gets the same in plain text without the D1 error text.
3 The error page was always English and never named the site runtime-workerd.ts stores the site row this request already read on the withRequestSite context (loadedSite(), no store read). The entry's catch (now inside withRequestSite) and withErrorPage pass it on. statusInput("error") gives the locale from host or path prefix and the name only: no logo, no theme tokens (§1.6). Before the site is read, the page stays English. status-entry.test.ts "errorPage": a Turkish-default site gives lang="tr", the Turkish title and the escaped name, with no logo, no brand token and no error text. /en/about gives English. Without a site the page is English and has no name.
4 An invalid preview token on a feed, sitemap or JSON path served the published content; the failure reason was echoed on machine and media paths statusStep also handles machine paths (statusPathKind: .xml/.txt/.json/.rss/.atom/.webmanifest, but not robots.txt, /.well-known/, media or static assets). preview-expired gets statusPlain: 410, text or JSON, in the site's language. Before the first publish, machine paths still get the theme's own statusPlain (§2.5). markInvalidPreview replaces the reason with x-cloudhal-preview-error: invalid plus no-store on the media route and in getSource (decision 12). status-entry.test.ts: /rss.xml, /sitemap.xml, /llms.txt, /feed.atom with a forged token answer 410 plain text with no reason header; /search.json answers 410 JSON; HEAD has no body; robots, media and /.well-known/ are left alone; the marker test covers the header. On workerd, cloudflare-status.test.ts after publish: /rss.xml and /sitemap.xml with a bad token answer 410 text/plain, not <rss/<urlset.
5 ADR-0012 said invalid tokens fall back to the published page The sentence now says: 410 on pages and machine paths, media served uncached, no reason given. Doc review
6 Translation docs described the exchange as Studio-only The status catalog (packages/astro-renderer/src/status-strings/, status.json) is added to the translate skill (files, import output, {name}-only rule, never hand-edit), DESIGN.md §1 "Translation workflow", the Weblate guide (component table and a note on the status component) and ADR-0034 (Context and a dated amendment). Doc review; pnpm design:lint
7 RFC-0043 §1.9 on unknown hosts New bullet: a host that maps to no site never reaches health and gets RFC-0034 §4's 60 s-cached plain 404. §1.2 (error row and the preview-expired note), §1.8 and the compatibility list now describe items 1–4. Doc review

Review fixes (second commit)

# Finding Change Verified by
R1 A real theme crash did not reach the error page withErrorPage now also rewrites a 500 with no content type (Astro 7.4.7's renderDefaultError fallback, new Response(null, { status })). A 500 with an explicit non-HTML type and a body still passes, and so do the x-cloudhal-state and theme-marker cases. A theme's null-body 500 with a non-HTML type also becomes the error page: Astro reroutes an empty 500 to its error route and the type is lost on the way. That is accepted. On workerd the starter's real path was worse than reported: when a page throws, Astro 7.4.7 renders its error route: /500 matches the catch-all [...path].astro, which runs with the original path and answers its 404 page for that path. The starter now rethrows Astro.props.error when Astro renders it as the error route. Astro then falls back to the empty 500, which becomes the error page. Unit: new Response(null, { status: 500 }) becomes the error page (state, HTML, CSP, reference, one log); a JSON 500 passes. Workerd: a fixture src/pages/broken.astro that throws in its frontmatter gets 500, x-cloudhal-state: error, an HTML body with Reference: <hex> and no error text, and harness.getLogs() has cloudhal: request failed (reference <same id>).
R2 The error page used the live site row SiteResolver.publicSite(resolution) returns the published snapshot's site node (source.site(), which the render loads anyway; a read failure gives undefined) for published, the live row only for unpublished, and nothing for a preview. loadedSite() is filled from it in the background, never awaited on the request path; health skips it and reads only the pointer. A render that throws has already awaited the same shared snapshot load, so the site is known by then. test/resolver.test.ts "publicSite": after publishing and then renaming the live site, publicSite still gives the published name; before publishing it gives the live row; a preview gives undefined; a snapshot that cannot be read gives undefined, not a throw.
R3 A failed multi-site host lookup gave a 500 route catches a SiteDispatch.choose() failure and calls hostLookupFailed. Health answers 503 { ok: false } with no-store. GET/HEAD page paths get the unavailable page and machine paths its plain answer (503, Retry-After: 300, English, no site name). Anything else gets a plain 503. health moved to status-entry.ts with the state injected, so it can be tested. status-entry.test.ts: a SiteDispatch over CLOUDHAL_SITE_IDS whose every sql call rejects, run through hostLookupFailed. Health gets 503 JSON { ok: false } with no-store; /about gets the page with 503, Retry-After: 300, unavailable and the English title; /rss.xml gets the plain 503; a POST gets a plain 503. Plus health itself (state shown, store failure gives 503). Wiring, on workerd (cloudflare-multisite.test.ts, CLOUDHAL_SITE_IDS): with site_hosts renamed away, after a purge reset the host map, health gets 503 { ok: false }, /about gets the English unavailable page (503, Retry-After: 300, no site name) and /rss.xml gets the plain 503. With the choose() catch removed, this case fails with a 500.

Old theme copies: a site made from an earlier starter still shows its own 404 when a page throws. It needs the same two lines at the top of its [...path].astro; the changeset says so. The cloudhal-template repository needs the starter change at its next sync.

Checks run (serially)

  • After the last review commit, run one package at a time (pnpm exec vitest run):
    • astro-renderer: 49 files, 347 tests (status-entry.test.ts is new, with 16 cases);
    • cli: 61 files, 441 tests, including every cloudflare* suite on workerd.
  • pnpm typecheck passes in both packages.
  • The starter has no typecheck script; its astro build runs inside the workerd test.
  • pnpm lint exits 0. The two complexity warnings on the touched files (serveSite, SiteResolver.resolve) were there before this change.
  • pnpm size:check and pnpm design:lint pass.

🤖 Generated with Claude Code

Madraka and others added 3 commits November 8, 2026 18:34
- withErrorPage rewrites only Astro's own 500 HTML page; status pages
  (x-cloudhal-state), a themed 500 and any other 5xx pass and log nothing,
  so the starter's not-published 503 is no longer a 500 error page.
- A store read failure in the state step answers 503 unavailable with
  Retry-After: 300, as /__cloudhal/health does.
- The error page uses the locale and name of a site the request already
  read (loadedSite(), no store read), else English.
- An invalid preview token on feeds, sitemaps, llms.txt and JSON paths
  answers statusPlain's 410; no response echoes the failure reason
  (x-cloudhal-preview-error: invalid).
- The state step and error page live in status-entry.ts (unit-tested).
- Docs: ADR-0012 preview fallback, the status catalog in the translate
  skill, DESIGN.md §1, the Weblate guide and ADR-0034, RFC-0043 §1.9
  unknown hosts and §1.2/§1.8 wording.

Closes #167

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Madraka <8898640+Madraka@users.noreply.github.com>
…failure (#167 review)

- withErrorPage also rewrites Astro's empty 500 fallback (no body, no
  content type); a 500 with an explicit non-HTML type still passes.
- The blog starter's catch-all rethrows when Astro renders it as the
  error route, so a page that throws gets the error page with a reference
  and a log line instead of the catch-all's 404 for /500.
- The error page names the published snapshot's site node
  (SiteResolver.publicSite), the live row only before the first publish.
- A failed multi-site hostname lookup is a store failure: health answers
  503 { ok: false }, page and machine paths the English unavailable
  answer (503, Retry-After: 300). health moved to status-entry.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Madraka <8898640+Madraka@users.noreply.github.com>
…ts (#167 review)

- resolveRequest no longer awaits publicSite: the error page's site is
  filled in the background (a failing render has already awaited the
  same snapshot load), and not at all for /__cloudhal/health, which
  reads only the pointer.
- Tests: publicSite on an unreadable snapshot returns undefined; on
  workerd, a multi-site Worker whose site_hosts cannot be read answers
  health 503 { ok: false } and the English unavailable page and feed
  (fails with a 500 when the choose() catch is removed).
- Wording: Astro renders its error route with the original path, so
  the starter's catch-all answered its 404 for that path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Madraka <8898640+Madraka@users.noreply.github.com>
@Madraka
Madraka merged commit 1ea52c1 into main Nov 8, 2026
9 checks passed
@Madraka
Madraka deleted the claude/charming-jennings-72b55a branch November 8, 2026 16:36
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.

Status pages: withErrorPage 503→500 regression and RFC-0043 deviations (follow-up to #162)

1 participant