4

I am using Chrome stable 46.

I have a very basic site. One button.onclick that fetch('a') and one <a> tag that opens b. Both urls a and b do not exist, I intend to catch them in the service worker and just return a new Response().

If I click on the button which fetches a, the service worker is not involved and I get a 404 error.
But if I click on the link that goes to b, the service worker onfetches and returns the response.

Question: Why is the service worker not getting any FetchEvent for the fetch('a') request?

See the files below

HTML

<html>
  <body>
    <p><button onclick="fetch('a');">fetch a</button></p>
    <p><a href="b">go to b</a></p>
    <script src="js.js" defer></script>
  </body>
</html>

js.js

navigator.serviceWorker.register('sw.js');

sw.js

self.onfetch = function(event) {
  var request = event.request;
  event.respondWith(
    caches.match(request).then(function(response) {
      return new Response('here is your onfetch response');
    })
  );
};
VLAZ
  • 26,331
  • 9
  • 49
  • 67
oldergod
  • 15,033
  • 7
  • 62
  • 88

1 Answers1

7

The service worker's fetch event handler isn't being called the first time you load the page because the service worker hasn't yet "claimed" the page, meaning it's not under the service worker's control. By default, the first time you visit a site that registers a service worker, the service worker's install (and potentially activate) event handlers will run, but fetch won't get triggered until the service worker takes control. That happens the next time you navigate to a page under the service worker's scope, or if you reload the current page.

You can override this default behavior and have the service worker take control during the initial navigation by calling self.clients.claim() inside your activate event handler.

I also want to point out that the fetch event handler in your sample has some issues—there's no reason to call caches.match(request) if you're always planning on returning a new Response object. More importantly, you need to do some sort of check of the event.request.url value and only return the new Response if it matches one of your "special" URLs, which in your case is a and b. The way things are implemented now, your fetch handler will return the dummy Response unconditionally, even if it's a subsequent request for your main page (index.html). That's presumably not what you want.

I put a gist together that modifies your service worker code to accomplish what I believe you're attempting. You can try out a live version thanks to RawGit. For posterity, here's the modified service worker code:

self.addEventListener('install', event => {
  // Bypass the waiting lifecycle stage,
  // just in case there's an older version of this SW registration.
  event.waitUntil(self.skipWaiting());
});

self.addEventListener('activate', event => {
  // Take control of all pages under this SW's scope immediately,
  // instead of waiting for reload/navigation.
  event.waitUntil(self.clients.claim());
});

self.addEventListener('fetch', event => {
  // In a real app, you'd use a more sophisticated URL check.
  if (event.request.url.match(/a|b$/)) {
    event.respondWith(new Response('here is your onfetch response'));
  }
});
Jeff Posnick
  • 53,580
  • 14
  • 141
  • 167
  • This question was such a mystery to me, I totally forgot that claiming would only apply for going to a new page but not a fetch request! Thanks for relieving my curiosity! – owencm Dec 02 '15 at 19:23
  • 1
    @Jeff is there a reason you use `self.addEventListener` instead of `self.on` ? – oldergod Dec 04 '15 at 02:56
  • 2
    Equivalent to http://stackoverflow.com/a/6348597/385997. Using aEL means we can register multiple callbacks vs. the single callback you get via onXYZ. This is import for fetch in particular, since you might structure your code in such a way that you have several callbacks, each of which gets an opportunity to handle the event via reapondWith(). – Jeff Posnick Dec 04 '15 at 03:03