Best practices for detecting offline state in a service worker

24,486

navigator.onLine and the related events can be useful when you want to update your UI to indicate that you're offline and, for instance, only show content that exists in a cache.

But I'd avoid writing service worker logic that relies on checking navigator.onLine. Instead, attempt to make a fetch() unconditionally, and if it fails, provide a backup response. This will ensure that your web app behaves as expected regardless of whether the fetch() fails due to being offline, due to lie-fi, or due to your web server experiencing issues.

// Other fetch handler code...

if (event.request.mode === 'navigate') {
  return event.respondWith(
    fetch(event.request).catch(() => caches.match(OFFLINE_URL))
  );
}

// Other fetch handler code...
Share:
24,486
Kaivosukeltaja
Author by

Kaivosukeltaja

Web Developer since 2000, currently using JavaScript, React, React Native, (S)CSS. In previous life lots of PHP, MySQL, Symfony, Drupal. Dabbling with Python and Django. Beer geek, whisky enthusiast, bass player, photographer.

Updated on September 14, 2020

Comments

  • Kaivosukeltaja
    Kaivosukeltaja over 3 years

    I have a service worker that is supposed to cache an offline.html page that is displayed if the client has no network connection. However, it sometimes believes the navigator is offline even when it is not. That is, navigator.onLine === false. This means the user may get offline.html instead of the actual content even when online, which is obviously something I'd like to avoid.

    This is how I register the service worker in my main.js:

    // Install service worker for offline use and caching
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/service-worker.js', {scope: '/'});
    }
    

    My current service-worker.js:

    const OFFLINE_URL = '/mysite/offline';
    const CACHE_NAME = 'mysite-static-v1';
    
    self.addEventListener('install', (event) => {
      event.waitUntil(
        // Cache the offline page when installing the service worker
        fetch(OFFLINE_URL, { credentials: 'include' }).then(response =>
          caches.open(CACHE_NAME).then(cache => cache.put(OFFLINE_URL, response)),
        ),
      );
    });
    
    self.addEventListener('fetch', (event) => {
      const requestURL = new URL(event.request.url);
    
      if (requestURL.origin === location.origin) {
        // Load static assets from cache if network is down
        if (/\.(css|js|woff|woff2|ttf|eot|svg)$/.test(requestURL.pathname)) {
          event.respondWith(
            caches.open(CACHE_NAME).then(cache =>
              caches.match(event.request).then((result) => {
                if (navigator.onLine === false) {
                  // We are offline so return the cached version immediately, null or not.
                  return result;
                }
                // We are online so let's run the request to make sure our content
                // is up-to-date.
                return fetch(event.request).then((response) => {
                  // Save the result to cache for later use.
                  cache.put(event.request, response.clone());
                  return response;
                });
              }),
            ),
          );
          return;
        }
      }
    
      if (event.request.mode === 'navigate' && navigator.onLine === false) {
        // Uh-oh, we navigated to a page while offline. Let's show our default page.
        event.respondWith(caches.match(OFFLINE_URL));
        return;
      }
    
      // Passthrough for everything else
      event.respondWith(fetch(event.request));
    });
    

    What am I doing wrong?

  • Kaivosukeltaja
    Kaivosukeltaja over 6 years
    That's a good approach but seems to also trigger the offline page on error 403, messing up our login process.
  • Jeff Posnick
    Jeff Posnick over 6 years
    It shouldn't. fetch() won't reject as long as there's some sort of response returned, even if the response has a non-200 error code. Try running fetch('https://httpbin.org/status/403').then(response => console.log('resolved', response)).catch(error => console.error('rejected', error)); in the JS console and see what it logs.
  • Kaivosukeltaja
    Kaivosukeltaja over 6 years
    Seems like you're absolutely right, there's something else causing the worker to serve wrong content. I'll need to investigate a bit further but since your solution seems to be the way to go I'll mark it as accepted. Thanks!
  • Sten Muchow
    Sten Muchow over 6 years
    for anybody reading the comments - we are using nginx to proxy a backend (at least on development) so using this approach it was never falling into the catch, i needed to sniff the res code and then manually reject - like so --> developers.google.com/web/updates/2015/03/introduction-to-fe‌​tch
  • James Tran
    James Tran over 5 years
    @JeffPosnick It seems that inside the service worker the online and offline events never get fired if they are added to self. Is that the way it should be or an oversight in Chrome? The navigator.onLine property is accessible, however.
  • Jeff Posnick
    Jeff Posnick over 5 years
    Hey Weston—It's intentional. A service worker isn't running most of the time, and what you're proposing would require effectively "waking it up" each time a device goes online/offline just to fire that event. Events like fetch and message can wake it up, but that's about it.
  • MalcolmOcean
    MalcolmOcean over 5 years
    @WestonRuter I don't know much about this but my understanding is that the sync event (not supported in all browsers) essentially functions as a "the app came online" event.
  • cherouvim
    cherouvim about 3 years
    This is triggered on other situations though, e.g a broken TLS certificate (net::ERR_CERT_AUTHORITY_INVALID), and this cannot be detected by the generic error caught which reads TypeError: Failed to fetch. Any idea on how to do this?