Never serve the service worker from a cache #309

Closed
opened 2026-09-08 06:02:33 +00:00 by jcoffey-dev · 0 comments
Owner

The deploy an hour ago went out at the origin and did not arrive. Caught by checking both sides rather than assuming:

$ curl -sD- https://ihasmail.com/sw.js | grep -iE 'cf-cache-status|age|cache-control'
cache-control: public, max-age=14400
age: 760
cf-cache-status: HIT

$ curl -s https://ihasmail.com/sw.js | grep -c 'actionsFor'   # new code
0
$ ssh Web_Host 'curl -s http://127.0.0.1:8090/sw.js | grep -c actionsFor'
3

Cloudflare was handing out the previous sw.js, with an edge TTL of four hours — longer than the hour we asked for — because the file is neither a hashed asset nor HTML and so fell into the ordinary max-age=3600 case in static.ts.

Why this one is not a freshness preference

The service worker is the app's whole update mechanism. A browser holding the old one keeps being served the shell that worker knows and never learns a deploy happened, so the deploy does not land at all — silently, and for as long as the CDN says.

The manifest matters for a second reason: the two have to agree. A fresh manifest advertising a share target, answered by a worker that has never heard of one, sends the share to the server for a 405. Either being old is survivable; the two disagreeing is not, and that is exactly the state prod was in between the deploy and this.

The change

no-cache for sw.js and *.webmanifest, alongside index.html which already had it. Not no-store — both browser and CDN may still keep a copy, they just have to revalidate, which is a 304 and costs nothing. What it forbids is either answering from its own copy without asking.

Narrow on purpose: two files, named, rather than a policy that quietly stops the icons and fonts being cached too. Tests assert the hashed assets keep their immutable year and that an ordinary file keeps its hour, so the blast radius is pinned as well as the fix.

Verification

npm run typecheck, npm test (1150 web + 168 server, 7 new) and the build pass. The new tests cover both the root and a subpath mount, since a stale worker is exactly as bad under a prefix.

This does not fix the copy already at the edge. That needs a purge of https://ihasmail.com/sw.js before any phone test is meaningful — until then a phone installs the old worker and none of #306, #307 or #308 will appear to work.

Merged 2026-09-07 as coffey-labs/ihasmail@a389b9e8c5

Rebuilt from: git history, session transcript.

The deploy an hour ago went out at the origin and **did not arrive**. Caught by checking both sides rather than assuming: ``` $ curl -sD- https://ihasmail.com/sw.js | grep -iE 'cf-cache-status|age|cache-control' cache-control: public, max-age=14400 age: 760 cf-cache-status: HIT $ curl -s https://ihasmail.com/sw.js | grep -c 'actionsFor' # new code 0 $ ssh Web_Host 'curl -s http://127.0.0.1:8090/sw.js | grep -c actionsFor' 3 ``` Cloudflare was handing out the previous `sw.js`, with an edge TTL of four hours — longer than the hour we asked for — because the file is neither a hashed asset nor HTML and so fell into the ordinary `max-age=3600` case in `static.ts`. ### Why this one is not a freshness preference The service worker is the app's whole update mechanism. A browser holding the old one keeps being served the shell that worker knows and never learns a deploy happened, so the deploy does not land at all — silently, and for as long as the CDN says. The manifest matters for a second reason: **the two have to agree**. A fresh manifest advertising a share target, answered by a worker that has never heard of one, sends the share to the server for a 405. Either being old is survivable; the two disagreeing is not, and that is exactly the state prod was in between the deploy and this. ### The change `no-cache` for `sw.js` and `*.webmanifest`, alongside `index.html` which already had it. Not `no-store` — both browser and CDN may still keep a copy, they just have to revalidate, which is a 304 and costs nothing. What it forbids is either answering from its own copy without asking. Narrow on purpose: two files, named, rather than a policy that quietly stops the icons and fonts being cached too. Tests assert the hashed assets keep their immutable year and that an ordinary file keeps its hour, so the blast radius is pinned as well as the fix. ### Verification `npm run typecheck`, `npm test` (1150 web + 168 server, 7 new) and the build pass. The new tests cover both the root and a subpath mount, since a stale worker is exactly as bad under a prefix. **This does not fix the copy already at the edge.** That needs a purge of `https://ihasmail.com/sw.js` before any phone test is meaningful — until then a phone installs the old worker and none of #306, #307 or #308 will appear to work. **Merged** 2026-09-07 as coffey-labs/ihasmail@a389b9e8c51c <sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.