How to prevent fetch() to ignore offline pages? - javascript

The JavaScript code below retrieves some texts from server by using Fetch API.
fetch("index.php?user_id=1234", {
method: "GET"
}).then(function(response) {
return response.text();
}).then(function(output) {
document.getElementById("output").innerHTML = output;
});
But during network errors, it retrieves the offline page (offline.html) due to service-worker.
"use strict";
self.addEventListener("install", function() {
self.skipWaiting();
});
self.addEventListener("activate", function(activation) {
activation.waitUntil(
caches.keys().then(function(cache_names) {
for (let cache_name of cache_names) {
caches.delete(cache_name);
};
caches.open("client_cache").then(function(cache) {
return cache.add("offline.html");
});
})
);
});
self.addEventListener("fetch", function(fetching) {
fetching.respondWith(
caches.match(fetching.request).then(function(cached_response) {
return cached_response || fetch(fetching.request);
}).catch(function() {
return caches.match("offline.html");
})
);
});
I want to let the fetch request know about the network error.
And I do not want to use window.navigator. So, what can I do?
(I prefer vanilla solutions.)

You should structure your service worker's fetch event handler so that it only returns offline.html when there's a network error/cache miss and the original request is for a navigation. If the original request is not a navigation, then responding with offline.html is (as you've seen) going to result in getting back HTML for every failure.
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
return cachedResponse || fetch(event.request);
}).catch((error) => {
if (event.request.mode === 'navigate') {
return caches.match('offline.html');
}
throw error;
})
);
});

Related

Crossdomain ServiceWorker load balancing

I'm trying to implement smth like crossdomain load balancing with ServiceWorker API.
My concept is:
After install on every request on fetch event I try to access main domain (https://example.com/)
If success I should return this to user with like event.respondWith(__response);
If failed (timed out or any other exception) I make CORS request to other server (https://balancer.com/) which returns other accessible domain (https://mirror1.example.com) and browser is redirected;
And I'm stucked on redirection step(((
So my current code is here
self.oninstall = function (event) {
event.waitUntil(self.skipWaiting());
};
self.onactivate = function (event) {
event.waitUntil(self.clients.claim());
};
self.initialUrl = false;
self.onfetch = async function (event) {
if (!self.initialUrl)
self.initialUrl = event.request.url;
if (self.initialUrl) {
event.respondWith(self.tryAccess(event))
} else {
event.respondWith(fetch(event.request));
}
};
self.tryAccess = async (event) => {
return new Promise(async (resolve, reject) => {
self.clients
.matchAll({type: 'window'})
.then(async (clients) => {
for (var i in clients) {
var _c = clients[0];
if (_c.url === event.request.url) {
try {
let __tryResponse = await fetch(event.request);
resolve(__tryResponse);
return;
} catch (e) {
let __json = await (await fetch("https://balancer.com/")).json();
return _c.navigate(__json.path).then(client => client.focus());
}
} else {
resolve();
}
}
});
});
};
Getting a reference to a WindowClient and forcibly changing its URL from inside of a fetch handler isn't the right way to redirect.
Instead, inside of your fetch handler, you can respond with a redirection response created by Response.redirect(). From the perspective of the browser, this will be treated just like any other redirection that might have originated from the server.
One thing to note is that if you initially request a subresource via a same-origin URL that results in a redirect to a cross-origin response, you might run into some issues. If your original requests are for cross-origin URLs and your potential redirects are also to cross-origin URLs, I think you'll be fine.
self.addEventListener('fetch', (event) => {
const fetchWithRedirection = async () => {
try {
// Use fetch(), caches.match(), etc. to get a response.
const response = await ...;
// Optional: also check response.ok, but that
// will always be false for opaque responses.
if (response) {
return response;
}
// If we don't have a valid response, trigger catch().
throw new Error('Unable to get a response.');
} catch (error) {
// Use whatever logic you need to get the redirection URL.
const redirectionURL = await ...;
if (redirectionURL) {
// HTTP 302 indicates a temporary redirect.
return Response.redirect(redirectionURL, 302);
}
// If we get to this point, redirection isn't possible,
// so just trigger a NetworkError.
throw error;
}
};
// You will probably want to wrap this in an if() to ensure
// that it's a request that you want to handle with redirection.
if (/* some routing criteria */) {
event.respondWith(fetchWithRedirection());
} else {
// Optionally use different response generation logic.
// Or just don't call event.respondWith(), and the
// browser will proceed without service worker involvement.
}
});

How to make service worker network first then cache

I am trying to set up pwa app. My sw.js is
var urlsToCache = [
'/danger/index.html',
'/danger/css/main.css',
'/danger/css/fontawesome-all.min.css',
'/danger/css/font.css'
];
var CACHE_NAME = 'progressive';
self.addEventListener('install', function (event) {
event.waitUntil(
caches.open(CACHE_NAME)
.then(function (cache) {
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener('fetch', function (event) {
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;
});
});
})
);
});
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.open(CACHE_NAME).then(function (cache) {
return fetch(event.request).then(function (response) {
cache.put(event.request, response.clone());
return response;
});
})
);
});
I want to make sw check for first network, then cache. I got a code from net.
self.addEventListener('fetch', (event) => {
event.respondWith(async function() {
try {
return await fetch(event.request);
} catch (err) {
return caches.match(event.request);
}
}());
});
How to implement it in my sw.js. I asked it because my app not updating the website content in network enabled mode also.
Browser will always reach out to network first and get the service-worker.js I would suggest you to use Workbox instead of manually writing all these code.
Workbox documentation
We should not cache service-worker file as that is a source of truth for browser to know whether any other files have changed. Usually a service-worker file will have a series of files that are cached along with their hashes. When service worker file changes, then browser knows that it needs to take update from server.
Thanks

Service worker returning correct response but still getting 'no internet' error

I am trying to setup a service worker so that my web-app can fully work offline. When I inspect the cache for static-v2 all of my assets that I want are there.
When I make a request and am online my SW is correctly doing the fetch and not falling into my catch statement.
However, when I am offline and I fall through to my cache the correct response is logged with the status of 200 but I get the chrome 'no internet' error.
Another interesting point is even when I comment out the line where I return the cache response and just return the hard coded response I still get the 'no internet' error.
Any help in figuring out why my service worker isn't working as expected would be greatly appreciated.
const CACHENAME = `static-v2`;
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(CACHENAME).then(function(cache) {
return cache
.addAll([
// your list of cache keys to store in cache
'bundle.js',
'index.html',
'manifest.json',
// etc.
])
.then(() => {
return self.skipWaiting();
});
}),
);
});
self.onactivate = (evt) => {
console.log(`on activate - ${CACHENAME}`);
evt.waitUntil(
caches.keys().then((cacheNames) => {
const deleteOldCaches = cacheNames.map((cacheName) => {
console.log(cacheName);
if (cacheName != CACHENAME) {
return caches.delete(cacheName);
}
return Promise.resolve();
});
return Promise.all(deleteOldCaches);
}),
);
self.clients.claim();
};
self.onfetch = (evt) => {
evt.waitUntil(
fetch(evt.request).catch((err) => {
caches.match(evt.request).then((response) => {
console.log(evt.request.url, response);
if (response) return response;
return new Response('<div><h2>Uh oh that did not work</h2></div>', {
headers: {
'Content-type': 'text/html',
},
});
});
}),
);
console.log(`on fetch - ${CACHENAME}`);
};
I am hosting all of my assets (inc SW) using http-server (https://www.npmjs.com/package/http-server)
It's a simple mistake.
In your fetch event handler, you should NOT call evt.waitUntil. You should call evt.respondWith.

How do I notify my app that it's receiving cached responses from a service worker?

I want to use a service worker to cache responses that can be used when the user is either offline or the my app's backend is down. For user experience reasons, I'd like to show a notification to the user that the app's backend currently can't be reached and that cached content is being served instead. What's the best way to do that? I can add a header to the response in the service worker but I'm not sure that's the "right way"... it seems like there should be a more straight-forward pattern. This is my service worker code:
self.addEventListener('fetch', event => {
console.log(`fetch event`, event);
event.respondWith(doFetch(event.request));
});
// fetch from network, fallback to cache
function doFetch(request) {
return fetch(request)
.then(response => {
return caches.open(CACHE)
.then(cache => {
cache.put(request, response.clone());
return response;
})
})
.catch(error => {
console.warn(`fetch to ${request.url} failed`, error);
return fromCache(request);
});
}
function fromCache(request) {
return caches.open(CACHE)
.then(cache => cache.match(request))
.then(match => {
if (match) {
// response.clone doesn't work here because I need to modify it
return cloneResponse(match);
} else {
throw new Error(`no match for ${request.url}`);
}
});
}
// this clones a response in a way that let's me modify it
function cloneResponse(response) {
let init = {
status: response.status,
statusText: response.statusText,
headers: { 'X-From-SW-Cache': 'true' }
};
response.headers.forEach((v, k) => {
init.headers[k] = v;
});
return response.text().then((body) => new Response(body, init));
}
Adding a header to the response is definitely one option.
Another option to consider is to use postMessage() to send a message to the client page letting it know that a cached response is being used. You could extend that logic to also send a message to the client letting it know when a network response is used, if you want to toggle some UI element once you're back to using the network.
Here's the code to accomplish that in your service worker:
async function postToClient(clientId, message) {
const client = await clients.get(clientId);
client.postMessage(message);
}
async function responseLogic(event) {
const cache = await caches.open('cache-name');
try {
const response = await fetch(event.request);
await cache.put(event.request, response.clone());
event.waitUntil(postToClient(event.request.clientId, 'FROM_NETWORK'));
return response;
} catch (error) {
const cachedResponse = await cache.match(event.request);
if (cachedResponse) {
event.waitUntil(postToClient(event.request.clientId, 'FROM_CACHE'));
return cachedResponse;
}
throw error;
}
}
self.addEventListener('fetch', (event) => {
// Check to see if this is a request for your API:
if (event.request.url === 'https://example.com/api') {
event.respondWith(responseLogic(event));
}
});
And then on your client page, you can register a message listener to respond to those messages from the service worker by showing or hiding some UI element, depending on whether the last response came from the network or cache:
navigator.serviceWorker.addEventListener('message', (event) => {
if (event.data === 'FROM_NETWORK') {
// Do something in your UI.
} else if (event.data === 'FROM_CACHE') {
// Do something in your UI.
}
});

Service worker unregistered when offline

So I've created and successfully registered a Service Worker when the browser is online. I can see that the resources are properly cached using the DevTools. The issue is when I switch to offline mode, the service worker seems to unregister itself and, as such, nothing but the google chrome offline page is displayed.
The code.
'use strict';
var CACHE_NAME = 'v1';
var urlsToCache = [
'/'
];
self.addEventListener('install', function(event) {
// Perform install steps
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
// Cache hit - return response
if (response) {
return response;
}
// IMPORTANT: Clone the request. A request is a stream and
// can only be consumed once. Since we are consuming this
// once by cache and once by the browser for fetch, we need
// to clone the response.
var fetchRequest = event.request.clone();
return fetch(fetchRequest).then(
function(response) {
// Check if we received a valid response
if(!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// IMPORTANT: Clone the response. A response is a stream
// and because we want the browser to consume the response
// as well as the cache consuming the response, we need
// to clone it so we have two streams.
var responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(function(cache) {
cache.put(event.request, responseToCache);
});
return response;
}
);
})
);
});
And the test script, if that helps.
'use strict';
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').then(function(registration) {
// Registration was successful
console.log('ServiceWorker registration successful with scope: ', registration.scope);
var serviceWorker;
if (registration.installing) {
serviceWorker = registration.installing;
} else if (registration.waiting) {
serviceWorker = registration.waiting;
} else if (registration.active) {
serviceWorker = registration.active;
}
if (serviceWorker) {
console.log('ServiceWorker phase:', serviceWorker.state);
serviceWorker.addEventListener('statechange', function (e) {
console.log('ServiceWorker phase:', e.target.state);
});
}
}).catch(function(err) {
// registration failed :(
console.log('ServiceWorker registration failed: ', err);
});
}
Edit: Checking the console I've found this error. sw.js:1 An unknown error occurred when fetching the script.
Aslo, as per a suggestion, I've added this code yet the problem persists.
this.addEventListener('activate', function(event) {
var cacheWhitelist = ['v2'];
event.waitUntil(
caches.keys().then(function(keyList) {
return Promise.all(keyList.map(function(key) {
if (cacheWhitelist.indexOf(key) === -1) {
return caches.delete(key);
}
}));
})
);
});
it seems you haven't added any activate event which meant to render cached elements when available. Hope the code help you.
self.addEventListener('activate', function(e) {
/*service worker activated */
e.waitUntil(
caches.key().then(function(keyList) {
return Promise.all(keyList.map(function(key) {
if(key){
//remove old cache stuffs
return caches.delete(key);
}
}));
})
);
});
The problem appears to have fixed itself. To anyone here from google, try restarting the browser.

Categories