Search code examples
progressive-web-appsservice-workerworkbox

Can I have both Workbox registerroute and service worker fetch event handler at the same time?


I'm building a PWA application, and I have used workbox registerroute for a few api endpoints, as well as an explicit service worker fetch event listener. During the debugging on some caching issues, I've noticed that these two seems to interfere with each other. Specifically sometimes the fetch handler is not triggered - which causes me trouble on debugging - I'm assuming this is due to the registerroute caching policy I have set via workbox.

My question is that, can I only pick one or the other, instead of having both fetch handler and registerroute? In my case, I needed fetch handler to deal with some advanced caching related to POST requests. So I think if I can only pick one, I'll have to stick with the fetch handler.


Solution

  • First, here's some background information about what happens when there's multiple fetch event handlers in the active service worker.

    With that background info in mind, there are a few approaches for accomplishing what you're describing.

    Option 1a: Register your own fetch event handler first

    As long as you register your own fetch handler first, before any calls to Workbox's registerRoute(), it's guaranteed to have the "first shot" at responding the incoming fetch event.

    The thing to keep in mind is that your own fetch handler needs to make a synchronous decision about whether or not to call event.respondWith(), and when you do call event.respondWith(), then Workbox's routes will not get used to respond to a given request.

    So, you could do the following:

    self.addEventListener('fetch', (event) => {
      // Alternatively, check event.request.headers,
      // or some other synchronous criteria.
      if (event.request.url.endsWith('.json')) {
        event.respondWith(customResponseLogic(event));
      }
    });
    
    // Then, include any Workbox-specific routes you want.
    registerRoute(
      ({request}) => request.destination === 'image',
      new CacheFirst()
    );
    
    // The default handler will only apply if your own
    // fetch handler didn't respond.
    registerDefaultHandler(new StaleWhileRevalidate());
    

    Option 1b: Ensure Workbox routes won't match

    This is similar to 1a, but the main thing is to make sure that you don't have a "catch-all" route that will match all requests, and that you don't use registerDefaultHandler().

    Assuming your Workbox routes just match a specific set of well-defined criteria, and don't match any of the requests that you want to respond to in your own handler, it shouldn't matter how you order them:

    // Because this will only match image requests, it doesn't
    // matter if it's listed first.
    registerRoute(
      ({request}) => request.destination === 'image',
      new CacheFirst()
    );
    
    self.addEventListener('fetch', (event) => {
      // Alternatively, check event.request.headers,
      // or some other synchronous criteria.
      if (event.request.url.endsWith('.json')) {
        event.respondWith(customResponseLogic(event));
      }
    });
    

    (What's going on "under the hood" is that if there isn't a Route whose synchronous matchHandler returns a truthy value, Workbox's Router won't call event.respondWith().)

    Option 2: Use custom handler logic

    It should be viable to use Workbox to handle all your routing, and run your custom response generation code in either a handlerCallback (more straightforward) or a custom subclass of the Strategy base class (more reusable, but overkill for simple use cases).

    The one thing to keep in mind is that if you're dealing with POST requests, you need to explicitly tell registerRoute() to respond to them, by passing in 'POST' as the (optional) third parameter.

    Here's an example of how you could do this, assuming as before that you custom logic is defined in a customResponseLogic() function:

    registerRoute(
      ({request}) => request.destination === 'image',
      new CacheFirst()
    );
    
    registerRoute(
      // Swap this out for whatever criteria you need.
      ({url}) => url.pathname.endsWith('.json'),
    
      // As before, this assumes that customResponseLogic()
      // takes a FetchEvent and returns a Promise for a Response.
      ({event}) => customResponseLogic(event),
    
      // Make sure you include 'POST' here!
      'POST'
    );