Prevent Accidental Caching of Service Worker Offline Responses

The Service Worker and Cache APIs allow web apps to provide a “Save for offline” button where a user’s click triggers the adding of a resource to the cache which can later be served by the service worker when offline.

A common feature of Progressive Web Apps is the provision of an offline fallback response when fetching from the network fails.

When implementing both of these features, it is important to make sure that the offline page is not cached by accident as the response to the request from the “Save for offline” button. On such an occurrence, we do not wish to cache this fallback resource.

Example

For example, if the service worker is configured to return an offline response when fetching from the network fails and the app simply caches the response given by the service worker’s fetch event listener then we will face this issue.

The request, response pair /some-resource-to-save-for-offline -> /offline would be cached if the fetch for /some-resource-to-save-for-offline from the network fails.

const OFFLINE_URL = '/offline';

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).
    then(response => {
      if (response) {
        return response;
      }

      return fetch(event.request)
      .catch(() => caches.match(OFFLINE_URL));
    })
  );
});
const path = '/some-resource-to-save-for-offline';

fetch(path)
.then((response) => {
  if (!response.ok) {
    return;
  }

  return caches.open('read-later-cache')
  .then((cache) => cache.put(path, response));
});

The Fix

This situation can be prevented by aborting the caching procedure if Response.url does not match the requested URL. If the offline fallback page was served response.url will be equal to the path of the offline page, not the originally requested URL.

const path = '/some-resource-to-save-for-offline';

fetch(path)
.then((response) => {
  if (!response.ok) {
    return;
  }

  const url = new URL(response.url);

  if (url.pathname !== path) {
    return;
  }

  return caches.open('read-later-cache')
  .then((cache) => cache.put(path, response));
});