Skip to content

fix: bound the service worker's navigation fetch with a timeout - #298

Merged
DuarteSantos8 merged 1 commit into
DuarteSantos8:mainfrom
bluzername:fix-sw-offline-cold-launch-timeout
Sep 28, 2026
Merged

DuarteSantos8 merged 1 commit into
DuarteSantos8:mainfrom
bluzername:fix-sw-offline-cold-launch-timeout

Conversation

@bluzername

Copy link
Copy Markdown
Contributor

Fixes #274.

What was wrong

On iOS, launching the installed PWA from the Home Screen with no network
showed a black screen, then a blank white page, forever. The reporter
traced it exactly: sw.js's network-first fetch handler calls
fetch(e.request) with no timeout. A dead radio does not reject that
fetch quickly - it just never settles - so the .catch() a few lines
down, which falls back to the cached app shell, never gets a turn to run.
The app is stuck waiting on a request that will never finish.

The fix

Same one the reporter already tested and confirmed works for them:
fetch(e.request, { signal: AbortSignal.timeout(3000) }). After 3
seconds the fetch aborts, the existing .catch() fallback runs, and the
cached shell (or the matching cached asset) is served instead.

Test

sw.js lives in frontend/public/ and isn't itself under src/lib, so
there is no existing unit-test convention for it - and this isn't
something clicking through the app can reliably reproduce (it needs an
installed PWA and a truly dead network, not just devtools' "Offline"
checkbox, which rejects fetches immediately instead of hanging). Rather
than skip a test, I added
frontend/src/lib/sw-offline-launch.test.js, which loads the actual
production public/sw.js file into a sandboxed vm context (real
self/caches/fetch/AbortSignal stand-ins) and drives its real
fetch event listener directly:

  • gives it a fetch that never resolves on its own (simulating a dead
    radio, not a quick rejection),
  • asserts a timeout is actually wired to the request (this is what's
    missing on unmodified code - the test fails there with "expected 0 to
    be greater than 0" because AbortSignal.timeout is never called),
  • asserts the response does not resolve on its own within a few
    microtask turns,
  • fires the abort and asserts the response then resolves from the cache
    instead of hanging.

I confirmed this fails against unmodified sw.js (stashed the fix,
reran, got the RED failure above, restored the fix) and passes with it.

Verification

  • cd frontend && npx vitest run src/lib/sw-offline-launch.test.js -
    1/1 pass.
  • cd frontend && npx vitest run - full suite, 1573 tests (1 new), all
    pass (a few files hit a vitest worker-pool timeout under sandboxed
    parallelism and pass cleanly alone - unrelated to this change).

No new dependency, one line of production code changed
(frontend/public/sw.js), plus a new test file.

This account (bluzername) is not a collaborator on this repo, so I can't
merge - opening this for a maintainer to review.

🤖 Generated with Claude Code

On iOS, a cold-launch of the installed PWA with no network stayed on a
black/white screen forever instead of falling back to the cached app
shell. A dead radio does not reject fetch() quickly - it just never
settles - so the .catch() that reads the shell from cache never got a
chance to run.

Bound the network-first fetch with AbortSignal.timeout(3000), same as
the reporter tested and confirmed fixes it for them in DuarteSantos8#274.

Added a test that loads the real public/sw.js into a sandbox and drives
its fetch listener with a fetch that never resolves on its own: it
checks a timeout is actually wired to the request, that the response
does not settle early, and that aborting it recovers from cache instead
of hanging.

Fixes DuarteSantos8#274.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@DuarteSantos8
DuarteSantos8 merged commit 9168b9d into DuarteSantos8:main Sep 28, 2026
DuarteSantos8 added a commit that referenced this pull request Sep 28, 2026
…ff mid-download, and iOS 15 keeps its offline start (#274)

The 3 s abort from #298 also fired on responses whose headers had arrived, erroring the body
the page was still reading, and AbortSignal.timeout does not exist before iOS 16. A request the
network has not answered within 3 s is now answered from the cache when the cache has it;
nothing is aborted, and a request the cache cannot answer waits for the network.
DuarteSantos8 added a commit that referenced this pull request Sep 28, 2026
@DuarteSantos8

Copy link
Copy Markdown
Owner

Thanks @bluzername, this fixes #274. I reworked it before merging: a 3 s timeout on every GET kept running while the body streamed, so on a slow link the 1.6 MB main bundle would have been cut mid-download. The timeout now applies only to navigations and is cleared once headers arrive, and a chunk or language pack is never cut off. iOS 15 keeps its offline start.

Released in v1.3.9: https://github.com/DuarteSantos8/openGym/releases/tag/v1.3.9

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.

[IOS] PWA shows a blank page when launched offline

2 participants