Skip to content

fix(#438): stop deleting the image cache on every deploy, and give the image handler a failure path - #440

Merged
TortoiseWolfe merged 1 commit into
mainfrom
fix/438-sw-image-cache
Jul 29, 2026
Merged

fix(#438): stop deleting the image cache on every deploy, and give the image handler a failure path#440
TortoiseWolfe merged 1 commit into
mainfrom
fix/438-sw-image-cache

Conversation

@TortoiseWolfe

Copy link
Copy Markdown
Owner

Reported from the live blog: four below-the-fold thumbnails rendering as broken-image icons with their alt text showing, while the already-painted cards above them were fine.

Root cause — three things in public/sw.js combining

install  -> skipWaiting(), so a new worker activates without waiting for tabs
activate -> deletes every cache not matching the current CACHE_VERSION, which is
            stamped with the commit SHA — so IMAGE_CACHE went with it
activate -> clients.claim(), seizing the page that is already open

The open page had its image cache deleted mid-session and was then claimed by the new worker. Its not-yet-requested lazy images all faulted into an empty cache at once — and the image handler was the only content handler with no failure path:

handler .catch
api / network yes
image no
navigate yes, plus offline fallback

A rejected fetch therefore rejected the promise handed to respondWith, which the browser paints as a network error. Nothing is cached on failure (only 200s are), so there was no self-repair. Top of page fine, bottom broken.

Triggered for real users by the #437 deploy (12c2699).

Changes

  • IMAGE_CACHE is now 'scripthammer-images-v1', independent of CACHE_VERSION. Entries are keyed by request URL, so a code deploy has no reason to discard them. Keeps the scripthammer- prefix that activate's purge depends on, and scripts/stamp-sw-version.mjs only targets const CACHE_VERSION = '…'; so it will not rewrite this one — verified.
  • The image handler gets .catch(() => caches.match(request)), matching the api handler. Re-checking the cache is not redundant with the first lookup: a sibling request may have populated it in between, which is exactly what happens when a claimed page faults in many images at once.

Why the existing checks missed it, and how this one can fail

A fresh browser cannot reproduce this bug. With no previous worker there is no takeover, no deletion, and no empty-cache window. Loading the live site in a clean profile and scrolling every lazy image in reports 14 of 14 loaded, 0 broken — which is what made my first pass conclude, wrongly, that nothing was wrong. Any test running one version against a healthy network passes straight through it.

So tests/unit/sw-images.test.ts asserts the two properties that actually failed — the handler must not reject when the network does, and the image cache must not be keyed to the build. It runs the real public/sw.js through the same new Function harness as sw-navigate.test.ts; a reimplementation would drift and prove nothing.

Verified by reverting sw.js to HEAD and re-running: 4 of 7 fail on the old code (both behaviour guards and both naming guards), while the 3 that pass either way stay green.

Not fixed here

#439 — the "Building a Centralized Admin Dashboard" thumbnail is a genuinely blank image: a valid PNG that is 89.1% pure white with 5 distinct colours across 10,557 samples. That is a content decision (replace the capture, or drop featuredImage so the card renders text-only as low-seo does) and is left to the owner.

Closes #438.

Returning visitors lost blog images whenever a deploy landed: below-the-fold
thumbnails rendered as broken-image icons with alt text showing, while the
already-painted cards above them were fine.

Three things combined.

  install  -> skipWaiting(), so a new worker activates without waiting
  activate -> deletes every cache not matching the current CACHE_VERSION,
              which is stamped with the commit SHA, so IMAGE_CACHE went too
  activate -> clients.claim(), seizing the page that is already open

The open page therefore had its image cache deleted mid-session and was then
claimed by the new worker. Its not-yet-requested lazy images all faulted into
an empty cache at once — and the image handler was the ONLY content handler
with no failure path, so a rejected fetch became a permanently broken image.
Nothing is cached on failure (only 200s are), so there was no self-repair.

Two changes:

- IMAGE_CACHE is now 'scripthammer-images-v1', independent of CACHE_VERSION.
  Entries are keyed by request URL, so a code deploy has no reason to discard
  them. Keeps the scripthammer- prefix, which activate's purge depends on, and
  the stamp script's regex only targets CACHE_VERSION so it will not rewrite
  this one.
- The image handler gets `.catch(() => caches.match(request))`, matching the
  api handler. Re-checking the cache is not redundant with the first lookup: a
  sibling request may have populated it in between, which is precisely what
  happens when a claimed page faults in many images at once.

tests/unit/sw-images.test.ts runs the REAL worker through the same harness as
sw-navigate.test.ts. Verified by reverting sw.js to HEAD and re-running:
4 of 7 fail on the old code (both behaviour guards and both naming guards),
while the 3 that pass either way stay green. A fresh browser cannot reproduce
this bug at all — a clean-profile check of the live site reported 14 of 14
images loaded — so a test against one version and a healthy network would have
proved nothing.

Closes #438.
@TortoiseWolfe
TortoiseWolfe merged commit 509c8fc into main Jul 29, 2026
18 checks passed
@TortoiseWolfe
TortoiseWolfe deleted the fix/438-sw-image-cache branch July 29, 2026 17:20
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.

Returning visitors lose blog images on every deploy: the SW deletes the image cache, claims the open page, and the image handler has no failure path

2 participants