Skip to content

Fix Service Worker downloads 404ing in Firefox (small files) - #7

Merged
polius merged 2 commits into
mainfrom
fix/sw-download-race
Sep 19, 2026
Merged

polius merged 2 commits into
mainfrom
fix/sw-download-race

Conversation

@polius

@polius polius commented Sep 19, 2026

Copy link
Copy Markdown
Owner

Summary

Small-file downloads via the Service Worker sink returned 404 (Transfer not found or expired.) in Firefox, while the UI still reported success. Root cause is a race between the page and the SW, ported unchanged from FileSync.

Root cause

deliver() registers the transfer, navigates a hidden iframe to /__download/{id}, then drains the staged OPFS file through the MessageChannel and finishes with { type: 'end' } — which sw.js handled by transfers.delete(id) immediately.

In Firefox, for small files, the OPFS drain (milliseconds) beats the iframe navigation's dispatch: the fetch event arrives after end deleted the entry, so the SW's own handler answered 404. Chromium dispatches faster, so the e2e suite (Chromium-only) never caught it. Verified experimentally: 256 KB → 404, 64 MB → 200 in real Firefox; same latent code exists in FileSync's web/sw.js (large transfers mask it there).

Changes

  • src/sw.js: keep the transfer entry after end — the stream is closed but fully buffered, so a late-arriving fetch still serves the complete file. Added a served flag so a transfer can't be served twice (previously re-serving a locked stream would throw); the 5-minute GC still reclaims old entries.
  • src/js/sink.js: register the worker with updateViaCache: 'none' so SW update checks are never served from the HTTP cache — without this, a fixed worker can stay invisible across deploys/containers (observed live during testing: the old worker persisted even after the container was replaced).
  • test/fake-sinks.mjs: makeSwBridge({ fetchAfterEnd }) dispatches the simulated iframe fetch after end is processed, reproducing Firefox's ordering deterministically in Node.
  • test/sinks.test.mjs: regression round-trip using that mode (fails against the old sw.js, passes now).

Verification

  • npm test: 49/49 pass (new regression test fails on old code — checked via stash)
  • Real headless Firefox: 256 KB encrypt → download 200 (was 404), UI success
  • Full Chromium e2e against the rebuilt image: sw / fs / blob all SHA-256 verified
  • Manually verified in Firefox against the fixed container (reporter-confirmed)

…404ed

In Firefox the page can finish streaming a small staged file (port 'end')
before the iframe navigation reaches the Service Worker fetch handler,
which then answered 404 'Transfer not found or expired.' because the
entry had just been deleted. Chromium wins that race, so e2e never saw it.

- sw.js: keep the transfer entry after 'end' (stream is closed but fully
  buffered, so a late fetch still serves the complete file); guard
  re-serving with a 'served' flag; GC still reclaims old entries.
- sink.js: register the worker with updateViaCache:'none' so SW update
  checks are never served from the HTTP cache (stale worker persisted
  across deploys/containers otherwise).
- tests: makeSwBridge({ fetchAfterEnd }) reproduces Firefox's ordering;
  new regression round-trip fails on the old sw.js, plus full suite.
@polius polius added the enhancement New feature or request label Sep 19, 2026
@polius
polius merged commit 272f007 into main Sep 19, 2026
3 checks passed
@polius
polius deleted the fix/sw-download-race branch September 19, 2026 11:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant