ServiceWorker not receiving fetch requests - service-worker

I am installing a service worker for the first time, and following the tutorial at: https://developers.google.com/web/fundamentals/getting-started/primers/service-workers
My service worker behaves as expected when installing and updating, but fetch requests are not triggered as expected.
var CACHE_NAME = 'test-cache-v1'
var urlsToCache = [
'/',
'/public/scripts/app.js'
]
self.addEventListener('install', function (event) {
console.log('Installing new service worker', event)
// Perform install steps
event.waitUntil(
caches.open(CACHE_NAME)
.then(function (cache) {
return cache.addAll(urlsToCache)
})
.catch(err => console.log('Error Caching', err))
)
})
self.addEventListener('fetch', function (event) {
console.log('Fetch req', event)
event.respondWith(
caches.match(event.request)
.then(function (response) {
console.log('Cache hit', response)
// Cache hit - return response
if (response) {
return response
}
return fetch(event.request)
.catch(e => console.log('Error matching cache', e))
}
)
)
})
I see 'Installing new service worker' outputted to the console when expected, but not 'Fetch req'. I am using Chrome devtools and have accessed the "Inspect" option next to the ServiceWorker under the Application tab.

If you listen for the activate event, and add in a call to clients.claim() inside that event, then your newly active service worker will take control over existing web pages in its scope, including the page that registered it. There's more information in this article on the service worker lifecycle. The following code is sufficient:
self.addEventListener('activate', () => self.clients.claim());
If you don't call clients.claim(), then the service worker will activate, but not control any of the currently open pages. It won't be until you navigate to the next page under its scope (or reload a current page) that the service worker will take control, and start intercepting network requests via its fetch handler.

On dynamic websites, be careful!
If service worker has scope: example.com/weather/
It does not have scope: example.com/weather
Especially on firebase which by default removes trailing slash
In this case, service worker will install, activate, and even cache files, but not receive ‘fetch’ events! Very hard to debug.
Add “trailingSlash”: true to firebase.json under ‘hosting’. This will solve the problem. Make sure to modify rewrite from:
{
"source": "/weather", "function": "weather"
}
To :
{
"source": "/weather/", "function": "weather"
}
As well as manifest.json

I found that Jeff Posnick's "clients.claim()" in the activate event handler was useful, but it was not enough to cache resources the first time the JS app runs. That is because on the first run the service worker has not finished activating when the JS starts loading its resources.
The following function lets the main app register the SW and then waits for it to activate before continuing to load resources:
/**
* Registers service worker and waits until it is activated or failed.
* #param js URI of service worker JS
* #param onReady function to call when service worker is activated or failed
* #param maxWait maximum time to wait in milliseconds
*/
function registerServiceWorkerAndWaitForActivated(js, onReady, maxWait) {
let bReady = false;
function setReady() {
if (!bReady) {
bReady = true;
onReady();
}
}
if ('serviceWorker' in navigator) {
setTimeout(setReady, maxWait || 1000);
navigator.serviceWorker.register(js).then((reg) => {
let serviceWorker = reg.installing || reg.waiting;
if (serviceWorker) {
serviceWorker.addEventListener("statechange", (e) => {
if (serviceWorker.state == "activated")
setReady();
});
} else {
if (!reg.active)
console.log("Unknown service worker state");
setReady();
}
}, () => setReady());
} else {
let msg = "ServiceWorker not available. App will not run offline."
if (document.location.protocol != "https:")
msg = "Please use HTTPS so app can run offline later.";
console.warn(msg);
alert(msg);
setReady();
}
}

Related

Background sync doesn't refresh page when back online

recently I started learning about service workers, background syncs... I implemented service worker and in install step I cached some files I want to show when offline.
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE)
.then((cache) => {
return cache.addAll([navigationIcon, offlineImage, offlineFallbackPage]);
})
);
});
I am listening to fetch event to catch when there is no internet connection so I can show offline page when then.
self.addEventListener('fetch', (event) => {
if (event.request.mode === 'navigate' || (event.request.method === 'GET'
&& event.request.headers.get('accept')
.includes('text/html'))) {
event.respondWith(
fetch(event.request.url)
.catch(() => {
// Return the offline page
return caches.match(offlineFallbackPage);
})
);
} else {
event.respondWith(caches.match(event.request)
.then((response) => {
return response || fetch(event.request);
}));
}
});
I also added background sync, so I can go back online when there is internet connection.
After registering service worker I added:
.then(swRegistration => swRegistration.sync.register('backOnline'))
And I listen to sync event in my service worker.
When I'm offline and go back online nothing happens. BUT when I delete my fetch event (don't show previously cached offline page) then page goes back online by itself (which I want to do when I have fetch event)
Does anyone know what should I add so my page can go back online by itself?
You can use navigator, Include it in your main js file that is cached or in your service-worker js file, just ensure it's cached
let onlineStatus = locate => {
if(navigator.onLine){
location.replace(locate)
}
}
let isOfflinePage = window.location.pathname == offlineFallbackPage ? true : false;
// kindly edit isOfflinePage to return true if it's offline page
if(isOfflinePage){
onlineStatus('index.html')
}
You can simply use location.reload() instead

Access service worker skipWaiting from within App build with Webpack+Workbox

I have a PWA built with Aurelia and compiled with Webpack, using the Workbox Plugin that generates the sw.js service worker file. I'm trying to make the "New version available" user notification so that the user can activate the new version when clicking on a link within the app.
I am successfully downloading and installing the new version in the background, and even detecting that a new version is ready. However, when I try to call the skipWaiting() method to force refresh of the page with the new version, it fails, because apparently I don't have the right scope or object.
The main problem is probably that I can't edit the actual sw.js because it is automatically generated. The examples all suggest the use of self.skipWaiting();, but I don't know how to access that object.
webpack.config.js
new WorkboxPlugin({
globDirectory: './dist',
globPatterns: ['**/*.{html,js,css,woff,woff2,ttf,svg,eot,jpg}'],
swDest: './dist/sw.js',
clientsClaim: true,
skipWaiting: false, // because I want to notify the user and wait for response
}),
index.ejs
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(reg => {
// make the registration available globally, for access within app
window.myServiceWorkerReg = reg;
// Check for update on loading the app (is this necessary?)
return reg.update();
})
.catch(console.error);
}
</script>
app.js
activate() {
// listener for service worker update
this.swReg = window.myServiceWorkerReg;
console.warn('[app.js] ACTIVATE.', this.swReg);
this.swReg.addEventListener('updatefound', () => {
// updated service worker found in reg.installing!
console.warn('[app.js] UPDATE FOUND.', this.swReg);
const newWorker = this.swReg.installing;
newWorker.addEventListener('statechange', () => {
// has the service worker state changed?
console.warn('[app.js] STATE HAS CHANGED.', newWorker, newWorker.state);
if (newWorker.state === 'installed') {
// New service worker ready.
// Notify user; callback for user request to load new app
myUserMessage({ clickToActivate: () => {
// reload fresh copy (do not cache)
console.warn('[app.js] Post Action: skipWaiting.');
// this.swReg.postMessage({ action: 'skipWaiting' });
// THIS IS THE LINE THAT FAILS
this.swReg.skipWaiting();
}});
}
});
});
}
Everything works fine except the last line (this.swReg.skipWaiting();). Has anyone else used webpack+workbox plugin and gotten the skipWaiting to happen as a result of user interaction?
I finally got it to work. One problem was that I was using an older version of workbox-webpack-plugin. The current version (4.2) includes a listener in the service worker that can trigger self.skipWaiting() when a message is posted to the worker like this:
newWorker.postMessage({ type: 'SKIP_WAITING' });
But you have to ensure that the config has skipWaiting: false; and that you are using the latest version.
These instructions are pretty good:
https://developers.google.com/web/tools/workbox/modules/workbox-webpack-plugin
https://developers.google.com/web/tools/workbox/guides/advanced-recipes#offer_a_page_reload_for_users
However, I tweaked things to work well between my App and the service worker instantiation in the index.ejs file.
webpack.config.js
new GenerateSW({
globPatterns: ['dist/**/*.{html,js,css,woff,woff2,ttf,svg,eot,jpg}'],
swDest: 'sw.js',
clientsClaim: true,
skipWaiting: false,
})),
index.ejs
<script>
if ('serviceWorker' in navigator) {
// register the service worker
navigator.serviceWorker.register('/sw.js')
.then(reg => {
window.myWorkerReg = reg;
// Check for update on loading the app (is this necessary?)
return reg.update();
})
.catch(console.error);
// The event listener that is fired when the service worker updates
navigator.serviceWorker.addEventListener('controllerchange', function () {
// when the service worker controller is changed, reload the page
if (window.swRefreshing) return;
window.location.reload();
window.swRefreshing = true;
});
}
</script>
app.js
activate() {
// listener for service worker update
this.swReg = window.myWorkerReg;
if (this.swReg) {
// if there is already a new service worker ready to install, prompt user
if (this.swReg.waiting) {
this.promptUpdateServiceWorker(this.swReg.waiting);
}
// add listener to detect when a new service worker is downloaded
this.swReg.addEventListener('updatefound', () => {
// updated service worker is being installed
const newWorker = this.swReg.installing;
// add listener to detect when installation is finished
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed') {
// New service worker ready to activate; prompt user
this.promptUpdateServiceWorker(newWorker);
}
});
});
}
}
// listener for buildVersion
buildVersionChanged(buildVersion) {
// through proprietary code, we've detected a new version could be downloaded now
window.myWorkerReg.update();
}
// New service worker ready. Show the notification
promptUpdateServiceWorker(newWorker) {
// actual code for UI prompt will vary; this is pseudocode
uiPrompt('New_version_ready').then((response) => {
if (response.approved) {
// reload fresh copy (do not cache)
newWorker.postMessage({ type: 'SKIP_WAITING' });
}
});
}
You cannot call it on the page (app.js). You call self.skipWaiting on the Service Worker script (service-worker.js).
https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/skipWaiting

Service worker spreading

I have a service worker for caching images, this service worker is only registered within the frontend template but it still keeps spreading into my admin template.
This causes my forms to behave unpredictably as the validation tokens get impacted with it.
With some console.log I figured the install event is triggered before getting to the requested page but I'm unable to determine the current/next URL there.
How can I prevent the service worker to spreading to the admin panel and interfere with the pages? I just want only assets to be cached.
This is my service worker as far as that is relevant:
const PRECACHE = 'precache-v1.0.0';
const RUNTIME = 'runtime';
// A list of local resources we always want to be cached.
const PRECACHE_URLS = [
"public",
"media",
"unify",
];
importScripts('./cache-polyfill.js');
// The install handler takes care of precaching the resources we always need.
self.addEventListener('install', function(event) {
console.log('installing resources');
event.waitUntil(
caches.open(PRECACHE)
//.then(cache => cache.addAll(PRECACHE_URLS))
.then(self.skipWaiting())
);
});
// The activate handler takes care of cleaning up old caches.
self.addEventListener('activate', function(event) {
const currentCaches = [PRECACHE, RUNTIME];
event.waitUntil(
caches.keys().then(cacheNames => {
return cacheNames.filter(cacheName => !currentCaches.includes(cacheName));
}).then(cachesToDelete => {
return Promise.all(cachesToDelete.map(cacheToDelete => {
return caches.delete(cacheToDelete);
}));
}).then(() => self.clients.claim())
);
});
// The fetch handler serves responses for same-origin resources from a cache.
// If no response is found, it populates the runtime cache with the response
// from the network before returning it to the page.
self.addEventListener('fetch', event => {
// Skip cross-origin requests, like those for Google Analytics.
if (event.request.method === "GET") {
if (event.request.url.indexOf(PRECACHE_URLS) > -1) {
console.log("fetching " + event.request.url + " by the service worker");
event.respondWith(
caches.match(event.request).then(cachedResponse => {
if (cachedResponse) {
return cachedResponse;
}
return caches.open(RUNTIME).then(cache => {
return fetch(event.request).then(response => {
// Put a copy of the response in the runtime cache.
return cache.put(event.request, response.clone()).then(() => {
console.log('cached: ' + event.request.url);
return response;
});
});
});
})
);
}
else {
console.log("fetching " + event.request.url + " by service worker blocked, it's not a resource");
}
}
return fetch(event.request);
});
The problem is most likely that your admin pages lie inside the SW scope. This means that your SW controls eg. everything in / and your admin pages are located in /admin/ or something.
You can prevent the behaviour by checking the fetch requests your SW is intercepting. Something like:
if (event.request.url.match('^.*(\/admin\/).*$')) {
return false;
}
This should be the first thing in the SW's fetch listener. It checks whether it received a request for something from the admin pages and then cancels out if it did. Otherwise, it continues normally.

How to wait for 'message' event inside the 'install' event in service worker?

I have a scenario where i need to send postMessage of latest version files array from client to service worker on the update event of service worker.
current code
reg.onupdatefound = function() {
// The updatefound event implies that reg.installing is set; see
// https://w3c.github.io/ServiceWorker/#service-worker-registration-updatefound-event
var installingWorker = reg.installing;
console.log('on update found');
// service worker is updated with version name eesh
if(reg.installing)
{
reg.installing.postMessage({
data: cacheUrls()
});
}
installingWorker.onstatechange = function() {
switch (installingWorker.state) {
case 'installed':
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and the fresh content will
// have been added to the cache.
// It's the perfect time to display a "New content is available; please refresh."
// message in the page's interface.
console.log('New or updated content is available. yo yo');
// navigator.serviceWorker.controller.postMessage({data: location})
} else {
// At this point, everything has been precached.
// It's the perfect time to display a "Content is cached for offline use." message.
console.log('ServiceWorker registration successful with scope: ', reg.scope);
}
break;
case 'redundant':
console.error('The installing service worker became redundant.');
break;
}
};
};
};
But sometimes the installation happens first and then "message" event is listened in service worker. How do i wait for 'message' event inside 'install' event of service worker ?
I believe you can do something like this:
// sw.js
self.addEventListener('install', function(e) {
const installPromise = new Promise(function(resolve, reject) {
// do install stuff, like caching resources, etc.
self.addEventListener('message', function(e) {
// 1. do something with the received data
// 2. remove this event listener
resolve();
});
});
e.waitUntil(installPromise);
});
e.waitUntil() accepts a Promise so we give it one. The install phase gets completed only when the promise passed to e.waitUntil() resolves. We only resolve that Promise once we've already received the message from the client.

Ignore ajax requests in service worker

I have an app with a basic 'shell' of HTML, CSS and JS. The main content of the page is loaded via multiple ajax calls to an API that is at another URL to the one my app is running on. I have set up a service-worker to cache the main 'shell' of the application:
var urlsToCache = [
'/',
'styles/main.css',
'scripts/app.js',
'scripts/apiService.js',
'third_party/handlebars.min.js',
'third_party/handlebars-intl.min.js'
];
and to respond with the cached version when requested. The problem I am having is that the response of my ajax calls are also being cached. I'm pretty sure that I need to add some code to the fetch event of the service-worker that always get them from the network rather than looking in the cache.
Here is my fetch event:
self.addEventListener('fetch', function (event) {
// ignore anything other than GET requests
var request = event.request;
if (request.method !== 'GET') {
event.respondWith(fetch(request));
return;
}
// handle other requests
event.respondWith(
caches.open(CACHE_NAME).then(function (cache) {
return cache.match(event.request).then(function (response) {
return response || fetch(event.request).then(function (response) {
cache.put(event.request, response.clone());
return response;
});
});
})
);
});
I'm not sure how I can ignore the requests to the API. I've tried doing this:
if (request.url.indexOf(myAPIUrl !== -1) {
event.respondWith(fetch(request));
return;
}
but according to the network tab in Chrome Dev Tools, all of these responses are still coming from the service-worker.
You do not have to use event.respondWith(fetch(request)) to handle requests that you want to ignore. If you return without calling event.respondWith browser will fetch the resource for you.
You can do something like:
if (request.method !== 'GET') { return; }
if (request.url.indexOf(myAPIUrl) !== -1) { return; }
\\ handle all other requests
event.respondWith(/* return promise here */);
IOW as long as you can determine synchronously that you don't want to handle the request you can just return from the handler and let the default request processing to take over. Check out this example.

Resources