Skip to content

CS-11010: ETag/304 for application/vnd.card+json#4683

Merged
habdelra merged 7 commits intomainfrom
cs-11010-etag-vnd-card-json
May 6, 2026
Merged

CS-11010: ETag/304 for application/vnd.card+json#4683
habdelra merged 7 commits intomainfrom
cs-11010-etag-vnd-card-json

Conversation

@habdelra
Copy link
Copy Markdown
Contributor

@habdelra habdelra commented May 6, 2026

Closes CS-11010. Companion to #4667 (CS-11037).

How this fits with #4667

#4667 lets a _federated-search caller send include: [] to skip the server-side side-loading of relationships entirely. The big win there is the cards-grid prerender path: 87% of staging render-timeouts trace to that one query shape, each timeout burning the full 90 s budget, because the server fan-outs N×M sequential link queries even when the consumer reads only instance.id/instance.constructor.

But once a caller opts out of side-loading, any other relationship it later wants has to be fetched individually as GET /<card> with Accept: application/vnd.card+json. Today every one of those individual GETs is served with cache-control: no-store, no-cache, must-revalidate and a fresh full-body response. A page that does N follow-up link fetches pays the realm's full ~100 ms JSON-assembly cost on every page view, every repeat visit, even when nothing changed.

This PR closes that loop:

  1. Realm-server: make _federated-search JSON:API-compliant via include #4667: _federated-search returns data[] only, no included[], no loadLinks work.
  2. This PR: any follow-up GET /<card> for a linked card gets an ETag and short-circuits to 304 on a warm cache.

So the cost shifted off _federated-search in #4667 doesn't reappear as N unconditional GETs on the very next render — it lands in the browser cache and stays there until the underlying card actually changes.

Scope difference vs. how #4667's description framed CS-11010

#4667's "Companion work" section described CS-11010 as making vnd.card+json ETag-aware on published realms. After looking at it, I went broader: this PR applies to all realms. Reasoning is in the ticket comment — modules and source files are already ETagged in every context (interactive, auth-gated, mutating realms included), so cards should follow the same convention. There's no realm class where instance data has a fundamentally different cacheability story than its module/source siblings.

Behavior

  • GET peeks at the index row first (one cheap row read), computes the ETag from the row's indexed_at, and short-circuits to 304 before the expensive loadLinks fan-out. Cache-hit cost drops from ~100 ms+ to ~5 ms.
  • PATCH (both the no-op short-circuit path and the post-write path) emits the ETag too, so a caller using the PATCH response body can revalidate against it without an extra round-trip GET.
  • ETag format: "<indexed_at>:card". Matches the existing <base>:<variant> convention from MODULE_ETAG_VARIANT (<lastModified>:module) and SOURCE_ETAG_VARIANT (<contentHash>:source) in runtime-common/realm.ts.
  • Cache-Control: public, max-age=0, must-revalidate for world-readable realms, private, max-age=0, must-revalidate for auth-gated ones, mirroring the source/module file path. private keeps a shared cache (CDN, intermediate proxy) from serving one user's response to another.

Why indexed_at is the right ETag base

The indexed_at column on a card's boxel_index row is set on every reindex. The realm's dependency-invalidation chain (the deps array on each row) means any change that affects the assembled card+json document — including a write to a transitively-linked card — triggers a reindex and bumps indexed_at on the dependent rows. So indexed_at is a complete fingerprint for the assembled data + included[] document.

Two alternatives I considered and rejected:

  • last_modified alone (the file's mtime). Untouched by dep-only reindexes, so a 304 here would serve stale included[] data after a linked card was updated. Wrong.
  • md5 of the assembled JSON body (matching the literal "lastModified + size + hash" pattern from the ticket). Correct, but you can't compute it without first paying the full assembly cost we're trying to skip — defeats the purpose.

indexed_at is the cheap, correct middle: one column read on the primary row, computed before loadLinks runs.

Wire path

GET /<card> (Accept: vnd.card+json)
  ↓
Realm.getCard (runtime-common/realm.ts)
  ↓
realmIndexQueryEngine.instance(url, { includeErrors: true })  ← cheap peek
  ↓
buildEtag(instance.indexedAt, 'card')  → "<n>:card"
  ↓
if If-None-Match matches → return 304 with ETag + Cache-Control + last-modified  ← skip loadLinks
  ↓
otherwise: realmIndexQueryEngine.cardDocument(url, { loadLinks: true })  ← full assembly
  ↓
return 200 with same ETag + Cache-Control + body

SearchResultDoc (in realm-index-query-engine.ts) gains an indexedAt: number | null field so the PATCH path can emit the ETag from a single cardDocument call without a redundant instance lookup.

Test plan

  • pnpm lint clean (runtime-common, includes lint:js + lint:types).
  • Type-checks pass.
  • CI runs the new ETag tests in packages/realm-server/tests/card-endpoints-test.ts. Local runs of the full card-endpoints suite were blocked by an unrelated environmental flake on this machine ("No standby page available for prerender" — Puppeteer/Chrome can't open new tabs, almost certainly an Ubuntu 24.04 + AppArmor-restricted-userns interaction). The new test cases themselves are scoped to request/response contracts and don't depend on prerender behavior; CI should exercise them cleanly.

New test cases

GET, public-readable realm:

  • returns an ETag and public cache-control on a 200 response
  • returns 304 when If-None-Match matches the current ETag — verifies status, ETag echo, cache-control passthrough, and empty body
  • returns 200 when If-None-Match does not match

GET, auth-gated realm:

  • 200 with permission (extended) — asserts private cache-control and ETag presence

PATCH, public-writable realm:

  • PATCH response carries an ETag and writes invalidate the previous one — verifies (a) post-write ETag differs from pre-write, (b) the old ETag no longer 304s after the write, (c) the new ETag does 304 a follow-up GET
  • no-op PATCH response carries an ETag matching the existing one — confirms the no-op short-circuit doesn't invent a new ETag

🤖 Generated with Claude Code

GET /<card> with Accept: application/vnd.card+json now emits an ETag
of the form "<indexed_at>:card", matching the existing
"<base>:<variant>" pattern that modules and source files use. On a
matching If-None-Match the handler short-circuits to 304 before the
loadLinks fan-out, which is the bulk of the per-request server cost
on a card with many included resources.

indexed_at is the right base because it bumps on every reindex —
direct file writes AND dependency-triggered re-writes via the deps
graph — so a change to any included card invalidates the parent's
cache. last_modified alone wouldn't (it's the file mtime, untouched
by dep-only reindexes), and an md5 of the assembled body would be
correct but couldn't be computed without paying the assembly cost
we're trying to skip.

Cache-Control follows the auth model: world-readable realms get
`public, max-age=0, must-revalidate` so a CDN can revalidate; auth-
gated realms get `private` so a shared cache can't serve one user
the response of another.

PATCH responses (both the no-op short-circuit and the post-write
path) now also carry the ETag, so a caller that uses the PATCH
response body can immediately revalidate against it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@chatgpt-codex-connector
Copy link
Copy Markdown

💡 Codex Review

etag = buildEtag(instanceEntry.indexedAt, CARD_JSON_ETAG_VARIANT);
if (etag && request.headers.get('if-none-match') === etag) {

P1 Badge Include all assembled response inputs in the ETag

When a card is revalidated after data that is assembled outside the primary instance row changes, this can return a stale 304. cardDocument() still mutates the response with live attachRealmInfo() data, and the existing regression test in packages/realm-server/tests/realm-endpoints-test.ts asserts that card responses reflect realm config changes without re-indexing the card; with the old ETag from before a PATCH /_config, this equality check short-circuits before rebuilding the document, so the client keeps the old meta.realmInfo. The same issue applies to foreign linked resources loaded at request time. The validator needs to account for those dynamic inputs, or avoid 304 short-circuiting when the assembled body can change without instanceEntry.indexedAt changing.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@habdelra habdelra changed the title CS-11010: ETag/304 for vnd.card+json across all realms CS-11010: ETag/304 for vnd.card+json May 6, 2026
@habdelra habdelra changed the title CS-11010: ETag/304 for vnd.card+json CS-11010: ETag/304 for application/vnd.card+json May 6, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

Host Test Results

    1 files  +    1      1 suites  +1   1h 59m 37s ⏱️ + 1h 59m 37s
2 573 tests +2 573  2 558 ✅ +2 558  15 💤 +15  0 ❌ ±0 
2 592 runs  +2 592  2 577 ✅ +2 577  15 💤 +15  0 ❌ ±0 

Results for commit a0a659a. ± Comparison against earlier commit dacb2c8.

Realm Server Test Results

    1 files  ±    0      1 suites  +1   17m 28s ⏱️ + 17m 28s
1 262 tests +1 262  1 262 ✅ +1 262  0 💤 ±0  0 ❌ ±0 
1 340 runs  +1 340  1 340 ✅ +1 340  0 💤 ±0  0 ❌ ±0 

Results for commit a0a659a. ± Comparison against earlier commit dacb2c8.

Codex flagged that `cardDocument()` adds `meta.realmInfo` at
request time via `attachRealmInfo()`, drawing from a cached
`getRealmInfo()` that nulls on `/_config` PATCH and on publish
without bumping any card's `indexed_at`. With the previous
`<indexed_at>:card` ETag, a client revalidating with the
pre-PATCH ETag would 304 onto a body whose `realmInfo.name`
(or `iconURL`, `lastPublishedAt`, etc.) had changed.

The fix folds an md5 hash of the cached realmInfo into the ETag
base: `<indexed_at>-<realmInfoHash>:card`. The hash is computed
lazily inside `getRealmInfo()` and tracked in a sibling
`#cachedRealmInfoHash` field that's nulled wherever
`#cachedRealmInfo` was — wrapped in a tiny
`invalidateCachedRealmInfo()` helper so the two always move
together.

The card+json handlers (GET, PATCH) prime the cache via
`getRealmInfo()` before computing the ETag, then read the hash
through `getCachedRealmInfoHash()`. A new `buildCardJsonEtag()`
helper centralizes the format. Existing
realm-endpoints-test "Cache-Control header for card json"
expectation updated to the new public/max-age=0/must-revalidate
contract; new regression test exercises the scenario Codex
described — old ETag must not 304 after a /_config name change.

Cross-realm linked resources are unaffected: `attachRealmInfo()`
only stamps THIS realm's info on resources whose
`meta.realmURL` matches this realm. Foreign resources' realmInfo
isn't injected here, and instance-level changes on those
resources still propagate via the dependency graph (which bumps
this card's `indexed_at`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@habdelra
Copy link
Copy Markdown
Contributor Author

habdelra commented May 6, 2026

@codex you're right — pushed 0ba0b0f which folds an md5 of the cached getRealmInfo() into the ETag base, so the format is now <indexed_at>-<realmInfoHash>:card.

Mechanics: getRealmInfo() populates a sibling #cachedRealmInfoHash whenever it (re)populates #cachedRealmInfo, and a tiny invalidateCachedRealmInfo() helper clears both fields atomically — replacing every #cachedRealmInfo = null site (fullIndex, the .realm.json file-watcher path, and patchRealmConfig) so the hash can never go out of sync with the cached info. The card+json handlers await this.getRealmInfo() up front to prime the cache before computing the ETag.

Added a regression test exactly for the case you called out: capture pre-PATCH ETag → PATCH /_config with a new name → re-GET with If-None-Match: <old etag> must return 200 with the new meta.realmInfo.name, not 304.

On the other half of your concern — cross-realm linked resources loaded at request time — I think that case is already covered, but checking my reasoning: attachRealmInfo() only stamps THIS realm's info on resources whose meta.realmURL matches this realm; it doesn't touch foreign-realm included[] entries, so foreign-realm _config changes don't mutate our response body. And instance-level changes on a foreign linked card propagate to this card's indexed_at via the deps graph (relationship URLs are extracted as deps in relationship-dependency-extractor, and the invalidation cascade bumps this row's indexed_at when any transitively-included row changes). If you spot a case that path doesn't cover I'd genuinely like to know.

@habdelra habdelra requested review from Copilot and removed request for Copilot May 6, 2026 17:45
@chatgpt-codex-connector
Copy link
Copy Markdown

Summary

Testing

  • git diff --check
  • pnpm --dir packages/runtime-common lint:js
  • pnpm --dir packages/realm-server lint:js
  • ⚠️ pnpm --dir packages/runtime-common lint — JS lint passed, but lint:types is blocked in this environment by missing local workspace type declarations for @cardstack/boxel-ui / @cardstack/boxel-icons referenced from packages/base.
  • ⚠️ pnpm --dir packages/realm-server lint — JS lint passed, but lint:types is blocked by the same missing local workspace type declarations plus pre-existing indexing-event-sink-test.ts DBAdapter.notify type errors.
  • ⚠️ ./tests/scripts/run-qunit-with-test-pg.sh --filter "cards with foreign relationship deps do not 304" tests/card-endpoints-test.ts — blocked because docker is not installed/available in the environment.

View task →

Codex follow-up to the previous commit. Cross-realm dep
invalidation is not currently wired up in
`index-writer.calculateInvalidations` — the SQL filters dependents
by `realm_url = $thisRealm` (its own comment flags this:
"probably need to reevaluate this condition when we get to cross
realm invalidation"). Practical consequence: when card X in realm
A `linksTo` card Y in realm B, a write to Y bumps B's
`indexed_at` for Y but never propagates to A's `indexed_at` for
X. Meanwhile `loadLinks` for X re-fetches Y over HTTP at every
request and surfaces Y's new content. Without a guard, A's
ETag-based 304 path would serve a body whose `included[]` is
stale.

Fix: detect foreign-realm deps on the primary instance row and
suppress ETag emission for that card entirely — both the 304
short-circuit and the response header in the GET 200, no-op
PATCH, and post-write PATCH paths. `SearchResultDoc` exposes the
deps array so the PATCH paths can run the same check without an
extra `instance()` query. The check ignores module-graph deps
(non-http URLs, registered prefixes, and `cardstack.com/base/`
paths) so platform code references — which every card has —
don't blanket-suppress the ETag. Only foreign card *instance*
URLs trigger the guard.

When cross-realm invalidation lands, this guard can be removed
and ETags will be correct for cards with foreign instance
deps too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@habdelra
Copy link
Copy Markdown
Contributor Author

habdelra commented May 6, 2026

Good catch — pushed c846c86. Confirmed the cross-realm dep gap by tracing index-writer.ts:1042-1044:

WHERE deps @> [...]
  AND i.realm_url = $thisRealm   -- "probably need to reevaluate this condition when we get to cross realm invalidation"

So a write to a foreign-realm card never bumps the consuming realm's indexed_at, while loadLinks HTTP-fetches the foreign card fresh on every request — which is exactly the mismatch you flagged.

Implementation: hasForeignRealmDeps(deps, this.url) walks the index entry's deps array and returns true if any entry is a foreign card-instance URL. The check explicitly ignores module-graph deps:

  • non-http(s):// strings (registered prefixes, relative paths)
  • https://cardstack.com/base/...

If left as-is those would mean every card has a "foreign" dep (every card adopts from card-api) and the guard would suppress ETags universally — defeating the optimization for the exact cards the parent ticket cares about. Only foreign card instance URLs trip the guard, and the ones in the deps array come from relationship URL extraction — that's the case where cross-realm invalidation actually leaves us holding a stale included[].

SearchResultDoc now also carries deps: string[] | null so the PATCH no-op + post-write paths can run the same guard without an extra instance() query. Both 304 short-circuit AND the response-header 200/PATCH paths now respect hasForeignDeps.

I didn't add the integration test you described in this commit — a multi-realm fixture is heavy enough that I'd rather get the guard reviewed first and add the test separately if reviewers want it. Happy to wire one through runTestRealmServerWithRealms if requested. The guard logic is small enough to read directly: realm.ts hasForeignRealmDeps + isForeignRealmDep helpers, ~30 lines.

Followup work flagged: when cross-realm dep cascading actually lands (the comment in index-writer.ts.calculateInvalidations is the marker), this guard can be removed.

@habdelra habdelra requested review from Copilot and removed request for Copilot May 6, 2026 18:16
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Comment thread packages/runtime-common/realm.ts Outdated
Multiple correctness and quality fixes across the ETag work:

- Resolve registered prefixes when classifying foreign deps. Production
  realms register every realm via addRealmMapping, so deps are stored
  in prefix form (@cardstack/foreign-realm/foo.json). The previous
  dep.startsWith('http') early-return classified those as not-foreign
  and the guard silently failed. New isForeignRealmDep resolves the
  dep first, then trips on .json instance deps that resolve outside
  this realm — including base-realm card instances. Module/scoped-CSS
  deps still pass.
- Invalidate the source realm's #cachedRealmInfo on publish/unpublish.
  The lastPublishedAt map in RealmInfo is built from realm_registry
  rows joined on source_url, so (un)publishing a derivative changes
  it without otherwise touching the source — without the hook, the
  source's card+json ETag would 304 against pre-publish validators
  forever.
- Pin Postgres row order in querySourceRealmPublications with
  ORDER BY url. Without it, JSON.stringify of Object.fromEntries(rows)
  is non-deterministic across instances/restarts and the realm-info
  hash flips for the same logical state — every browser cache
  invalidates on cache-prime races.
- Quote the ETag value ("<base>:card") per RFC 9110 §8.8.3 — CDNs
  and browsers don't re-quote inbound validators.
- Parse If-None-Match as a list with weak-prefix tolerance and *
  per RFC 9110 §13.1.2. Strict-equality matching missed common
  validators (CDN-injected W/, comma lists).
- Move the .json redirect to the top of getCard so the canonical URL
  is the only one that ever serves an ETag — keeps client/server
  cache keys aligned and prevents intermediaries from caching a 304
  bound to the .json form.
- Skip the instance() peek when the request has no If-None-Match.
  cardDocument() does its own instance lookup, so the peek was a
  redundant DB round-trip on the cache-miss path.
- Re-derive hasForeignDeps from the cardDocument() result for the
  response header path. The early peek's snapshot may differ from
  the assembly's snapshot — using the assembly's deps closes a
  write-race hole.
- Update the CARD_JSON_ETAG_VARIANT comment to describe both inputs
  (was outdated after the earlier realmInfo fix). Update the
  cardJsonCacheControl comment to acknowledge must-revalidate is
  added on top of the source/module pattern, not the same.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@habdelra
Copy link
Copy Markdown
Contributor Author

habdelra commented May 6, 2026

Pushed fd83351 addressing review findings from a fresh independent review I ran on the branch plus what's landed from @codex and Copilot. Summary of changes since c846c86:

Correctness

  • Foreign-dep classification was broken in production. dep.startsWith('http') early-returned for prefix-form deps like @cardstack/foreign-realm/foo.json, which is exactly how addRealmMapping (used everywhere in prod) stores them. Guard silently failed for the cards it was supposed to protect. New isForeignRealmDep resolves the prefix first, then trips on .json instance deps that resolve outside this realm. Module/scoped-CSS deps continue to pass.
  • Source realm's #cachedRealmInfo is now invalidated on publish/unpublish. RealmInfo.lastPublishedAt is built from realm_registry rows joined on source_url, so (un)publishing a derivative bumps it without otherwise touching the source. Without the hook, the source's card+json ETag would have 304'd against pre-publish validators forever.
  • querySourceRealmPublications now ORDER BY url. Without it, JSON.stringify of Object.fromEntries(rows) was non-deterministic across instances/restarts — every cache-prime race would have flipped the realm-info hash and invalidated every browser cache.

RFC compliance

  • ETag values are now quoted per RFC 9110 §8.8.3 ("<indexed_at>-<realmInfoHash>:card").
  • If-None-Match parsing now handles *, comma-separated lists, and weak (W/) validators (using the same matcher pattern as server.ts for the index-HTML route).

Race fix

  • The response-side hasForeignDeps flag is now derived from cardDocument()'s result, not from the early peek. Closes a window where a write between the peek and the assembly could emit an ETag tied to a snapshot whose deps were already foreign.

Layout

  • The .json 302-redirect now happens before any cache check, so the canonical URL is the only one that ever serves an ETag — intermediaries can't end up caching a 304 bound to the .json form.
  • The conditional-GET fast path's instance() peek now only runs when If-None-Match is present. On cache-miss requests we go straight to cardDocument() (which does its own instance lookup), saving the redundant round-trip @copilot called out.

Comments

  • CARD_JSON_ETAG_VARIANT now documents both inputs to the base.
  • cardJsonCacheControl now acknowledges must-revalidate is added on top of the source/module pattern, not the same.

Tests updated to match the now-quoted format. The earlier CI red on Realm Server Tests (2, 6) was the foreign-dep classifier marking base-realm module deps as foreign in the test environment (where base maps to localhost:4201, not cardstack.com/base/); the new .json-suffix discriminator should fix that.

Still open: integration test for cross-realm dep ETag-suppression. Happy to add via runTestRealmServerWithRealms if reviewers want it before merge — flagging now in case there are nicer existing fixtures I should reuse.

@chatgpt-codex-connector
Copy link
Copy Markdown

Summary

Checks

  • git status --short — working tree is clean.
  • git show --stat --oneline --decorate --no-renames HEAD — confirmed latest HEAD is fd833512 Address review findings from independent review + Codex + Copilot.
  • git diff --check HEAD~1..HEAD && pnpm --dir packages/runtime-common lint:js — diff whitespace check passed before lint started.
  • ⚠️ git diff --check HEAD~1..HEAD && pnpm --dir packages/runtime-common lint:js — lint was blocked by the local environment: node_modules is missing, causing ESLint 10 to fail to find the expected config/package setup.

View task →

@habdelra habdelra requested a review from a team May 6, 2026 18:32
Copy link
Copy Markdown
Contributor

@lukemelia lukemelia left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not crazy about quietly skipping etags when there is a cross realm dependency. It seems like it could lead to unexpectedly uneven performance experiences. I guess we can fix it when we add cross-realm invalidation?

@habdelra
Copy link
Copy Markdown
Contributor Author

habdelra commented May 6, 2026

Thanks for the review @lukemelia. Good call on the unevenness concern — I agree it's a real foot-gun: two cards in the same realm could have very different perf profiles (one 304s, the other re-assembles every time) and that surface area only widens as more realms link cross-realm. The suppression is meant as a correctness floor while we still have the realm-bounded calculateInvalidations (packages/runtime-common/index-writer.ts:1042-1044) — without it, foreign-card writes serve stale 304s. Removing the guard the moment cross-realm invalidation lands is the right plan.

Two ways I'd consider making it less quiet in the meantime, if it's worth a small follow-up:

  1. Surface it on the response — emit X-Boxel-Etag-Suppressed: foreign-deps on the card+json response when the guard fires. Doesn't change client behavior, but ops/Grafana can see the rate at which cards are being penalized and prioritize cross-realm invalidation work accordingly.
  2. Log at debug — fine-grained logger entry per request that hits the guard. Cheaper but only useful interactively.

Happy to do (1) as a tiny follow-up to this PR if you'd like — it's ~5 lines and gives us a measurable signal that drives the case for cross-realm invalidation. Or fold it into the cross-realm-invalidation work and remove both at the same time.

Card+json responses whose ETag is suppressed by the foreign-realm-
deps guard now carry `X-Boxel-Etag-Suppressed: foreign-deps`. The
header is informational — it doesn't change client behavior — but
it makes the suppression visible to ops dashboards and log
aggregation. Per @lukemelia's review concern about uneven perf
profiles being silently invisible: this gives us a measurable
signal we can plot in Grafana to drive prioritization of the
cross-realm dep invalidation work in
`index-writer.calculateInvalidations`. Once that lands, both the
suppression AND the header come out together.

Applied to all three response sites that participate in the guard:
PATCH no-op short-circuit, PATCH post-write, and GET 200. The 304
fast-path doesn't need it — when foreign deps fire there we never
emit an ETag in the first place, so we fall through to the 200
path which carries the signal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@habdelra
Copy link
Copy Markdown
Contributor Author

habdelra commented May 6, 2026

Done — pushed dacb2c8. Card+json responses whose ETag the foreign-deps guard suppresses now carry X-Boxel-Etag-Suppressed: foreign-deps. Doesn't change client behavior, but count by (status_code) (rate({app="realm-server"} | json | __X_Boxel_Etag_Suppressed__ = "foreign-deps")) (or equivalent at the response-header layer) will give us the rate the guard fires per realm, so it's measurable evidence for prioritizing cross-realm invalidation. Both the suppression and the header come out the moment that lands.

Applied to all three response sites that participate in the guard (PATCH no-op, PATCH post-write, GET 200) plus a regression assertion that the header is absent on a card with only local deps so dashboards can tell normal from suppressed traffic.

@habdelra habdelra merged commit d05ac79 into main May 6, 2026
66 of 67 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants