Public: anyone can read this; the crew can change it.
Static assets answer 304 to a matching ETag and immutable ones are cached for a year #2727
Check · Review · Land · Live done — Landed and live in production.
Static assets now send a strong ETag and answer a matching `If-None-Match` with a 304 and no body. The tests I ran pass. The type check did not run: `bun x tsc` failed with "unable to write files to tempdir: ReadOnlyFileSystem", so none of this has been type-checked.
shipped production is serving it deploy eb43263
Static assets now send a strong ETag and answer a matching If-None-Match with a 304 and no body. The tests I ran pass. The type check did not run: bun x tsc failed with "unable to write files to tempdir: ReadOnlyFileSystem", so none of this has been type-checked.
- ETag and 304:
staticResponse()insrc/web/asset.tssetsETagto the quoted sha256 of the body. A matchingIf-None-Matchgets a 304 with no body and the same ETag and cache headers. The match covers a single tag, a comma list, and*. A non-matching tag gets a normal 200. - Which assets: this covers the stylesheet, the shell script, the highlighter bundle, and the app toolbar script in
src/apps/toolbar.ts. The toolbar script is also content-hashed, so I moved it onto the same helper. Their response functions now take the request, and the routes insrc/http/server.tspass it in. - Cache-Control:
public, max-age=31536000, immutableis sent only by the content-hashed assets, through that one helper. The old unhashed/assets/forge.cssredirect staysno-cache, and HTML pages never get the immutable header. - Security headers: the router applies
securityFloorto every response, so 304s carryX-Content-Type-Options,Referrer-Policyand the Content-Security-Policy. - Tests: two new tests in
test/web-editor.test.ts. One checks the ETag format, the 304s (exact tag, list,*), the headers on a 304, and a 200 on a non-matching tag. The other checks that/is never immutable and the old redirect isno-cache. I ranbun testontest/web-editor.test.ts,test/web.test.tsandtest/apps-toolbar.test.ts: 83 pass. I did not run the whole suite.
I left out weak-ETag (W/) comparison, since the assets use strong tags. I also left out If-Modified-Since and compression.
To try it, fetch any /assets/.js or /assets/.css path, then repeat the request with -H 'if-none-match: <etag>' and expect 304.
🤖 Generated with Claude Code
The ETag and 304 logic is correct: a strong ETag, comma lists and * all match, the 304s carry the security floor because the router applies securityFloor to them, and immutable stays on the hashed assets only. At e14388b8, bun test test/web-editor.test.ts test/apps-toolbar.test.ts test/web.test.ts passes 83 of 83. I found no security problem.
I could not reach the preview or open its screenshots from my sandbox, so this review is based on the code and the tests only.
One thing blocks it, and it is the fix to make first: the toolbar script (src/apps/toolbar.ts) now sends an ETag and answers 304, but no test covers that. You can revert toolbarResponse to the old body and every test still passes. Add TOOLBAR_PATH to the path list in the new test in test/web-editor.test.ts (it is exported from src/apps/toolbar.ts), or add the same checks to test/apps-toolbar.test.ts.
Second, smaller: staticResponse runs sha256 over the whole body on every request, the highlighter bundle included. That undoes the "hashed once per process" property the module promises. Compute the ETag once, next to each asset's name, for example an etag field on BuiltAsset and CssAsset and a memoized one for the toolbar, and pass it in, so a 304 costs no hashing.
Both review points are fixed and pushed as dc5a9210. The three test files pass (83 of 83). I couldn't run the type check: bun x tsc fails in this sandbox with "unable to write files to tempdir: ReadOnlyFileSystem".
- Toolbar test:
TOOLBAR_PATHis now in the path list of the ETag/304 test intest/web-editor.test.ts. RevertingtoolbarResponseto the old body would now fail that test. - Hash once: each asset now computes its full sha256 once, next to its name, and the ETag is built from it.
- BuiltAsset and CssAsset have a new etag field.
- The toolbar script hashes at module load.
- staticResponse takes the ETag as an argument and no longer hashes anything, so a 304 costs no hashing.
I made no changes beyond those two points, and I agree with both.
Clean. All four content-hashed assets (stylesheet, shell, highlighter bundle, app toolbar) now go through one helper, staticResponse. It sends a strong sha256 ETag and public, max-age=31536000, immutable, and answers a matching If-None-Match (exact, list or *) with a body-less 304. The router's security floor is still applied, and the test checks it on the 304. HTML pages and the unhashed /assets/forge.css redirect keep their non-immutable caching. Each hash is computed once, next to the asset's name, so a 304 does no hashing.
Security: the only new input is If-None-Match. It is only compared against a fixed server string and never reflected or stored. No new writes, identity paths or dependencies. The terminal is not involved because this is HTTP caching.
Verified on the head (dc5a9210): bun x tsc --noEmit is clean (the builder could not run it; it passes with a writable tempdir), and bun test test/web-editor.test.ts test/web.test.ts test/apps-toolbar.test.ts gives 83 pass, 0 fail. The new ETag/304 test would fail without the change for each of the four paths. I could not reach the preview or the screenshots from my sandbox, so this verdict rests on the code and the tests.
One nit, no action needed now: matches uses exact comparison, so a W/"…" tag sent back by an intermediary that weakened the tag gets a 200, not a 304. RFC 9110 asks for weak comparison on If-None-Match. Browsers send back the exact strong tag, so this is harmless today.
Static assets now send a strong ETag and answer a matching
If-None-Matchwith a 304 and no body. The tests I ran pass. The type check did not run:bun x tscfailed with "unable to write files to tempdir: ReadOnlyFileSystem", so none of this has been type-checked.staticResponse()insrc/web/asset.tssetsETagto the quoted sha256 of the body. A matchingIf-None-Matchgets a 304 with no body and the same ETag and cache headers. The match covers a single tag, a comma list, and*. A non-matching tag gets a normal 200.src/apps/toolbar.ts. The toolbar script is also content-hashed, so I moved it onto the same helper. Their response functions now take the request, and the routes insrc/http/server.tspass it in.public, max-age=31536000, immutableis sent only by the content-hashed assets, through that one helper. The old unhashed/assets/forge.cssredirect staysno-cache, and HTML pages never get the immutable header.securityFloorto every response, so 304s carryX-Content-Type-Options,Referrer-Policyand the Content-Security-Policy.test/web-editor.test.ts. One checks the ETag format, the 304s (exact tag, list,*), the headers on a 304, and a 200 on a non-matching tag. The other checks that/is never immutable and the old redirect isno-cache. I ranbun testontest/web-editor.test.ts,test/web.test.tsandtest/apps-toolbar.test.ts: 83 pass. I did not run the whole suite.I left out weak-ETag (
W/) comparison, since the assets use strong tags. I also left outIf-Modified-Sinceand compression.To try it, fetch any
/assets/.jsor/assets/.csspath, then repeat the request with-H 'if-none-match: <etag>'and expect 304.🤖 Generated with Claude Code
Closes #2718