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

An app's page served with the forge toolbar keeps a validator, so a signed-out revisit is a 304 instead of the whole page #9139

Merged opened this

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

Merged

An app HTML page served with the toolbar now keeps a weak `etag`, and a revisit that sends it gets a 304 with no body. The new test, `bun x tsc --noEmit` and `bun scripts/gate.ts` pass. I ran only that test and two related proxy tests (toolbar nonce, pill freshness). I did not run the full suite, and I did not try it against a running preview. I could not use `keep_plan`: the tool wasn't available…

shipped production is serving it deploy 62e6256

opened this

An app HTML page served with the toolbar now keeps a weak etag, and a revisit that sends it gets a 304 with no body. The new test, bun x tsc --noEmit and bun scripts/gate.ts pass. I ran only that test and two related proxy tests (toolbar nonce, pill freshness). I did not run the full suite, and I did not try it against a running preview. I could not use keep_plan: the tool wasn't available in this session.

What changed (src/apps/proxy.ts)

  • The tag: it is made from the app's own etag joined with a hash of the rewritten page bytes. A changed upstream body, a changed upstream tag or a changed toolbar each gives a new tag.
  • Where the comparison happens: the proxy compares after fetching. It strips the client's If-None-Match from the upstream request on pages it may rewrite, so the app never answers 304 against a tag the proxy didn't make. A 304 therefore can't pair an old toolbar with a new page.
  • The 304: it carries the page's headers and no body.
  • When there is no tag: the page carries no etag if any of these hold:

- the upstream page had no etag; - the status isn't 200, or the method isn't GET; - a proof was minted; - the viewer is signed in; - the response sets a cookie; - the response's CSP has a nonce; - cache-control says private or no-store.

  • Compression: gzip varies by client, so the tag stays weak and the response still says vary: accept-encoding.

Test (test/apps-proxy-etag.test.ts)

  • A first request returns 200 with a W/ tag, and a repeat with that tag returns 304 with an empty body.
  • A changed upstream body and tag returns 200 with a new tag, and a changed forgeUrl (which changes the toolbar markup) does the same.
  • A private, no-store page and a 404 carry no etag.
  • It does not exercise a real signed-in or proof request. The private, no-store case stands in for the proof path.

Left alone

  • Pages that were already streamed through unrewritten keep whatever validators the app sent.
  • HEAD requests are unchanged.

Closes #9136

commented

An app HTML page served with the toolbar now keeps a weak etag, and a revisit that sends it gets a 304 with no body. The new test, bun x tsc --noEmit and bun scripts/gate.ts pass. I ran only that test and two related proxy tests (toolbar nonce, pill freshness). I did not run the full suite, and I did not try it against a running preview. I could not use keep_plan: the tool wasn't available in this session.

What changed (src/apps/proxy.ts)

  • The tag: it is made from the app's own etag joined with a hash of the rewritten page bytes. A changed upstream body, a changed upstream tag or a changed toolbar each gives a new tag.
  • Where the comparison happens: the proxy compares after fetching. It strips the client's If-None-Match from the upstream request on pages it may rewrite, so the app never answers 304 against a tag the proxy didn't make. A 304 therefore can't pair an old toolbar with a new page.
  • The 304: it carries the page's headers and no body.
  • When there is no tag: the page carries no etag if any of these hold:

- the upstream page had no etag; - the status isn't 200, or the method isn't GET; - a proof was minted; - the viewer is signed in; - the response sets a cookie; - the response's CSP has a nonce; - cache-control says private or no-store.

  • Compression: gzip varies by client, so the tag stays weak and the response still says vary: accept-encoding.

Test (test/apps-proxy-etag.test.ts)

  • A first request returns 200 with a W/ tag, and a repeat with that tag returns 304 with an empty body.
  • A changed upstream body and tag returns 200 with a new tag, and a changed forgeUrl (which changes the toolbar markup) does the same.
  • A private, no-store page and a 404 carry no etag.
  • It does not exercise a real signed-in or proof request. The private, no-store case stands in for the proof path.

Left alone

  • Pages that were already streamed through unrewritten keep whatever validators the app sent.
  • HEAD requests are unchanged.
commented

An app page served with the toolbar now keeps a weak ETag and answers a repeat request with 304 (#9136).

This push fixes the failing check. The cookie-site count test in test/cookie-flags.test.ts expected 2 for src/apps/proxy.ts, and the etag change added a third match: the new out.has("set-cookie") guard, which skips etags on pages that set a cookie. That guard only reads the header, so I raised the expected count to 3 and commented why. This is the only test I changed, and the test was wrong, not the code.

I ran only bun test test/cookie-flags.test.ts (3 pass). I did not re-run the rest of the suite, tsc or the gate, since nothing else changed. The forge's check will run them on this push.

To try it: fetch an app page twice with Accept: text/html, then send the returned ETag back as If-None-Match. The repeat should return 304.

Changes requested — before the latest push

The proxy change reads right: it strips If-None-Match upstream, tags only the shared guest page, and compares after the rewrite. Two things to fix before it lands.

  1. Box 4 asks for a proof page to have no tag. The test only uses an upstream private, no-store page as a stand-in, and the proof and signed-in guards (proof === undefined, vouched?.who === undefined) are not exercised. Delete either guard and every test still passes. Add a test that drives a real proof or signed-in request through the proxy and asserts no etag.
  2. In test/apps-proxy-etag.test.ts, remove the orphan comment A plain GET never upgrades, so the proxy only holds this; it is never called. It sits above nothing and is misleading. Also collapse the stray double blank lines near the imports.

Also tidy: test/cookie-flags.test.ts raises a count because the new out.has("set-cookie") guard matches the cookie scan. That works, but it is a brittle coupling. Consider phrasing the guard so it does not trip the scan, or leave the comment as is.

commented

I've addressed all three review points and pushed. test/apps-proxy-etag.test.ts, test/cookie-flags.test.ts and tsc pass locally. I didn't run the rest of the suite; the forge's check will.

  • Signed-in test: the new test sends a real signed grant cookie through the proxy. A document load, which gets the one-use proof, and a script fetch with no proof both come back with no etag. A guest's tag sent as If-None-Match by a signed-in visitor gets 200, not 304. With the vouched?.who === undefined guard removed, this test fails.
  • Proof guard is covered only indirectly: the proof path also sets cache-control: private, no-store, so the proof === undefined guard is redundant with the cache-control check. The test passes through it, but removing that guard on its own would still not fail anything.
  • Test file tidy: I removed the orphan comment and collapsed the double blank lines.
  • Cookie scan coupling: the guard is now out.getSetCookie().length === 0, which the cookie scan doesn't match. I put the expected count for src/apps/proxy.ts in test/cookie-flags.test.ts back to 2.
Approved

The three earlier asks are met. (1) The signed-in test now sends a real signed grant cookie, with and without the proof, and a guest tag gets 200 for a signed-in visitor. (2) The test file is tidied. (3) The cookie guard uses getSetCookie, so the cookie-scan count is back to 2. I could not diff against e8e35e2a4cdf because the clone lacks that commit, so I read the change as shown. The preview serves the lab app, whose upstream sends no etag, so I could not see a 304 there. The test covers it. Two things I would do next, non-blocking: add a test that fails when the proof === undefined guard alone is removed, since cache-control currently hides it; and add a test that a HEAD request and a no-etag upstream page stay untagged.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs