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
Check · Review · Land · Live done — Landed and live in production.
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
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
etagjoined 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-Matchfrom 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
etagif 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-storepage and a 404 carry noetag. - It does not exercise a real signed-in or proof request. The
private, no-storecase 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.
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.
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.
- Box 4 asks for a proof page to have no tag. The test only uses an upstream
private, no-storepage 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 noetag. - 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.
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 asIf-None-Matchby a signed-in visitor gets 200, not 304. With thevouched?.who === undefinedguard removed, this test fails. - Proof guard is covered only indirectly: the proof path also sets
cache-control: private, no-store, so theproof === undefinedguard 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 forsrc/apps/proxy.tsintest/cookie-flags.test.tsback to 2.
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.
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 --noEmitandbun scripts/gate.tspass. 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 usekeep_plan: the tool wasn't available in this session.What changed (
src/apps/proxy.ts)etagjoined 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.If-None-Matchfrom 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.etagif 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-controlsaysprivateorno-store.vary: accept-encoding.Test (
test/apps-proxy-etag.test.ts)W/tag, and a repeat with that tag returns 304 with an empty body.forgeUrl(which changes the toolbar markup) does the same.private, no-storepage and a 404 carry noetag.private, no-storecase stands in for the proof path.Left alone
Closes #9136