Repository navigation
fix(runtime): GET /i18n/translations/:locale carries an ETag and Cache-Control: private, no-cache, and a matching If-None-Match is answered 304 (#25785) - #25956
Conversation
…ol: private, no-cache, and a matching If-None-Match is answered 304 GET /i18n/translations/:locale (and its ?locale= spelling) served the whole locale bundle on every console open with no validator, so the browser could not revalidate it. The answer now carries a weak ETag over the bytes it serves and `Cache-Control: private, no-cache`; a request whose If-None-Match names that representation is answered 304 with no body. Claude-Session: https://claude.ai/code/session_01HFAuCZPty5SsvseoPRgukQ Co-authored-by: Claude <noreply@anthropic.com>
…the 304 over a real Hono socket Claude-Session: https://claude.ai/code/session_01HFAuCZPty5SsvseoPRgukQ Co-authored-by: Claude <noreply@anthropic.com>
…toResponse; changeset for the translations validator Claude-Session: https://claude.ai/code/session_01HFAuCZPty5SsvseoPRgukQ Co-authored-by: Claude <noreply@anthropic.com>
…-typecheck ratchet Claude-Session: https://claude.ai/code/session_01HFAuCZPty5SsvseoPRgukQ Co-authored-by: Claude <noreply@anthropic.com>
…ng answers (kind 1) The translations 200 carries ETag and Cache-Control, which deps.success cannot express, and the matching-If-None-Match 304 has no body. Row 0 -> 2 with its note; no other row and no gate logic moves. Claude-Session: https://claude.ai/code/session_01HFAuCZPty5SsvseoPRgukQ Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 27 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 74966ee15c6c1ac587366b860cf2560d6e9cdab7 && git checkout 74966ee15c6c1ac587366b860cf2560d6e9cdab7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 493a6c4a5f5b83d953f19092279abdf07b6f4017 773c3f3bc02786ba72ec530247136b0bda2e82af && git checkout -B drift-repro 493a6c4a5f5b83d953f19092279abdf07b6f4017 && git merge --no-ff 773c3f3bc02786ba72ec530247136b0bda2e82af
node scripts/docs-audit/affected-docs.mjs --json 493a6c4a5f5b83d953f19092279abdf07b6f4017
|
席位核对ACCEPT ·
Generated by Claude Code |
Requested by Elon · project thread
Part of #25785 (the first step the triage comment 6397341079 names: the validator and cache directive on the bundle). #25785 remains open for its second half, keeping
metadataFormsout of the boot bundle, which is a wire-shape change fordomain:spec.Clause-②: no
Metadata-protocol: none
The claim is 6407675488 (seat
domain:server#2, card threadsession_01HFAuCZPty5SsvseoPRgukQ). Declared surface addition: amendment 6408228005 (onescripts/check-route-envelope.mjsrow, below).What changes
GET /i18n/translations/:locale(and its?locale=spelling) answered every console open with the whole locale bundle and noETagorCache-Control, so the browser could not revalidate what it already held. On a hosted tenant that is 80–88 KB compressed per open (the card's cloud#2914 reading).packages/runtime/src/domains/i18n.ts, the one handler body every transport converges on. A newtranslationsAnswerbuilds the translations answer on both of the route's success exits (the direct read and thezh→zh-CNfallback):200isdeps.success's envelope, unchanged, plusETagandCache-Control: private, no-cache;ETagis weak,W/"BASE64URL-SHA256"overJSON.stringifyof the exact envelope served (locale,requestedLocaleandtranslations), computed on every read. Nothing cheaper (a counter, the bundle object's identity) stands in for the bytes, so a304can only ever pin a caller to the bundle it really holds. It is weak because it vouches for the JSON, not for the encoding a compressing layer puts on the wire. This is the invariantgetMetaItemCachedstates for/meta(packages/metadata-protocol/src/protocol.ts), with SHA-256 in place of its 32-bit hash;If-None-Matchnames that representation (RFC 9110 §13.1.2: a comma list, weak comparison, or*) is answered{ status: 304, headers }with nobody. The304carries the same two headers (§15.4.5). A malformed or stale tag is served the full bundle, the safe direction;privatebecause the bundle is served only to a signed-in caller and names the application's own objects;no-cachebecause the bundle changes without notice (an authored translation, a package install), so freshness is the validator's job and never a TTL.401, and no refusal carries either header.dispatcher-plugin.ts'ssendResultBaseand@objectstack/hono'stoResponsealready writebody: undefinedas a body-less response (JSON.stringify(undefined)isundefined, so Hono buildsResponse(undefined, { status: 304 })). Both are now pinned on that.scripts/check-route-envelope.mjsDISPATCHER_DOMAINS['i18n.ts']goes 0 → 2 with a note: the two answers are kind 1, the same classificationmeta.tstook forVary: Accept-Language(deps.successtakes no headers, and a304has no envelope a helper could build). No other row and no gate logic moves, andDomainHandlerDepsgains no member.@objectstack/runtimepatch.Not touched:
packages/spec, every published response schema (the200body is byte-identical andGetTranslationsResponseSchemais unchanged),/i18n/locales,/i18n/labels/*.The console needs no change:
ui/apps/console/src/loadLanguage.tsreads the bundle throughcreateAuthenticatedFetch, a plainfetch(input, { ...init, headers })with the default cache mode, so the browser sendsIf-None-Matchitself and hands the console the cached body on a304.Measured on a real composition
examples/app-crmbooted withpnpm dev:crm -- --freshfrom this branch (fullpnpm build,VERDICT command-exit 0), signed in as the seeded admin, read over loopback withcurl:ETaginIf-None-Matchzh-CN200, 220,391 B raw (67,303 B atgzip -9; the dev server does not compress)304, 0 Ben200, 213,264 B raw (58,351 B atgzip -9)304, 0 BCache-Control: private, no-cacheand the sameETag; the200keepsVary: Originfrom the CORS layer.zh-CNanswered oneETag, and 25 conditional reads all answered304.ETagrecomputed offline from the servedzh-CNbody equals the header byte for byte (W/"cOAyyUQ7yb5bulH5g7ybiAf8sbJebhwAZur_peIct_E"): the invariant, read on the real bundle.JSON.stringifyplus SHA-256 of the 220 KB envelope is 1.8 ms per read on this 4-core shared box (stringify alone 0.9 ms; mean of 50 after a warm-up). Loopback wall time:200median 16.1 ms,304median 11.6 ms over 25 reads each. The saving the card targets is the bytes: on the ops seat's measured link (326–464 KB/s) the 80–88 KB compressed hosted bundle is about 0.2–0.27 s per warm open.origin/maincb0a2fcf's handler sets no header on this route (the card's measurement on pin888d6865shows none, and the ablation below reds every header assertion on that blob).Verification
Commits:
a373155e(the fix),e8c4b7a7(runtime tests),0b2e6592(adapter test, changeset),7b2a1a2b(test typing),773c3f3b(the gate row). Each reading names its head.packages/runtime/src/domains/i18n-translations-etag.test.ts, 21 tests on the handler body: the200headers on both spellings and the fallback arm; the validator recomputed from the served body; stable while unchanged; moves on a one-string change and on an in-place mutation of the same bundle object;304for the weak tag, its strong spelling, a list with and without spaces,*, a FetchHeadersobject and a repeated header in the record shape, on both spellings; full bundle for a stale, unquoted or empty tag; a validator issued before a change gets the new bundle; one locale's validator does not answer another locale; the anonymous floor answers401with no headers and the provider unread.packages/runtime/src/i18n-translations-etag.hono.integration.test.ts, 4 tests over a real socket (LiteKernel+HonoServerPlugin+createDispatcherPlugin):200with both headers and the same envelope;304with a 0-byte body and both headers; a validator from before a bundle change is served the new bundle; anonymous with the validator is401, noETag.packages/adapters/hono/src/hono-not-modified-response.test.ts, 3 tests: a dispatcher{ status: 304, headers }leavestoResponseas a 0-byte304with both headers; the domain is handed the rawRequestwhoseif-none-matchit reads; the200control keeps its JSON body. Characterization pins:toResponseis unchanged, and they were green before the domain change.i18n.tsreplaced on disk by itsorigin/maincb0a2fcfblob under a trap, restored withgit checkout HEAD --): the two runtime files went 23 failed of 25. The 2 that stay green are controls that hold either way (the missing-locale400carries no headers; anonymous gets401). Restored blobbb41d094equalsHEAD's,git diff HEADempty,git statusclean.0b2e6592(the later commits touch only the integration test's typing, re-run green at7b2a1a2b, and the gate row):pnpm --filter @objectstack/runtime test(vitestlocalproject, under the verify lock,VERDICT command-exit 0): 446 files, 7,125 passed, 19 skipped.pnpm --filter @objectstack/hono test: 13 files, 228 passed.typecheckfor both packages exits 0 (runtime's includescheck:test-typecheck, which first caught two untypedres.json()reads in the integration test, fixed in 7b2a1a2).node scripts/pm/dispatch-gates.mjs --commandsat head773c3f3bderives 87 families on the 6 changed paths, and all 87 exit 0 (each exit code captured before any pipe).node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran: 87 derived, 87 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. The first run, at7b2a1a2b(67 families), had three that were not green:check-plugin-teardown-shape.mjs --self-testandcheck:dual-build-cjs-loadsanswered PREREQUISITE NOT MET (exit 3: the shallow clone lacked the fixture commit621a4876, anddist/was unbuilt), and both exit 0 after fetching that commit and a fullpnpm build;check:route-envelopewas red oni18n.ts handBuilt: found 2, declared 0, which the declared row above classifies.pnpm exec eslint --no-inline-configon the five changed code files exits 0.node scripts/pm/check-governed-merges.mjs --branch HEAD: 0 of 6 paths hit the register, NOT governed, 561 changed lines.Acceptance notes
domain:spec: the card's second half, keepingmetadataForms(46.8 KB raw on the measured tenant) out of the boot bundle and loading it with Studio, plus the optional scoping ofobjectsto the current app. Both change whatGET /i18n/translations/:localereturns, which is a wire-shape change (triage 6397341079). rest/i18n: GET /i18n/translations/:locale serves the whole locale bundle (288–361 KB raw, 80–88 KB compressed) on every console open with no ETag or Cache-Control, and carries Studio's metadataForms (46.8 KB) into the boot read #25785 stays open for it./i18n/localesand/i18n/labels/:object/:localestill carry no validator. Neither was measured on the open path and the card does not ask for them; noted, not filed.Generated by Claude Code