workbox: fetchDidFail with StaleWhileRevalidate is not called

Library Affected: workbox v5.1.3 npm

Browser & Platform: Google Chrome v85

Issue or Feature Request Description: I don’t seem to get how to run some code when a fetch in a StaleWhileRevalidate strategy fails. I’d like to use the callback to detect offline usage, but fetchDidFail is not called when I tick “offline” and reload the pahe. (fetchDidSucceed works tho). Is this expected? I didn’t find any mention of that in the documentation.

Here’s an example:

const messageIfFail: WorkboxPlugin = {
    fetchDidFail: async function () {
        // No return expected.
        // NOTE: `originalRequest` is the browser's request, `request` is the
        // request after being passed through plugins with
        // `requestWillFetch` callbacks, and `error` is the exception that caused
        // the underlying `fetch()` to fail.
        while (true) {
            console.log("called");
        }
        // send message to client
    }
}

registerRoute(
    ({url}) => {
        return url.origin == "https://fonts.googleapis.com"
            || url.origin == "https://cdn.jsdelivr.net"
    },
    new StaleWhileRevalidate({
        cacheName: 'cdn',
        plugins: [
            messageIfFail
        ]
    })
);

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Comments: 16 (2 by maintainers)

Most upvoted comments

When attempting to debug this, I would suggest starting fresh from an Incognito window, and additionally, not checking Disable Cache. It sounds like you’ve made a number of changes to your SW during development, and it would be good to rule out inconsistent cache state (using the Incognito window) while also replicating what users will actually see in production (don’t check Disable Cache).

You might see a log message about a failed fetch() made by StaleWhileRevalidate in development, but if the strategy uses a cached response from the Cache Storage API to satisfy the request, the “catch” handler won’t run. The “catch” handler only runs when there’s no response (from fetch() or Cache Storage API) that can be used by a strategy.

FWIW, your code could be simplified to:

registerRoute(
    ({url}) => {
        return url.origin === "https://fonts.googleapis.com"
            || url.origin === "https://cdn.jsdelivr.net"
    },
    new StaleWhileRevalidate({
      cacheName: 'cdn',
      plugins: [...],
    })
});

setCatchHandler(async () => { //doesn't get called in my situation
    console.log("TEST");
    return Response.error();
});

@TheForsakenSpirit So sorry! Changed the code without re-checking.

@jeffposnick Very well, I’ve seen some responses have 80000+ seconds on max-age, but that’s ok, I don’t actually need to detect the connection status, just signal the app went to “offline mode”. Until an http cached response expires, I can consider it indistinguishable from online work. Hoping that I’m not forgetting something stupid again, I still have two errors I can’t manage to solve.
Only this route is affected. (I also have the recommended route for Google Fonts font files, but it has never given errors as now. I clear the application data regularly during testing)

The FetchEvent for "https://cdn.jsdelivr.net/npm/normalize.css@8/normalize.min.css" resulted in a network error response: an "opaque" response was used for a request whose type is not no-cors
normalize.min.css:1 Failed to load resource: net::ERR_FAILED
sw.js:377 workbox Precaching is responding to: /font-atlas-generator/build/css/main.css
sw.js:377 workbox Using StaleWhileRevalidate to respond to 'https://fonts.googleapis.com/css2?family=Open+Sans&display=block'
The FetchEvent for "https://fonts.googleapis.com/css2?family=Open+Sans&display=block" resulted in a network error response: an "opaque" response was used for a request whose type is not no-cors
css2:1 Failed to load resource: net::ERR_FAILED

This happens randomly, when opening the browser. If it occurs, it doesn’t go away until the app data is cleared. I have no Idea why. Normally cross-origin requests are correctly stored as “cors”. (Only if made explicit through html). This one breaks the sw functionality.


workbox Network request for 'https://cdn.jsdelivr.net/npm/normalize.css@8/normalize.min.css' threw an error. TypeError: Failed to fetch
print @ sw.js:377
(anonymous) @ sw.js:390
wrappedFetch @ sw.js:2641
async function (async)
wrappedFetch @ sw.js:2550
_getFromNetwork @ sw.js:3945
handle @ sw.js:3885
(anonymous) @ sw.js:4019
handleRequest @ sw.js:786
(anonymous) @ sw.js:653
sw.js:377 workbox Network request for 'https://fonts.googleapis.com/css2?family=Open+Sans&display=block' threw an error. TypeError: Failed to fetch
print @ sw.js:377
(anonymous) @ sw.js:390
wrappedFetch @ sw.js:2641
async function (async)
wrappedFetch @ sw.js:2550
_getFromNetwork @ sw.js:3945
handle @ sw.js:3885
(anonymous) @ sw.js:4019
handleRequest @ sw.js:786
(anonymous) @ sw.js:653
sw.js:1 Uncaught (in promise) TypeError: Failed to fetch
sw.js:377 workbox Using StaleWhileRevalidate to respond to 'https://fonts.googleapis.com/css2?family=Open+Sans&display=block'
sw.js:1 Uncaught (in promise) TypeError: Failed to fetch

This happens instead when the app is offline and the http cache is disabled (“Disable Cache” ticked). This is the “development” version, the same errors appears in “production” mode too. The same errors show up in a completely generated sw. Since service workers exist to provide offline support, I’m surely missing something here.

This is the code I’m using:

registerRoute(
    ({url}) => {
        return url.origin == "https://fonts.googleapis.com"
            || url.origin == "https://cdn.jsdelivr.net"
    },
    ({event}) => {
        const {request} = event as FetchEvent;
        return new StaleWhileRevalidate({
            cacheName: "cdn",
            plugins: [
                {
                    fetchDidFail: async function () {
                        //...
                    }
                }
            ]
        }).handle({event, request});
    });

setCatchHandler(async () => { //doesn't get called in my situation
    console.log("TEST");
    return Response.error();
});

fetchDidFail will execute when the underlying fetch() fails when using StaleWhileRevalidate, even if the strategy ends up using a response from the Cache Storage API.

I see from your sample route that you’re attempting to use this with subresources loaded from a CDN, and in most cases, responses to those sorts of URLs will use long-lived Cache-Control headers that don’t require revalidation. So what my guess is as to what’s happening is that even when you’re offline, the underlying fetch() request succeeds, rather than fails, since a cache hit in the browser’s “normal” HTTP cache is sufficient, and fetch() never ends up going against the network anyway.

You can see this in action at https://glitch.com/edit/#!/upbeat-rebel-octopus?path=sw.js, where the example is for a HTTP response that has a maximum age of 20 seconds. If you go offline and try again within 20 seconds of populating the “normal” HTTP cache with a response, then the fetch() will be fulfilled from the HTTP cache. If you wait longer than 20 seconds, then the response in the HTTP cache will be considered too old to use without revalidation, and the fetch() will fail when it makes the network request to revalidate.

I’m going to close this for now, but if I’m misinterpreting anything, let me know and we can revisit.

@Lucide requestWillFetch must return Request object. Check this.

Alright. I find that to be a bit misleading tho. Because it mentions “fetch” specifically, “request” might be better? Maybe some more explanatory lines in the docs. Thank you!