Service worker Fetch - javascript

I'm using a Service Worker for working offline. so for Each Fetch request, i store it in the cache.
Now,
I'd like that the service worker will make a request, and store it as well for next time.
the problem is when i use fetch(myUrl).then... , the Fetch Listener self.addEventListener('fetch', function(e)...in the service worker doesn't catch it.
I wouldn't like to duplicate code.. any Ideas ?
The fetch listener is:
self.addEventListener('fetch', function(e) {
// e.respondWidth Responds to the fetch event
e.respondWith(
// Check in cache for the request being made
caches.match(e.request)
.then(function(response) {
// If the request is in the cache
if ( response ) {
console.log("[ServiceWorker] Found in Cache", e.request.url, response);
// Return the cached version
return response;
}
// If the request is NOT in the cache, fetch and cache
var requestClone = e.request.clone();
return fetch(requestClone)
.then(function(response) {
if ( !response ) {
console.log("[ServiceWorker] No response from fetch ")
return response;
}
var responseClone = response.clone();
// Open the cache
caches.open(cacheName).then(function(cache) {
// Put the fetched response in the cache
cache.put(e.request, responseClone);
console.log('[ServiceWorker] New Data Cached', e.request.url);
// Return the response
return response;
}); // end caches.open
// returns the fresh response (not cached..)
return response;
})
.catch(function(err) {
console.log('[ServiceWorker] Error Fetching & Caching New Data', err);
});
}) // end caches.match(e.request)
.catch(function(e){
// this is - if we still dont have this in the cache !!!
console.log("[ServiceWorker] ERROR WITH THIS MATCH !!!",e, arguments)
})// enf of caches.match
); // end e.respondWith
});

Since i cant comment to get any specific details and your code seems right to me, my guesses are:
You Service worker is not registered, or running. you can mak sure It is running by checking the application tab of your inspector. It should look like the following:
You assume It is not working because of the messages not being logged to the console.
Service workers run on a different environment from your page, so the messages are logged to a different console that you can check by clicking the inspect button in the image above.

The Cache then Network strategy is probably what you are looking for:
https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/#cache-then-network
var networkDataReceived = false;
startSpinner();
// fetch fresh data
var networkUpdate = fetch('/data.json').then(function(response) {
return response.json();
}).then(function(data) {
networkDataReceived = true;
updatePage();
});
// fetch cached data
caches.match('/data.json').then(function(response) {
if (!response) throw Error("No data");
return response.json();
}).then(function(data) {
// don't overwrite newer network data
if (!networkDataReceived) {
updatePage(data);
}
}).catch(function() {
// we didn't get cached data, the network is our last hope:
return networkUpdate;
}).catch(showErrorMessage).then(stopSpinner);

Related

Service Worker - how to know whether cache cleared?

I'm trying to implement a basic service worker to assure that users of my simple web app have the latest code. So when I update html, js, or css files I can increment the cachename in the service worker file and prompt users to refresh, clear their cache, and get the latest code.
Until now I've relied on hacky ways to update javascript files (including a parameter in the referring URL: /javascript-file.js?v=1).
The with the service worker code below seem unpredictable: sometimes small changes to JS or CSS are reflected after I increment the cachename (code below). Sometimes the changes are reflected without incrementing the cachename, which suggests the code is ALWAYS pulling from the network (wasting resources).
How can you troubleshoot which version of files the code is using and whether the service worker is using cached or network versions? Am I not understanding the basic model for using service workers to achieve this goal?
Any help appreciated.
serv-worker.js (in root):
console.log('Start serv-worker.js');
const cacheName = '3.2121';
var urlsToCache = [
'home.html',
'home-js.js',
'web-bg.js',
'css/main.css',
'css/edit-menus.css'
];
self.addEventListener('install', event => {
console.log('Install event...', urlsToCache);
event.waitUntil(
caches.open(cacheName)
.then(function(cache) {
console.log('Opened cache', cacheName);
return cache.addAll(urlsToCache);
})
);
});
// Network first.
self.addEventListener('fetch', (event) => {
// Check the cache first
// If it's not found, send the request to the network
// event.respondWith(
// caches.match(event.request).then(function (response) {
// return response || fetch(event.request).then(function (response) {
// return response;
// });
// })
// );
event.respondWith(async function() {
try {
console.log('aPull from network...', event.request);
return await fetch(event.request);
} catch (err) {
console.log('aPull from cache...', event.request);
return caches.match(event.request);
}
}());
});
self.addEventListener('message', function (event) {
console.log('ServiceWorker cache version: ', cacheName, event);
console.log('Received msg1: ', event.data);
if (event.data.action === 'skipWaiting') {
console.log('ccClearing cache: ', cacheName);
// caches.delete('1.9rt1'); // hardcode old one
// caches.delete(cacheName); // actually removes cached versions
caches.keys().then(function(names) {
for (let name of names)
caches.delete(name);
});
self.skipWaiting();
}
});
Code in web-bg.js, which home.html references:
function servWorker(){
let newWorker;
function showUpdateBar() {
console.log('Show the update mssgg...ddddd');
$('#flexModalHeader').html('AP just got better!');
$('#flexModalMsg').html("<p>AP just got better. Learn about <a href='https://11trees.com/support/release-notes-annotate-pro-web-editor/'>what changed</a>.<br><br>Hit Continue to refresh.</p>");
$('#flexModalBtn').html("<span id='updateAPbtn'>Continue</span>");
$('#flexModal').modal('show');
}
// The click event on the pop up notification
$(document).on('click', '#updateAPbtn', function (e) {
console.log('Clicked btn to refresh...');
newWorker.postMessage({ action: 'skipWaiting' });
});
if ('serviceWorker' in navigator) {
console.log('ServiceWORKER 1234');
navigator.serviceWorker.register(baseDomain + 'serv-worker.js').then(reg => {
console.log('In serviceWorker check...', reg);
reg.addEventListener('updatefound', () => {
console.log('A wild service worker has appeared in reg.installing!');
newWorker = reg.installing;
newWorker.addEventListener('statechange', () => {
// Has network.state changed?
console.log('SSState is now: ', newWorker.state);
switch (newWorker.state) {
case 'installed':
if (navigator.serviceWorker.controller) {
// new update available
console.log('Detected service worker update...show update...');
showUpdateBar();
}
// No update available
break;
}
});
});
});
let refreshing;
navigator.serviceWorker.addEventListener('controllerchange', function (e) {
console.log('a1111xxxListen for controllerchange...', e);''
if (refreshing) return;
console.log('Refresh the page...');
window.location.reload();
refreshing = true;
});
} // End serviceworker registration logic
return;
} // END serv-worker
You've commented out the section for /// Check the cache first and then below that the try/catch statement again pulls from the network and falls back to the cache.
Uncomment this section of code and see if you're loading from the cache first.
// event.respondWith(
// caches.match(event.request).then(function (response) {
// return response || fetch(event.request).then(function (response) {
// return response;
// });
// })
// );
Don't forget that even if you request from the network from the service worker the browser will still use it's own internal cache to serve data. How long the data stays in the browser's cache depends on the expiration headers being sent by the server.
When using expires, it's still a fairly common solution to do something like:
index.html - expires after an hour. Has script/css tags that call out file names with ?v=x.y.z
/resources - folder that holds js and css. This folder has a very long expiration time. But that long expiration is short circuited by changing the ?v=x.y.z in index.html
I've used the above successfully in Progressive Web Apps (PWAs). But it is a little painful when debugging. The best option here is to manually clear out the cache and service worker from Dev Tools \ Application, if you're in Chrome.

Service worker returns offline html page for javascript files

I'm new to service workers and offline capabilities. I created a simple service worker to handle network requests and return a offline html page when offline. This was created following Google's guide on PWA.
The problem is that the service worker returns offline.html when requesting javascript files (not cached). It should instead return a network error or something. Here is the code:
const cacheName = 'offline-v1900'; //increment version to update cache
// cache these files needed for offline use
const appShellFiles = [
'./offline.html',
'./css/bootstrap.min.css',
'./img/logo/logo.png',
'./js/jquery-3.5.1.min.js',
'./js/bootstrap.min.js',
];
self.addEventListener("fetch", (e) => {
// We only want to call e.respondWith() if this is a navigation request
// for an HTML page.
// console.log(e.request.url);
e.respondWith(
(async () => {
try {
// First, try to use the navigation preload response if it's supported.
const preloadResponse = await e.preloadResponse;
if (preloadResponse) {
// console.log('returning preload response');
return preloadResponse;
}
const cachedResponse = await caches.match(e.request);
if (cachedResponse) {
// console.log(`[Service Worker] Fetching cached resource: ${e.request.url}`);
return cachedResponse;
}
// Always try the network first.
const networkResponse = await fetch(e.request);
return networkResponse;
} catch (error) {
// catch is only triggered if an exception is thrown, which is likely
// due to a network error.
// If fetch() returns a valid HTTP response with a response code in
// the 4xx or 5xx range, the catch() will NOT be called.
// console.log("Fetch failed; returning offline page instead.", error);
const cachedResponse = await caches.match('offline.html');
return cachedResponse;
}
})()
);
When offline, I open a url on my site, it loads the page from the cache but not all assets are cached for offline. So when a network request is made for, say https://www.gstatic.com/firebasejs/9.1.3/firebase-app.js, the response I get is the html of offline.html page. This breaks the page because of javascript errors.
It should instead return a network error or something.
I think the relevant sample code is from https://googlechrome.github.io/samples/service-worker/custom-offline-page/
self.addEventListener('fetch', (event) => {
// We only want to call event.respondWith() if this is a navigation request
// for an HTML page.
if (event.request.mode === 'navigate') {
event.respondWith((async () => {
try {
// First, try to use the navigation preload response if it's supported.
const preloadResponse = await event.preloadResponse;
if (preloadResponse) {
return preloadResponse;
}
const networkResponse = await fetch(event.request);
return networkResponse;
} catch (error) {
// catch is only triggered if an exception is thrown, which is likely
// due to a network error.
// If fetch() returns a valid HTTP response with a response code in
// the 4xx or 5xx range, the catch() will NOT be called.
console.log('Fetch failed; returning offline page instead.', error);
const cache = await caches.open(CACHE_NAME);
const cachedResponse = await cache.match(OFFLINE_URL);
return cachedResponse;
}
})());
}
// If our if() condition is false, then this fetch handler won't intercept the
// request. If there are any other fetch handlers registered, they will get a
// chance to call event.respondWith(). If no fetch handlers call
// event.respondWith(), the request will be handled by the browser as if there
// were no service worker involvement.
});
Specifically, that fetch handler checks to see whether event.request.mode === 'navigate' and only returns HTML when offline if that's the case. That's what's required to make sure that you don't end up returning offline HTML for other types of resources.

Get only HTML in single fetch request in service worker

I'm using Cloudflare service workers and I want on every request to:
request only the HTML (therefore count as only 1 request)
search the response for a string
Purge that page's cache if the message exists
I've solved points #2 and #3. Can't figure out if #1 is feasible or possible at all.
I need it as only one request because there is a limit per day on the number of free requests. Otherwise I have about 50-60 requests per page.
My current attempt for #1, which doesn't work right:
async function handleRequest(request) {
const init = {
headers: {
'content-type': 'text/html;charset=UTF-8',
},
};
const response = await fetch(request);
await fetch(request.url, init).then(function(response) {
response.text().then(function(text) {
console.log(text);
})
}).catch(function(err) {
// There was an error
console.warn('Something went wrong.', err);
});
return response;
}
addEventListener('fetch', event => {
return event.respondWith(handleRequest(event.request))
});
You can't request "only the html", the worker will act on any request that matches the route that it is deployed at. If you only care about the html, you will need to set up your worker path to filter to only the endpoints that you want to run the worker on.
Alternatively you can use the worker on every request and only do your logic if the response Content-Type is one that you care about. This would be something along these lines:
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
})
async function handleRequest(request) {
let response = await fetch(request);
let type = response.headers.get("Content-Type") || "";
if (type.startsWith("text/")) {
//this is where your custom logic goes
}
return response;
}

Javascript service worker: Fetch resource from cache, but also update it

I'm using a service worker on chrome to cache network responses. What I intend to do when a client requests a resource:
Check cache - If it exists, return from cache, but also send a request to server and update cache if file differs from the cached version.
If cache does not have it, send a request for it to the server and then cache the response.
Here's my current code for doing the same:
self.addEventListener('fetch', function (event) {
var requestURL = new URL(event.request.url);
var freshResource = fetch(event.request).then(function (response) {
if (response.ok && requestURL.origin === location.origin) {
// All good? Update the cache with the network response
caches.open(CACHE_NAME).then(function (cache) {
cache.put(event.request, response);
});
}
// Return the clone as the response would be consumed while caching it
return response.clone();
});
var cachedResource = caches.open(CACHE_NAME).then(function (cache) {
return cache.match(event.request);
});
event.respondWith(cachedResource.catch(function () {
return freshResource;
}));
});
This code does not work as it throws an error:
The FetchEvent for url resulted in a network error response: an object that was not a Response was passed to respondWith().
Can anyone point me in the right direction?
Okay, I fiddled with the code after people pointed out suggestions (thank you for that) and found a solution.
self.addEventListener('fetch', function (event) {
var requestURL = new URL(event.request.url);
var freshResource = fetch(event.request).then(function (response) {
var clonedResponse = response.clone();
// Don't update the cache with error pages!
if (response.ok) {
// All good? Update the cache with the network response
caches.open(CACHE_NAME).then(function (cache) {
cache.put(event.request, clonedResponse);
});
}
return response;
});
var cachedResource = caches.open(CACHE_NAME).then(function (cache) {
return cache.match(event.request).then(function(response) {
return response || freshResource;
});
}).catch(function (e) {
return freshResource;
});
event.respondWith(cachedResource);
});
The entire problem originated in the case where the item is not present in cache and cache.match returned an error. All I needed to do was fetch actual network response in that case (Notice return response || freshResource)
This answer was the Aha! moment for me (although the implementation is different):
Use ServiceWorker cache only when offline

Random Service Worker Response Error

I'm using Service Worker and Cache API to cache static resources but randomly http request to REST API endpoint (which is not being cached in SW) fails and the only message I have is xhr.statusText with text Service Worker Response Error and response code 500.
I can't tell if this error happens only with URLs that are not being cached or not. There is not enough evidence about either. This happens only in Chrome (50.0.2661.75 - 64b) and it works in Firefox 45
I wasn't able to reproduce it manually as it happens in Selenium tests and it appears to be random. Moreover it happens on localhost (where SW should work despite plain http) but also in domain with HTTPS that has self-signed certificate and as such SW should not even work there ...
Selenium tests are often refreshing pages and closing browser window but I have no idea if it matters.
Any ideas why it could be happening or how to get more information?
Update:
Service Worker code:
var VERSION = "sdlkhdfsdfu89q3473lja";
var CACHE_NAME = "cache" + VERSION;
var CACHE_PATTERN = /\.(js|html|css|png|gif|woff|ico)\?v=\S+?$/;
function fetchedFromNetwork(response, event) {
var cacheCopy = response.clone();
var url = event.request.url;
if (url.indexOf("/api/") === -1 // must not be a REST API call
&& url.indexOf(VERSION) > -1 // only versioned requests
&& VERSION !== "$CACHE_VERSION"
&& CACHE_PATTERN.test(url)) { //
caches.open(CACHE_NAME)
.then(function add(cache) {
cache.put(event.request, cacheCopy);
});
}
return response;
}
function unableToResolve() {
return new Response("Service Unavailable", {
status: 503,
statusText: "Service Unavailable",
headers: new Headers({
"Content-Type": "text/plain"
})
});
}
this.addEventListener("fetch", function (event) {
// cache GET only
if (event.request.method !== "GET") {
return;
}
event.respondWith(
caches.match(event.request)
.then(function (cached) {
if (cached) {
return cached;
} else {
return fetch(event.request)
.then(function (response) {
return fetchedFromNetwork(response, event);
}, unableToResolve)
.catch(unableToResolve);
}
}, function () { // in case caches.match throws error, simply fetch the request from network and rather don't cache it this time
return fetch(event.request);
}));
});
this.addEventListener("activate", function (event) {
event.waitUntil(
caches.keys()
.then(function (keys) {
return Promise.all(
keys.filter(function (key) {
// Filter out caches not matching current versioned name
return !key.startsWith(CACHE_NAME);
})
.map(function (key) {
// remove obsolete caches
return caches.delete(key);
}));
}));
});

Categories

Resources