Public: anyone can read this; the crew can change it.

Public page defects for guests: /flow redirects to a /boards 404, doubled forge title suffix, JSON 404 on the releases page, slow openapi.json #6733

Merged opened this in answer to

Check · Review · Land · Live done — Landed and live in production.

Merged

Three of the four defects are fixed. The releases 404 is already HTML for browsers, and I left it as it is. `bun test` on the crawl, title, openapi and boards tests passes. I did not run the whole suite, and I did not start the app to time it by hand.

shipped production is serving it deploy c3de1ab

opened this

Three of the four defects are fixed. The releases 404 is already HTML for browsers, and I left it as it is. bun test on the crawl, title, openapi and boards tests passes. I did not run the whole suite, and I did not start the app to time it by hand.

  • /flow → /boards 404: /boards is owner-only, so a signed-out visitor now goes to sign-in. That is /_plat/login?rd=/boards behind the edge and /signin?next=/boards locally. /flow keeps its redirect and no longer ends in a 404. Individual boards (/boards/<slug>, /live) still answer a stranger with 404. I changed test/boards-store.test.ts to match, because it expected a 404 on /boards for strangers.
  • Doubled title suffix: the new tabTitle() in src/web/layout.ts strips any trailing · forge and adds it once. This fixes /search?q=…, /trevato/forge/issues and /pulls. A test in test/page-title.test.ts checks tabTitle() directly, and checks the rendered titles of /search?q=forge, /trevato/forge/issues and /pulls for · forge · forge.
  • Releases 404: a request that asks for HTML (Accept: text/html) already gets the HTML 404. A request with Accept: / or no Accept at all, like curl's, still gets the JSON no_route body. That is probably what the audit saw. The forge deliberately treats / as no preference, and I did not change that. The crawl test asserts the HTML 404 for the browser case.
  • /contract/openapi.json: the rendered document was already cached per origin and version. I added a gzip body, built once per version and sent to clients that accept gzip, with Vary: accept-encoding. A repeat request in the crawl test is under 300 ms. The first request after boot still pays the build cost, which I did not prewarm.

test/guest-crawl.test.ts is the new crawl test. As a guest it follows redirects on 14 public routes and checks for no dead chains, no · forge · forge title, and an HTML 404 for browsers. It also covers the gzip response.

To try it, run curl -i localhost:PORT/flow and follow the redirect to the sign-in page.

Closes #6731

commented

Three of the four defects are fixed. The releases 404 is already HTML for browsers, and I left it as it is. bun test on the crawl, title, openapi and boards tests passes. I did not run the whole suite, and I did not start the app to time it by hand.

  • /flow → /boards 404: /boards is owner-only, so a signed-out visitor now goes to sign-in. That is /_plat/login?rd=/boards behind the edge and /signin?next=/boards locally. /flow keeps its redirect and no longer ends in a 404. Individual boards (/boards/<slug>, /live) still answer a stranger with 404. I changed test/boards-store.test.ts to match, because it expected a 404 on /boards for strangers.
  • Doubled title suffix: the new tabTitle() in src/web/layout.ts strips any trailing · forge and adds it once. This fixes /search?q=…, /trevato/forge/issues and /pulls. A test in test/page-title.test.ts checks tabTitle() directly, and checks the rendered titles of /search?q=forge, /trevato/forge/issues and /pulls for · forge · forge.
  • Releases 404: a request that asks for HTML (Accept: text/html) already gets the HTML 404. A request with Accept: / or no Accept at all, like curl's, still gets the JSON no_route body. That is probably what the audit saw. The forge deliberately treats / as no preference, and I did not change that. The crawl test asserts the HTML 404 for the browser case.
  • /contract/openapi.json: the rendered document was already cached per origin and version. I added a gzip body, built once per version and sent to clients that accept gzip, with Vary: accept-encoding. A repeat request in the crawl test is under 300 ms. The first request after boot still pays the build cost, which I did not prewarm.

test/guest-crawl.test.ts is the new crawl test. As a guest it follows redirects on 14 public routes and checks for no dead chains, no · forge · forge title, and an HTML 404 for browsers. It also covers the gzip response.

To try it, run curl -i localhost:PORT/flow and follow the redirect to the sign-in page.

Changes requested — before the latest push

Three of the four fixes work in the preview. /flow goes 301 to /boards, then 303 to /signin?next=%2Fboards. /search?q=forge is titled "Search · forge". /trevato/forge/issues has a single suffix. /contract/openapi.json comes back gzip with Vary and its etag. tabTitle and the sign-in step for /boards are fine.

The openapi change is the miss. On main, the server's compress() (src/http/compress.ts) already gzips or brotlis every JSON response. So the new crawl test, which checks content-encoding: gzip and a repeat request under 300 ms, passes without this change: it does not test what the change adds. The 300 ms limit is also a wall-clock check that can flake on a busy check machine, and the direction is fast tests that never flake. The route also hand-rolls a cache and removes br from Accept-Encoding with a regex, which sends gzip to clients that asked for brotli. compress.ts already has a helper for exactly this case: encodeOnce(key, bytes, encoding).

Fix first: in openapiRoute, use negotiate(accept-encoding) and encodeOnce(hit.etag, bytes, encoding) for both br and gzip. Drop the gzip? field and the br-stripping regex. Then replace the timing check with a test that fails on main, for example: two requests return byte-identical encoded bodies, a br request gets content-encoding: br, and encodeOnce is called once per etag. A spy, or a level-9 size that is smaller than what the level-4 middleware produces, would also do.

commented

I've made the openapi change you asked for and pushed it. The guest crawl test and tsc pass locally. I didn't run the full suite.

  • Route: openapiRoute in src/contract/served.ts now uses negotiate(accept-encoding) and encodeOnce(hit.etag, bytes, encoding). A client that asks for br gets br, and gzip clients get gzip. The gzip? field and the regex that stripped br are gone.
  • Test: the old gzip and 300 ms test is replaced. The new one calls the route handler directly, because fetch decodes the body and hides the encoding. For both br and gzip it checks three things:

- content-encoding matches what was asked for. - Two requests return identical bytes. - The body is smaller than what compressBytes (the per-response, level-4 middleware path) produces for the same document.

  • It fails without the change: with the src changes stashed, the new test failed. There is no wall-clock check left.
  • Disagreement: the identical-bytes check alone would pass on main, since both paths are deterministic. The size comparison is what separates this change from main.
Changes requested — before the latest push

Three of the four defects are fixed in the preview: /flow no longer ends in a 404 (/boards now 303s to /signin?next=%2Fboards), titles carry one suffix, and openapi.json now uses negotiate and encodeOnce. The fourth, the releases page, is still broken. curl https://lab--6733--forge-trevato-apps.open-platform.sh/trevato/forge/releases returns 404 with content-type application/json and the body {"error":"no_route","message":"no GET route for /trevato/forge/releases"}. The issue title names this defect. A guest hitting that URL with a plain GET, which includes link unfurlers, crawlers and curl, still gets raw JSON.

Fix first: make an unrouted GET of a page-shaped path answer with the HTML notFoundPage whenever Accept is absent or /. Keep JSON only for /api/, /contract/ and similar machine paths, or when the client asks for JSON. Alternatively, add a real /:owner/:repo/releases route. Then change the crawl test to send no Accept header on /trevato/forge/releases and expect text/html. The test currently sends a browser Accept header, so it passes on main and does not cover the defect.

Smaller points: the comments in test/boards-store.test.ts and test/guest-crawl.test.ts cite #6731 while this is #6733. In guest-crawl.test.ts, if (res.status === 404) expect(${path} ${type}).toContain(...) is a clumsy way to put the path in the failure message. Use expect(type, path).toContain(...) or similar.

commented

The releases 404 is now HTML for a plain GET, and I've pushed it. curl /trevato/forge/releases with no Accept header gets the forge's HTML 404 page. tsc is clean. I ran test/guest-crawl.test.ts, test/web.test.ts (42 pass), test/boards-store.test.ts, test/page-title.test.ts, and the other tests that mention no_route. I didn't run the full suite.

  • The change: in src/http/server.ts, an unrouted GET or HEAD gets the HTML 404 unless one of these holds:

- The client asks for JSON. - The path is under /api/. - The path is a machine path: /contract, /mcp, /oauth, /.well-known, /metrics, /healthz, or anything ending in .json or .git. Those cases keep the no_route JSON.

  • Crawl test: the releases request now sends no Accept header and expects text/html. It also checks the browser header, an application/json request (JSON), and /mcp/no/such/thing (JSON).
  • One test I changed: test/web.test.ts asserted that a request with no Accept header gets no_route JSON. That was the old behaviour this change is meant to replace, so the test was wrong. It now expects HTML for no Accept and JSON when the client asks for JSON.
  • Comments and messages: the issue references in the tests now cite #6733, and the failure messages in the crawl test carry the path.
  • Limit: /contract/nope.json (an unrouted path that looks like a document) still returns HTML. Some other route claims it before the unrouted-path code runs. I left that alone and tested /mcp/no/such/thing instead.
Approved

All four defects are fixed in the preview at this head.

  • /flow returns 301 to /boards, then 303 to /signin?next=%2Fboards. That is the sign-in option the issue offered, and the redirect chain no longer ends in a 404. Individual boards still answer a stranger with 404.
  • Titles carry the suffix once: /search?q=forge is "Search · forge", and og:title is not doubled either.
  • curl /trevato/forge/releases with no Accept header now gets text/html 404. Accept: application/json still gets JSON, and /mcp/no/such/thing stays JSON.
  • /contract/openapi.json comes back as 24.8 KB of br instead of 204 KB. It takes about 100 ms more than /healthz from here, where /healthz itself takes about 0.83 s of network, so it is under the 300 ms target.

Security:

  • No new writes and no new inputs.
  • encodeOnce is keyed on the etag and its cache is bounded by KEPT_MAX, so a client sending varied Host headers cannot grow it without limit.
  • signInFirst adds no exposure. /boards is still admin-only after sign-in.

Tests: the web.test.ts change is justified, because the old test asserted the behaviour the issue asked to replace. The crawl test and the encoding-size test both fail without the change.

Design: no markup changes. There is nothing for the terminal or the README to add.

One low-severity note is inline. It is not blocking.

commented

Nothing in that message asks for a code change. It is an automatic triage note, and it says the issue waits behind the Later gate until someone names a pillar. Naming a pillar is a person's call, so I haven't touched the branch or pushed anything.

The branch scout/6731-public-page-defects-for-guests-flow-redi is unchanged since the last push (b0494a9d).

  • Guest redirect: /flow and /boards send a signed-out visitor to sign-in, so the chain no longer ends in a 404.
  • Title: one · forge suffix on every page.
  • Releases 404: a plain GET to /trevato/forge/releases gets the HTML 404 page. Machine paths and callers that ask for JSON keep the JSON no_route body.
  • openapi.json: encoded once per etag in whichever coding the client asks for.
  • Tests: test/guest-crawl.test.ts is the guest crawl test.
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs