tl;dr: Do not cache faulty responses in your service worker.
In trying to make my 1hz app an installable PWA in the most simple way possible, I tried using Jeremy Keith's "Minimal viable service worker". The app loads water.css from jsDelivr's CDN. Unfortunately, this caused service worker to fail to fetch and the page is displayed without a style sheet.
On the Console, I see:
Text version
The FetchEvent for "https://cdn.jsdelivr.net/npm/water.css@2.0.0/out/dark.min.css" resulted in a network error response: an "opaque" response was used for a request whose type is not no-cors
serviceworker.js:22 Uncaught (in promise) TypeError: Failed to execute 'put' on 'Cache': Request scheme 'chrome-extension' is unsupported
at serviceworker.js:22
The FetchEvent for "https://cdn.jsdelivr.net/npm/water.css@2.0.0/out/dark.min.css" resulted in a network error response: an "opaque" response was used for a request whose type is not no-cors
Promise.then (async)
(anonymous) @ serviceworker.js:16
dark.min.css:1 Failed to load resource: net::ERR_FAILED
serviceworker.js:22 Uncaught (in promise) TypeError: Failed to execute 'put' on 'Cache': Request scheme 'chrome-extension' is unsupported
at serviceworker.js:22
(anonymous) @ serviceworker.js:22
DevTools failed to load SourceMap: Could not load content for https://1hz.glitch.me/dark.min.css.map: HTTP error: status code 404, net::ERR_HTTP_RESPONSE_CODE_FAILURE
Even worse, this happens intermittently. Sometimes it works, and sometimes it fails1. I thought maybe this is a race condition issue... or maybe not. Anyways, I was in a hurry, so I decided to replace the minimum viable service workers with the tried-and-true Workbox (but using the same logic):
/* global workbox */
importScripts(
'https://storage.googleapis.com/workbox-cdn/releases/5.0.0/workbox-sw.js'
)
const { registerRoute } = workbox.routing
const { StaleWhileRevalidate, NetworkFirst } = workbox.strategies
// HTML pages
registerRoute(
({ request }) => request.headers.get('Accept').includes('text/html'),
new NetworkFirst()
)
// Anything else
registerRoute(() => true, new StaleWhileRevalidate())
Nope, still failed, intermittently. The FetchEvent for ".../dark.min.css" resulted in a network error response: an "opaque" response was used for a request whose type is not no-cors and even Workbox cannot deal with that???? Argh!! Guess I'll have to dig in and fix the service worker then.
This led me to this StackOverflow question: Can a service worker fetch and cache cross-origin assets? The answer tells me to make sure to "call clone() before the final return response executes." Maybe it's a race condition. So I made the change:
- const fetchPromise = fetch(request);
+ const splittedPromise = originalFetchPromise.then(response => ({
+ original: response,
+ copy: response.clone(),
+ }));
+ const fetchPromise = splittedPromise.then(({ original }) => original);
+ const responseCopyPromise = splittedPromise.then(({ copy }) => copy);
+ const originalFetchPromise = fetch(request);
fetchEvent.waitUntil(async function() {
- const responseFromFetch = await fetchPromise;
- const responseCopy = responseFromFetch.clone();
+ const responseCopy = await responseCopyPromise;
const myCache = await caches.open(cacheName);
return myCache.put(request, responseCopy);
}());
...to no avail. Still erroring out, and page still loads without external CSS applied.
Next, I found this Smashing Magazine article: Leonardo Losoviz (2017). “Implementing A Service Worker For Single-Page App WordPress Sites”, and I quote (emphasis mine):
Whenever the resource originates from the website’s domain, it can always be handled using service workers. Whenever not, it can still be fetched but we must use
no-cors
fetch mode. This type of request will result in an opaque response, so we won’t be able to check whether the request was successful; however, we can still precache these resources and allow the website to be browsable offline.
+ const fetchOptions =
+ (new URL(request.url)).origin === self.location.origin
+ ? {}
+ : {mode: 'no-cors'};
- const fetchPromise = fetch(request);
+ const fetchPromise = fetch(request, fetchOptions);
Again it doesn't help. Worse, it makes more requests fail. The app is more broken.
It's time for my special attack: console.log()
the heck out of this service worker.
What's that? We're putting faulty responses into the cache?
fetchEvent.waitUntil(async function() {
const responseFromFetch = await fetchPromise;
const responseCopy = responseFromFetch.clone();
+ if (!responseCopy.ok) return;
const myCache = await caches.open(cacheName);
return myCache.put(request, responseCopy);
}());
...and the white unstyled pages are nowhere to be seen again. Service workers are indeed rocket science.
Footnotes
In hindsight, I reckon that it fails around 20% of the time, making debugging this extremely frustrating. ↩