Repository navigation
Conversation
Contract web responses carried no cache directive or validator, so a
browser that had a webapp open kept running the superseded bundle after
`fdev website update` until a hard refresh. The app cannot fix this
itself: the stale document is the one that would carry the fix.
Apply `Cache-Control: no-store` as a layer over the
`/v{1,2}/contract/web/{key}/` and `/{key}/{*path}` routes, so all four
pipelines that emit contract bytes (shell page, sandboxed HTML, the JS
rewrite branch, ServeFile) get it from the route tree rather than per
handler. The no-trailing-slash 308 stays outside the layer so it remains
cacheable (#4019).
Adds a router-level test that drives every pipeline through the real
composed router for v1 and v2 against a stub node, asserting a 200, a
pipeline-specific body, and the header, plus its absence on the 308.
Closes #5323
Claude-Session: https://claude.ai/code/session_01GKBuAzByqMPegsXcUYJb7h
Adds a Playwright spec checking that the shell and the sandboxed app document carry Cache-Control: no-store through the node's production composition, and that the no-slash 308 does not. Also replaces let _ = send(..) in the router test's stub node with expect, which clippy's let_underscore_must_use requires. Claude-Session: https://claude.ai/code/session_01GKBuAzByqMPegsXcUYJb7h
Claude Rule Review: Failed to runThe Claude rule review step failed (likely an authentication or API issue). Check the workflow logs for details. Advisory review against |
- route_layer instead of layer, so the content sub-router's default 404 fallback is never wrapped (merged fallbacks replace each other, so a wrapped one could become the node-wide 404 depending on merge order). - New unit test pins no_store's semantics: it overrides a cacheable header a handler set, wraps only the routes it is given, and leaves the 404 fallback alone. - Router test: HEAD and the sandbox-as-document 303 are no-store; routes outside the subtree (notify worker max-age=3600, /v1/version, unmatched paths) are not. Stub node no longer panics inside its task when a handler has dropped its receiver. - Reword path_handlers comments that said #5323 was unfixed. Claude-Session: https://claude.ai/code/session_01GKBuAzByqMPegsXcUYJb7h
Re-review of the follow-up: the controls asserted no Cache-Control at all without checking status, and their comment overstated what the 404 case discriminates. Now they assert 200/404 and only that no-store did not leak. Also reword the asset_not_found_response comment. Claude-Session: https://claude.ai/code/session_01GKBuAzByqMPegsXcUYJb7h
Review summaryTier and lenses. Light by the risk list, but this touches every locally served webapp, so it got four independent Claude lenses at Blocking findings: none. Addressed in this PR
Not code defects, recorded in the description
[AI-assisted - Claude] |
…alone no-store on every contract-web response cancelled try.freenet.org's reverse-proxy immutable caching of hashed assets/* (Caddy adds its header and passes the node's through; browsers honour no-store), so every hosted River load would re-download its wasm. Narrow the route layer to responses whose Content-Type is text/html: the shell, the sandboxed HTML and raw HTML served through ServeFile. The HTML names the assets, so this keeps content-hashed apps fresh. The JS rewrite branch is left alone: it sends no Last-Modified or validator, so browsers never treat it as heuristically fresh, and a no-store there would cancel the same proxy caching for hashed JS. Unhashed ServeFile assets stay heuristically cacheable through Last-Modified; validators for them are a follow-up. Claude-Session: https://claude.ai/code/session_01GKBuAzByqMPegsXcUYJb7h
Re-review of the HTML-only scope found controls that could no longer fail: the 308, /v1/version and the 404 are not HTML, so the layer leaves them alone wherever it is applied. Now: - the out-of-subtree control is the node homepage (HTML), which catches the layer being widened to the whole router; - no_store_html's unit test gives the sub-router an HTML fallback, so wrapping the fallback (layer instead of route_layer) shows; - each router-test row asserts whether Last-Modified is present, pinning that the JS rewrite branch has none and ServeFile assets do, and telling raw ServeFile HTML apart from the sandbox rows; - application/xhtml+xml counts as HTML; - the Playwright spec requires both documents among the HTML responses. Comments now cite #5888 and describe the Caddy rule accurately. Claude-Session: https://claude.ai/code/session_01GKBuAzByqMPegsXcUYJb7h
Comment-only: the try.freenet.org @assets matcher covers files directly under assets/, and the no-store match includes application/xhtml+xml. Claude-Session: https://claude.ai/code/session_01GKBuAzByqMPegsXcUYJb7h
Review update: scope narrowed to HTML documentsThe first review's skeptical lens showed that Re-review of the narrowed head
Mutation checks at the new head (each red, then reverted): forcing the HTML predicate to Codex was unavailable (usage limit, retried once), so these Claude lenses stand in for it. [AI-assisted - Claude] |
Problem
A contract's bundle is mutable at a stable URL:
fdev website updatechanges what/v{1,2}/contract/web/<key>/…returns while every URL stays the same. Contract web responses carried no cache directive, so a browser could keep running a superseded build after a republish. The app cannot fix this itself, because the stale document is the one that would have carried the fix.What this changes
Responses under
/{key}/and/{key}/{*path}(v1 and v2) whoseContent-Typeis HTML (text/html,application/xhtml+xml) are now sent withCache-Control: no-store. That covers the shell page, the sandboxed contract HTML, and raw HTML thatServeFileserves when an app frames its own pages. Nothing else changes. It is aSetResponseHeaderLayer::overridingroute layer (no_store_htmlinclient_api.rs) rather than a header set in each handler, because HTML leaves through several unrelated pipelines and a header wired into only some of them reproduces the bug. It usesroute_layerso the sub-router's 404 fallback is never wrapped, and the no-trailing-slash 308 stays outside (#4019).Assets are deliberately left alone. An earlier revision of this PR set
no-storeon everything. The skeptical review found that this regresses try.freenet.org: its reverse proxy addsCache-Control: public, max-age=31536000, immutableto files directly underassets/and passes the node's header through alongside, so the browser would honour the node'sno-storeand re-download River's ~1.87 MB (gzipped) wasm on every load. The JS rewrite branch is also left alone. It sends noLast-Modifiedand no validator, so browsers give it no heuristic freshness and refetch it on normal loads anyway, and ano-storeon it would cancel the same proxy caching for hashed JS.This enables tower-http's
set-headerfeature (no new crate;Cargo.lockis unchanged)..claude/rules/browser-assets.md§6 records the invariant and the reason assets are excluded.How much of #5323 this fixes
I measured against the node on nova. The shell and sandboxed HTML responses never carried
Last-Modifiedor a validator, so a normal reload already refetched them. That means for apps with content-hashed assets, the "a reload doesn't help" symptom did not come from the HTML. This PR fixes the HTML half:no-storealso makes the shell ineligible for the back/forward cache in some browsers, so Back now reloads it instead of restoring it. The shell's live WebSockets already made that mostly the case, and the existing Back-navigation Playwright test (feat: integrate browser history with gateway iframe navigation #3839) passes.ServeFile: it carriesLast-Modified, so it was heuristically cacheable.The asset half remains, and it is tracked in #5888.
ServeFileassets carryLast-Modifiedand anETag, so browsers treat them as heuristically fresh for about 10% of their age. A normal reload keeps using them. #5323's statement that assets have no validators was wrong for these: the validators exist, butvariable_contentdrivesServeFilewith a synthesized request, so conditional requests are never evaluated. First-party apps, checked on 2026-10-14:assets/*-dxh<hash>.*files, so they are not exposed.main.cssandstyles.cssby fixed names. A Delta republish can leave the old CSS in use until those expire or the user hard-refreshes. Contract webapp assets with unhashed names stay stale after republish (validators + no-cache) #5888 proposes conditional requests plusno-cachefor assets.This PR fixes the HTML half of #5323, and #5888 carries the asset half with the evidence above. A commit message in this branch links #5323, so merging will close it. That is intentional: #5888 is the single open issue for what remains.
Testing
client_api::tests::contract_html_is_served_no_store_and_assets_are_notdrives the real composed router (as_router_with_origin_contracts) against a stub node that serves a real packed bundle. For v1 and v2 it requests seven URLs, one per pipeline: shell, deep-link shell, sandboxed root, sandboxed sub-page, rawServeFileHTML, the JS rewrite branch and aServeFileCSS asset. Each row asserts a 200, a pipeline-specific body, the expectedCache-Control(no-storefor HTML, none for assets), and whetherLast-Modifiedis present. That last check pins the premises above and tells the raw-HTML row apart from the sandbox rows.no-store, and the notification worker keepsmax-age=3600.client_api::tests::no_store_html_overrides_only_html_and_wraps_only_its_routeschecks that HTML and XHTML withmax-age=3600becomeno-store, that CSS is untouched, that an HTML route outside the wrapper is untouched, and that an HTML fallback inside the wrapper is untouched.is_html_responsetotruefails both tests.falsealso fails both.route_layerforlayerfails the unit test.crates/core/tests/playwright/tests/contract-cache-headers.spec.tschecks in a real browser that the shell and the sandboxed iframe documents areno-storeand the 308 is not. I ran the full suite locally at11da5f89b: 180 passed across chromium, firefox and webkit. At an earlier revision, removing the layer failed the spec in all three engines.cargo test -p freenet --lib server::passes, andcargo clippy -p freenet --all-targets -- -D warningsis clean.Review
Several Claude lenses reviewed this. Code-first, testing, skeptical and big-picture ran on the first revision. Skeptical and code-first ran again on the narrowed revision
d18c4ba84, and a skeptical read covered the test fix-up11da5f89b. The findings are addressed. The external pass did not run:codex review --base origin/mainfailed twice with a usage-limit error, so these Claude lenses stand in for it.[AI-assisted - Claude]
https://claude.ai/code/session_01GKBuAzByqMPegsXcUYJb7h