Skip to content

Inconsistent Vary/Cache-Control across conneg variants causes stale-render race on reload #315

Description

@melvincarvalho

Symptom

Loading https://<pod>/profile/card.jsonld#me in a browser sometimes renders the mashlib data-browser view (expected), and sometimes renders raw RDF text (Turtle or JSON-LD) depending on cache state. A hard refresh always produces the mashlib view; a soft refresh can produce the raw text.

Root cause

Not a mashlib/webpack race — it's a JSS response-header inconsistency.

Variant (by Accept) Vary Cache-Control
text/html (mashlib HTML wrapper) Accept no-store
text/turtle Accept, Origin (missing)
application/ld+json Accept, Origin (missing)

Two problems:

  1. Vary mismatch across variants — some browser caches (observed in Brave/Chromium) get confused when variants of the same URL carry different Vary sets, and can serve the wrong variant on a matching request.
  2. Data variants have no Cache-Control — mashlib does an XHR for Turtle to populate the view; that response gets cached aggressively. On a soft refresh, the browser can serve the cached Turtle entry against the top-level HTML navigation.

Hard refresh sends Cache-Control: no-cache which bypasses the browser cache entirely → always goes upstream → gets the HTML wrapper → mashlib loads → view renders.

Security angle (why the fix matters beyond UX)

Solid responses vary with auth (WAC). Without client-side revalidation, a cached response from one auth state can leak into another (logged-in → logged-out body, or vice versa). The fix below addresses this in addition to the rendering race.

Proposed fix

  • Emit consistent Vary across all variants. Vary: Accept, Origin on every response (safer, covers CORS).
  • Emit Cache-Control: private, no-cache, must-revalidate on data variants (Turtle/JSON-LD/N3). Combined with the ETag we already send, this lets clients keep the body but always revalidate via If-None-Match → cheap 304s, no stale bodies, no auth-state leakage.
  • Keep Cache-Control: no-store on the HTML wrapper (it's a dynamic bootstrap template; no caching benefit).

Reproduction

for accept in "text/html,*/*;q=0.8" "text/turtle" "application/ld+json"; do
  echo "=== $accept ==="
  curl -s -D - -o /dev/null "https://<pod>/profile/card.jsonld" -H "Accept: $accept" \\
    | grep -Ei "vary|cache-control|content-type"
done

Shows the Vary/Cache-Control mismatch documented above.

Metadata

Metadata

Assignees

No one assigned

    Labels

    cachingHTTP caching, ETags, Cache-Control

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions