Service Worker (sw.js) should always return offline.html document if there is no network connection

I’m having an issue with a service worker that’s working partially. The manifest defines the start_url correctly (https://example.com/start.html) for users that add the website to the Homescreen, and both the start.html and offline.html are cached correctly as well, and both are available while the browser has no internet connection.

If the user gets offline (no network connection), the service worker successfully serves both https://example.com/start.html and https://example.com/offline.html — but if the user tries opening anything else (e.g. https://example.com/something.html) the browser throws a "site can’t be reached" error message.

What I actually need, is that, if there is no network connection, the service worker always returns the offline.html cached document, no matter which url the user is trying to reach.

In other words, the problem is that the Service Worker is not properly serving offline.html for the user’s requests when there’s no network connection (whatever solution is found, it also needs to cache the start.html for the manifest’s start_url).

This is my current code:

manifest.json

{
    "name": "My Basic Example",
    "short_name": "Example",
    "icons": [
        {
            "src": "https://example.com/static/ico/manifest-192x192.png",
            "sizes": "192x192",
            "type": "image/png"
        },
        {
            "src": "https://example.com/static/ico/manifest-512x512.png",
            "sizes": "512x512",
            "type": "image/png",
            "purpose": "any maskable"
        }
    ],
    "start_url": "https://example.com/start.html",
    "scope": "/",
    "display": "standalone",
    "orientation": "portrait",
    "background_color": "#2196f3",
    "theme_color": "#2196f3"
}

core.js

if('serviceWorker' in navigator) {
    navigator.serviceWorker.register('sw.js', {
        scope: '/'
    }).then(function(registration) {
    }).catch(function(err) {
    });
    navigator.serviceWorker.ready.then(function(registration) {
    });
}

sw.js

const PRECACHE = 'cache-v1';
const RUNTIME = 'runtime';
const PRECACHE_URLS = [
    '/offline.html',
    '/start.html'
];
self.addEventListener('install', event => {
    event.waitUntil(
        caches.open(PRECACHE)
        .then(cache => cache.addAll(PRECACHE_URLS))
        .then(self.skipWaiting())
    );
});
self.addEventListener('activate', event => {
    const currentCaches = [PRECACHE, RUNTIME];
    event.waitUntil(
        caches.keys().then(cacheNames => {
            return cacheNames.filter(cacheName => !currentCaches.includes(cacheName));
        })
        .then(cachesToDelete => {
            return Promise.all(cachesToDelete.map(cacheToDelete => {
                return caches.delete(cacheToDelete);
            }));
        })
        .then(() => self.clients.claim())
    );
});
self.addEventListener('fetch', event => {
    if(event.request.url.startsWith(self.location.origin)) {
        event.respondWith(
            caches.match(event.request).then(cachedResponse => {
                if(cachedResponse) {
                    return cachedResponse;
                }
                return caches.open(RUNTIME).then(cache => {
                    return fetch(event.request).then(response => {
                        return cache.put(event.request, response.clone()).then(() => {
                            return response;
                        });
                    });
                });
            })
        );
    }
});

Any ideas? Thanks!

1 thought on “Service Worker (sw.js) should always return offline.html document if there is no network connection”

  1. Most of your code worked as expected, but you needed a check to see if the user was requesting start.html. I took the code from Create an offline fallback page and modified it to suit your request.

    // Incrementing OFFLINE_VERSION will kick off the install event and force
    // previously cached resources to be updated from the network.
    const OFFLINE_VERSION = 1;
    const CACHE_NAME = "offline";
    // Customize this with a different URL if needed.
    const START_URL = "start.html";
    const OFFLINE_URL = "offline.html";
    
    self.addEventListener("install", (event) => {
      event.waitUntil(
        (async () => {
          const cache = await caches.open(CACHE_NAME);
          // Setting {cache: 'reload'} in the new request will ensure that the
          // response isn't fulfilled from the HTTP cache; i.e., it will be from
          // the network.
          await Promise.all([
            cache.add(new Request(OFFLINE_URL, { cache: "reload" })),
            cache.add(new Request(START_URL, { cache: "reload" })),
          ]);
        })()
      );
      // Force the waiting service worker to become the active service worker.
      self.skipWaiting();
    });
    
    self.addEventListener("activate", (event) => {
      event.waitUntil(
        (async () => {
          // Enable navigation preload if it's supported.
          // See https://developers.google.com/web/updates/2017/02/navigation-preload
          if ("navigationPreload" in self.registration) {
            await self.registration.navigationPreload.enable();
          }
        })()
      );
    
      // Tell the active service worker to take control of the page immediately.
      self.clients.claim();
    });
    
    self.addEventListener("fetch", (event) => {
      // We only want to call event.respondWith() if this is a navigation request
      // for an HTML page.
      if (event.request.mode === "navigate") {
        event.respondWith(
          (async () => {
            try {
                      
              // First, try to use the navigation preload response if it's supported.
              const preloadResponse = await event.preloadResponse;
              if (preloadResponse) {
                return preloadResponse;
              }
    
              // Always try the network first.
              const networkResponse = await fetch(event.request);
              return networkResponse;
            } catch (error) {
              // catch is only triggered if an exception is thrown, which is likely
              // due to a network error.
              // If fetch() returns a valid HTTP response with a response code in
              // the 4xx or 5xx range, the catch() will NOT be called.
              console.log("Fetch failed; returning cached page instead.", error);
    
              const cache = await caches.open(CACHE_NAME);
              if (event.request.url.includes(START_URL)) {
                return await cache.match(START_URL);
              }
              return await cache.match(OFFLINE_URL);
            }
          })()
        );
      }
    
      // If our if() condition is false, then this fetch handler won't intercept the
      // request. If there are any other fetch handlers registered, they will get a
      // chance to call event.respondWith(). If no fetch handlers call
      // event.respondWith(), the request will be handled by the browser as if there
      // were no service worker involvement.
    });
    

    One thing to note with this, once start.html has been cached when the service worker is first installed, it will not be updated again until the service worker is updated. That means your users may see an old/outdated start.html any time they’re offline and load your app. You probably want to use a network first strategy for start.html.

    You can try the working demo and source

    Reply

Leave a Comment