Skip to content

[BUGFIX] PWA service worker update handling - #453

Open
maurerle wants to merge 4 commits into
mainfrom
fix/pwa-service-worker-updates
Open

[BUGFIX] PWA service worker update handling#453
maurerle wants to merge 4 commits into
mainfrom
fix/pwa-service-worker-updates

Conversation

@maurerle

Copy link
Copy Markdown
Member

Description

This PR resolves several lingering issues with the PWA and service worker implementation following the Vite migration, primarily ensuring that users receive app updates reliably and experience correct offline behaviors.

The four fixes include:

  • Reliable Service Worker Updates: Changed the Service Worker registration to autoUpdate. Previously, users had to close all tabs of the origin to get a new deployment. Now, the app
    will automatically update and reload when a new version is activated.
  • Fixed Navigation Fallbacks: Restricted the SPA navigation fallback so it no longer erroneously intercepts paths with file extensions (like /sw.js), ensuring static assets are
    served correctly.
  • Modernized Offline Handling: Dropped the obsolete offline.html (a remnant of the 2018 network-only worker) and updated the app to properly catch fetch rejections. When offline, the
    app shell now gracefully displays a "No connection available" message instead of hanging on the loading screen.
  • Correct Nginx Cache Headers: Replaced the stock Nginx configuration to send proper Cache-Control headers. Entry points (index.html and sw.js) are now always revalidated to prevent
    stale caches from blocking app updates, while hashed assets/ are cached aggressively for performance.

Motivation and Context

The service worker was not updating correctly after deploying the new version and did show the old version after deployment of a new version on the server.

Furthermore links like https://map.aachen.freifunk.net/sw.js do show a map loaded from the service worker instead of the service worker js - if the PWA is cached. This should not be the case.

How Has This Been Tested?

  • Offline Handling: Tested using headless Chromium with the network disabled. Verified that the app now successfully catches the failed fetch and gracefully displays the "No
    connection available." UI, whereas previously it would hang indefinitely on the loading screen and log unhandled Failed to fetch errors.
  • Service Worker Updates: Tested against a real build with a scripted deployment (simulating an upgrade from version 13.2.0 to 13.2.1) using headless Chromium. Verified that the new
    bundle now appears on the very first page reload. Previously, the new bundle would only appear if all tabs to the origin were completely closed.

Screenshots/links:

Checklist:

  • My code follows the code style of this project. (CI will test it anyway and also needs approval)
  • My change requires a change to the documentation.
    • I have updated the documentation accordingly.

registerType defaulted to "prompt", which emits a worker that only calls
skipWaiting() on a SKIP_WAITING message, and nothing in the app ever sent
one. Every deployment installed a new worker that then sat in "waiting"
indefinitely, so users kept being served the previous precache until they
closed every tab of the origin.

Switching to "autoUpdate" alone is not enough: with injectRegister "auto"
and no virtual:pwa-register import, only a bare register() call is emitted
and the already rendered page is never reloaded, delaying the update by one
reload. Importing registerSW pulls in the register template that reloads on
activation. This requires the plugin to stay loaded even for the PR preview
build, hence disable: instead of dropping it from the plugin list.

Measured against a real build with a scripted deployment (13.2.0 -> 13.2.1)
in headless Chromium: before, the new bundle appeared only after closing all
tabs; with autoUpdate alone on the second reload; with the import on the
first. devOptions is switched off because autoUpdate would hard-reload the
dev server on every worker regeneration, discarding HMR state.

Assisted by (claude-5): Claude Opus 5
navigateFallbackDenylist was written as new RegExp(".*\.json"), where the
escape is dropped by the string literal, yielding an unanchored /.*.json/.
More importantly it covered only JSON, so every other non-precached path hit
the index.html fallback. sw.js is the one asset a worker never precaches, so
navigating to /sw.js returned the full application as text/html.

Denying anything that ends in a file extension fixes that while leaving the
SPA fallback intact for extension-less routes. Workbox matches the denylist
against pathname + search, so the pattern allows a trailing query and keeps
covering URLs like /config.json?v=2, which the old unanchored pattern did.

Assisted by (claude-5): Claude Opus 5
offline.html dates back to the 2018 network-only worker, which fetched every
request and fell back to that page on failure. The Vite migration replaced
that strategy with Workbox precaching, and nothing has referenced the page
since: it was built as an entry point and precached, but no navigateFallback,
no catch handler and no link pointed at it, so it could never be reached.

Precaching also introduced the gap it was meant to cover. Offline the shell
now loads from the precache and only config.json fails, and because fetch
rejects rather than returning a non-ok response, that rejection went
unhandled and the loading text stayed on screen forever. Verified in headless
Chromium with the network disabled: before, the page kept showing "Lade Karten
& Knoten..." and logged "Failed to fetch"; now it shows "No connection
available." with the existing reload button and no unhandled error.

Assisted by (claude-5): Claude Opus 5
The image ran on the stock nginx config, which sends no Cache-Control at all,
leaving browsers to cache the entry points heuristically from Last-Modified.
That works against the service worker update path: a stale index.html or
sw.js delays the update the previous commits enable.

Revalidate everything by default and cache only assets/ aggressively, which
is safe because Vite puts a content hash in those filenames.

Assisted by (claude-5): Claude Opus 5
github-actions Bot pushed a commit that referenced this pull request Aug 13, 2026
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Preview deployed: https://freifunk.github.io/meshviewer/pr-preview/pr-453/

Updated for commit 8e705db.

@maurerle
maurerle force-pushed the fix/pwa-service-worker-updates branch from 870f489 to 8e705db Compare August 13, 2026 07:58
github-actions Bot pushed a commit that referenced this pull request Aug 13, 2026
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.

1 participant