bit Bswup

Preparing the app for offline use, please wait...

0%

Troubleshooting & FAQ

Service worker problems are notoriously hard to read: the symptom shows up in the app, but the cause lives in a background thread with its own lifecycle. This page maps the symptoms people actually report back to their causes.

Diagnose it first

Four steps that identify most problems before you change a single setting:

  1. Turn the logs up. Set log="verbose" on the script tag and self.enableDiagnostics = true in service-worker.js. The page-side handshake and the worker then narrate themselves to the console.
  2. Look at the registration. Dev tools → Application → Service Workers shows the scope, which worker is active, and whether one is stuck waiting. Dev tools → Application → Cache Storage shows the buckets and their contents.
  3. Check the version. BitBswup.version in the console reports the Bswup build the page is running.
  4. Watch the events. The Live Playground on this site does all of the above with buttons - and your app can do the same in a few lines by chaining a handler.

Install and startup

The app never appears - the splash sits there forever

Cause. Almost always a silent install failure. The most common is the browser terminating the service worker mid-install (Chromium caps installs at roughly five minutes), which reports nothing at all: no error, no lifecycle event.

Fix. Nothing, in current versions - this is what stallTimeout exists for. After 60 seconds of total worker silence on a first install, Bswup gives up waiting and starts Blazor directly from the network. If you are seeing a permanent splash, check that you have not set stallTimeout="0", and look for a handler of your own that throws before revealing the app.

The progress bar freezes partway through

Cause. A large asset is genuinely still downloading, or an asset is failing repeatedly. Progress only advances per completed asset, so one slow 30 MB .wasm looks identical to a stall.

Fix. Check the Network tab first. If assets really are failing, they are reported through the error event - under the default lax tolerance with fatal: false, which means the install continues and the asset is fetched from the network on first use. Transient failures (rejected fetches, 408/429/5xx) are already retried with backoff; tune with maxRetries and retryDelay.

Nothing installs at all - no worker, no caches

Causes, in order of likelihood:

  • Not HTTPS. Service workers only register over HTTPS or localhost.
  • Missing manifest. Without <ServiceWorkerAssetsManifest> in the WebAssembly project, service-worker-assets.js is never generated and the worker has nothing to cache.
  • Only one worker file edited. service-worker.js is used in development; service-worker.published.js is what published builds ship. See below.
  • Scope rejected. A scope above the worker script's own folder needs a Service-Worker-Allowed header. Bswup retries with the default scope and logs a warning rather than losing the worker entirely, so look for that warning.

It works locally but not when published

Cause. The two service worker files diverged. wwwroot/service-worker.js is used during development, and wwwroot/service-worker.published.js is what a published build actually deploys, through the ServiceWorker item's PublishedContent mapping.

Fix. Keep the same content in both, and confirm the item is in the project file:

xml
<PropertyGroup>
    <ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
</PropertyGroup>

<ItemGroup>
    <ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js" />
</ItemGroup>

Updates

Users keep running an old version

Cause. Either the browser is not checking, or it is being served a cached worker script. By default a browser re-checks the worker only on navigation and roughly every 24 hours, so a long-lived SPA tab can run a stale build for a very long time.

Fix. Both halves:

html
<script src="_content/Bit.Bswup/bit-bswup.js"
        updateInterval="3600"
        updateOnVisibility="true"></script>

Then make sure your host sends Cache-Control: no-cache for service-worker.js and bit-bswup.sw.js - see the deployment checklist. Bswup already registers with updateViaCache: 'none', but support is uneven on older Safari/iOS and proxies are not bound by it.

An update downloads but the reload button never appears

Cause. Usually custom splash markup written before v-10-6-0. The built-in handling no longer reveals the overlay for updates - a background update must not paint over the running app - so a reload button left inside #bit-bswup can never become visible.

Fix. Move <button id="bit-bswup-reload"> outside the overlay and give it its own z-index. The BswupProgress component already does this for you, including with custom ChildContent. See the migration guide.

Publishing a new build raises no update

Cause. The browser compares the worker script byte-for-byte. If service-worker.js and everything it imports are unchanged, there is no new worker - even though your app's assets changed.

Fix. The default setup already handles this: service-worker-assets.js is part of the import chain and its contents change whenever an asset hash changes. If you have moved the manifest out of that chain, stamp cacheVersion from your build instead.

Every publish re-downloads the entire app

Cause. The cache bucket name rotated. It tracks Blazor's asset-manifest hash, so any asset hash change rotates it - including the noisy rebuild hashes .NET produces locally.

Fix. This is correct behavior for a real publish. For development, pin cacheVersion to a constant; for CI, stamp it with a value that bumps once per release - see Stamp the cache version from your build.

Offline

Going offline and reloading shows a network error

Cause. The SPA fallback has nothing to serve. Bswup announces this at startup:

text
BitBswup SW: defaultUrl ('/') matches no asset - navigations will NOT be served
from cache and the app will not work offline.

Fix. defaultUrl must name an entry that really exists in service-worker-assets.js or externalAssets. For a standalone WebAssembly app that is index.html (the default). For a Blazor Web App or _Host.cshtml setup the shell is the root URL, which is not in the manifest, so add it:

javascript
self.defaultUrl = '/';
self.externalAssets = [{ "url": "/" }];   // the shell is not in service-worker-assets.js

Cause. Usually one of two things. Either the cached shell is a prerendered copy of the home page - so every offline deep link flashes home content before the router corrects it - or your host redirects the shell URL (//index.html, common on Cloudflare Pages and Netlify) and the browser rejects a redirected response for a navigation.

Fix. The redirect case is handled automatically - Bswup rebuilds the response before serving it to a navigation. For the prerendering case, set noPrerenderQuery and honor it in your host document so the copy the worker caches is route-agnostic. This site does exactly that; view the source of Server/Components/App.razor in the repository for the pattern.

Cached audio or video will not play in Safari

Cause and fix. Safari refuses media served as a 200 in response to a ranged request. Since v-10-6-0 Bswup answers requests carrying a Range header with a real 206 Partial Content sliced from the cached body, so upgrading fixes it. Media must be declared as an asset (it is in the manifest, or added via externalAssets) for the slice to be possible.

The app stops working offline after a while

Cause. The browser reclaimed the cache. Everything Bswup stores lives in best-effort storage by default: browsers evict it under disk pressure, and Safari deletes all storage for a site not interacted with for seven days.

Fix. Request persistent storage - ideally from a user gesture, where grant odds are best. See Ask for durable storage at the right moment. An install-infra error means the install died before or while touching CacheStorage, which is usually quota exhaustion or a restricted private-browsing mode; it is always fatal, and Bswup boots the app from the network instead.

Frequently asked

Does Bswup work with Blazor Server or Blazor Web Apps?

Bswup targets Blazor WebAssembly - it caches a client-side app so it can boot without the network, which is only meaningful when the app runs in the browser. That includes a Blazor Web App using the Interactive WebAssembly render mode (this documentation site is exactly that: server-prerendered, then interactive WebAssembly). A pure Blazor Server app keeps a live circuit to the server and has nothing to boot offline.

Do I have to write a handler function?

No. Use the built-in progress UI and you write none. And if no handler is ever registered at all, a first install still completes - Bswup drives the finish handshake itself so the app boots; updates are simply left staged until the next full restart.

Can I use my own UI instead of the built-in splash?

Three levels, in increasing order of control: pass ChildContent to BswupProgress to replace the markup while keeping the behavior; pass the Handler parameter to layer your own logic after the built-in handling (see Replace the reload button with your own banner); or skip the component entirely and write a handler function that drives whatever markup you like.

What happens in a browser with no service worker support, or in private mode?

The app boots normally, straight from the network, with no offline support. Bswup starts Blazor itself rather than waiting for a worker that will never arrive. The same applies on the very first visit before anything is installed, and on a hard reload, which bypasses the worker by design.

Does it interfere with my API calls or other caches?

No. The worker only takes over requests it manages - assets from the manifest, your declared externalAssets, and SPA navigations. Everything else is left to the browser exactly as if no worker were installed. Cache buckets your app owns are likewise left alone, including by forceRefresh()'s default filter, since they can hold data with no other copy.

How do I remove Bswup from an app that is already deployed?

Swap the worker's contents for the self-destructing cleanup worker - it purges the caches, unregisters itself, and detaches every client. See Backing Out of Bswup. To reset a single broken client without changing the deployment, use BitBswup.forceRefresh().

How is this different from the service worker in the standard Blazor template?

The template gives you a worker that precaches silently and, on an update, waits for every tab to close before the new version takes effect - with no progress, no prompt, and no recovery path when an install fails. Bswup replaces that with a visible install, an update the user accepts explicitly and that stays consistent across tabs, retries and error tolerance for flaky networks, a watchdog so a failed install can never freeze the app, and a small API to drive all of it. There is a side-by-side comparison on the home page, and How It Works covers the mechanics.

Still stuck?
Open an issue on GitHub with your service-worker.js, your script tag, and the console output at log="verbose" with enableDiagnostics on - those three almost always contain the answer.