Skip to content

Enterprise Cache Refresh & Proxy Fingerprint Logs

The problem

Some users β€” notably at large institutions like utsa.edu β€” load a very old version of pdf.theaccessible.org even days after a deploy. This is almost always a corporate forward proxy / secure web gateway (SWG) on the user’s side (Blue Coat / Symantec, Squid, Zscaler, etc.) serving a stale HTML shell from its own cache and ignoring our Cache-Control: must-revalidate.

Our server-side headers are already correct (verified live):

ResponseCache-Control
HTML (/, /pricing/, …)public, max-age=0, must-revalidate + ETag
Hashed JS/CSS (/_next/static/*)public, max-age=31536000, immutable

Once a proxy serves an old HTML shell, it references old (but still-present) hashed chunks, so the whole app is stale. No response header can fully force a non-compliant proxy to revalidate β€” which is why the fixes below are layered.

What we deployed

1. Client-side version self-heal (the real cure)

  • The CF web worker (apps/web/worker/index.ts) serves GET /__version β€” dynamic, no-store, deployed atomically with the app β€” returning the live build number. Because the worker generates it (not a static file), a proxy that caches our origin can’t stale it.
  • VersionChecker (apps/web/src/components/VersionChecker.tsx) polls it on mount, on tab focus, and every 5 minutes, comparing to the build number baked into the running bundle (NEXT_PUBLIC_BUILD_NUMBER).
  • When the running bundle is older, it shows a β€œRefresh now” banner. We prompt rather than force-reload because the app has in-flight uploads and unsaved edits.
  • Loop guard: a per-build sessionStorage marker (tae:reloadedForBuild) detects when a refresh did not clear the staleness (the proxy re-served the old shell) and escalates the copy to a hard-refresh hint (Ctrl/⌘+Shift+R).

The build number reaches the worker via wrangler deploy --var (buildVersion.ts is gitignored, so it can’t be imported into the worker bundle). See apps/web/scripts/wrangler-version-args.mjs; the deploy npm script wires it in.

2. Legacy proxy headers

apps/web/public/_headers adds Pragma: no-cache + Expires: 0 to the HTML rules β€” HTTP/1.0 directives that older appliances honor even when they ignore must-revalidate. A /*/ rule covers trailing-slash directory routes (/pricing/, /admin/) that /*.html does not match. Extensioned assets never end in /, so the immutable hashed-asset rule is untouched.

3. Edge proxy fingerprint logging

For HTML document requests that carry a forward-proxy signature, the worker logs the network fingerprint to app_logs, so we can identify exactly what infrastructure a stale user sits behind.

Reading the fingerprint logs

  1. Open /admin/logs (admin.theaccessible.org β†’ Logs).

  2. Filter for proxy fingerprint (the log message) or component = browser. Each entry’s metadata has kind: "proxy-fingerprint".

  3. Key fields in metadata:

    FieldMeaning
    asOrg / asnCloudflare-resolved network. The biggest tell. University of Texas at San Antonio = on-campus egress; a Zscaler ASN (AS22616 / AS40384 / AS53813) = cloud SWG.
    cfIpClient IP as Cloudflare saw it. In UTSA’s 129.115.0.0/16 = on-campus; elsewhere = proxied off-site.
    viaOften literally names the appliance, e.g. 1.1 …(BlueCoat), 1.1 zscaler, squid/….
    xForwardedFor / forwardedProxy chain.
    blueCoat / zscalerVendor-specific headers when present.
    ifNoneMatchabsent on an HTML navigation is a smoking gun β€” a well-behaved cache echoes our ETag to revalidate; its absence means the proxy is serving from its own cache without checking us.
    coloCloudflare edge that served the request (geography sanity check).

Interpreting the result

  • asOrg/cfIp = UTSA, via names an appliance, ifNoneMatch: absent β†’ on-campus caching proxy serving stale HTML. The version self-heal + hard- refresh hint is the mitigation; there is no server-side fix.
  • asOrg = a Zscaler/cloud SWG ASN β†’ cloud gateway. These usually cache less aggressively; check whether they strip our headers.
  • Few or no UTSA entries despite stale-bundle reports β†’ the proxy is caching our origin so uniformly that even /__version requests don’t reach us. In that worst case the client self-heal can’t fire either, and the hard-refresh hint (or a proxy-side cache purge by their IT) is the only recourse.

Tuning

  • FINGERPRINT_SAMPLE (wrangler var, default "1" = log all matching requests) throttles volume if the logs get noisy. Set e.g. "0.2" to sample 20%, "0" to disable. Override at deploy with --var FINGERPRINT_SAMPLE:0.2 or edit wrangler.toml.
  • The ingest endpoint (/api/client-logs) rate-limits per source IP; since the worker’s egress is a small set of Cloudflare IPs, sustained high volume can be dropped collectively β€” another reason to sample if traffic is heavy.

Verifying a deploy

Terminal window
# Should return the just-deployed build number, no-store:
curl -si https://pdf.theaccessible.org/__version | grep -iE 'cache-control|pragma|buildNumber'
curl -s https://pdf.theaccessible.org/__version
# HTML should carry the legacy headers:
curl -sI https://pdf.theaccessible.org/pricing/ | grep -iE 'cache-control|pragma|expires'