What to change to prevent double request from service worker? - service-worker

Please do not mark as duplicate. This is not an exact duplicate of the other similar questions here on SO. It's more specific and fully reproducible.
Clone this repo.
yarn && yarn dev
Go to localhost:3000 and make sure under (F12)->Applications->Service workers, the service worker is installed.
Go to Network tab and refresh a few times(F5)
Observe how the network requests are doubled.
Example of what I see:
Or if you want to do it manually follow the instructions below:
yarn create-next-app app_name
cd app_name && yarn
in public folder, create file called service-worker.js and paste the following code:
addEventListener("install", (event) => {
self.skipWaiting();
console.log("Service worker installed!");
});
self.addEventListener("fetch", (event) => {
event.respondWith(
(async function () {
const promiseChain = fetch(event.request.clone()); // clone makes no difference
event.waitUntil(promiseChain); // makes no difference
return promiseChain;
})()
);
});
open pages/index.js and just below import Head from "next/head"; paste the following code:
if (typeof window !== "undefined" && "serviceWorker" in navigator) {
window.addEventListener("load", function () {
// there probably needs to be some check if sw is already registered
navigator.serviceWorker
.register("/service-worker.js", { scope: "/" })
.then(function (registration) {
console.log("SW registered: ", registration);
})
.catch(function (registrationError) {
console.log("SW registration failed: ", registrationError);
});
});
}
yarn dev
go to localhost:3000 and make sure the service worker has been loaded under (F12)Applications/Service Workers
Go to the Network tab and refresh the page a few times. See how the service worker sends two requests for each one
What do I need to change in the service-worker.js code so that there are no double requests?

This is how Chrome DevTools shows requests and is expected.
There is a request for a resource from the client JavaScript to the Service Worker and a request from the Service Worker to the server. This will always happen unless the service worker has the response cached and does not need to check the server for an update.

Does not seems the right way to initialize service worker in Next.js.You may need to look into next-pwa plugin to do it right.Here is the tutorial PWA with Next.js

If anyone is looking for an answer to the original question 'What to change to prevent double request from service worker?', specifically for network requests.
I've found a way to prevent it. Use the following in the serviceworker.js. (This also works for api calls etc.)
self.addEventListener('fetch', async function(event) {
await new Promise(function(res){setTimeout(function(){res("fetch request allowed")}, 9999)})
return false
});

Related

Fetch of the service worker doesn't seem to get triggered

When a browser requests an image from the server, the call is getting picked up by an API controller in the back end. There, a authorization check must be done before returning the image in order to check if the request is allowed or not.
So I need to add the authorization header and when searching for the best solution, I found this article: https://www.twelve21.io/how-to-access-images-securely-with-oauth-2-0/ and I was mostly intereseted in the solution number 4 which uses a Service Worker.
I made my own implementation, I registered a serviceWorker:
if ('serviceWorker' in navigator) {
console.log("serviceWorker active");
window.addEventListener('load', onLoad);
}
else {
console.log("serviceWorker not active");
}
function onLoad() {
console.log("onLoad is called");
var scope = {
scope: '/api/imagesgateway/'
};
navigator.serviceWorker.register('/Scripts/ServiceWorker/imageInterceptor.js', scope)
.then(registration => console.log("ServiceWorker registration successful with scope: ", registration.scope))
.catch(error => console.error("ServiceWorker registration failed: ", error));
}
and this is in my imageInterceptor:
self.addEventListener('fetch', event => {
console.log("fetch event triggered");
event.respondWith(
fetch(event.request, {
mode: 'cors',
credentials: 'include',
header: {
'Authorization': 'Bearer ...'
}
})
)
});
When I run my application, I see in my console that the registration seems to be successfully executed as I see the console.logs printed (ServiceWorker active, onLoad is called and successful registration with correct scope: https://localhost:44332/api/imagesgateway/
But when I load an image (https://localhost:44332/api/imagesgateway/...) via the gateway, I still get a 400 and when put a breakpoint on the backend I see that the authentication header is still null. Also, I don't see "fetch event triggered" message in my console. In another article it is stated that I can see the registered service workers via this setting: chrome://inspect/#service-workers but I don't see my worker there either.
My question is: Why isn't the authorization header added? Is it because, although the registration seems to go successfully, this isn't actually the case and therefore I don't see the worker in inspect#service-workers either?
You're not seeing fetch event triggered in the browser console because your Service Worker script isn't allowed to intercept the image requests. This is because your Service Worker script is located in a directory outside the scope of the requests you're interested in.
In order to intercept requests that handle resources at
/api/imagesgateway/
the SW script needs to be located in either
/, /api/, or /api/imagesgateway/. It cannot be located in /some/other/directory/service-worker.js.
This is the reason that your Service Worker registers successfully! There is no probelm in registering the SW. The problem lies in what it can do.
More info: Understanding Service Worker scope

reactjs service worker update

I have a PWA made by create-react-app.
I have service worker enabled as by default.
import registerServiceWorker from './registerServiceWorker'
import App from './App'
ReactDOM.render(
<App />
, document.getElementById('root'))
registerServiceWorker()
On every publish I change package version, but my service-worker doesn't update.
I have tried with function
import { unregister } from './registerServiceWorker';
unregister()
as described here https://github.com/facebook/create-react-app
And this
navigator.serviceWorker.getRegistrations()
.then(registrationsArray => {
if (registrationsArray.length > 0) {
registrationsArray[0].update()
}
})
It doesn't work, what is my mistake? What is wrong?
Thanks
Update to react-scripts ^3.2.0. Verify that you have the new version of serviceWorker.ts or .js. The old one was called registerServiceWorker.ts and the register function did not accept a configuration object. Here is the new one: https://github.com/facebook/create-react-app/blob/3190e4f4a99b8c54acb0993d92fec8a859889a28/packages/cra-template/template/src/serviceWorker.js
Note that this solution only works well if you are Not lazy-loading.
then in index.tsx:
serviceWorker.register({
onUpdate: registration => {
alert('New version available! Ready to update?');
if (registration && registration.waiting) {
registration.waiting.postMessage({ type: 'SKIP_WAITING' });
}
window.location.reload();
}
});
The latest version of the ServiceWorker.ts register()function accepts a config object with a callback function where we can handle upgrading. If we post a message SKIP_WAITING this tells the service worker to stop waiting and to go ahead and load the new content after the next refresh. In this example I am using a javascript alert to inform the user. Please replace this with a custom toast.
The reason this postMessage function works is because under the hood CRA is using workbox-webpack-plugin which includes a SKIP_WAITING listener.
More About Service Workers
good guide: https://redfin.engineering/how-to-fix-the-refresh-button-when-using-service-workers-a8e27af6df68
CRA issue discussing service worker cache: https://github.com/facebook/create-react-app/issues/5316
If you are not using CRA, you can use workbox directly: https://developers.google.com/web/tools/workbox

Service worker installs via localhost, but fails when deployed to GitHub pages

I am working on a PWA and I have installed and activated a service worker on my site. It works perfectly well while testing on local server but when I ship my code live, it fails.
This is my SW:
const cacheName = 'v1';
const cacheFiles = [
'/',
'/css/styles.css',
'/images/test1.png',
'/images/test2.png',
'/js/app.js',
'/js/sw-registration.js'
]
// Install event
self.addEventListener('install', function(event) {
console.log("SW installed");
event.waitUntil(
caches.open(cacheName)
.then(function(cache){
console.log('SW caching cachefiles');
return cache.addAll(cacheFiles);
})
)
});
// Activate event
self.addEventListener('activate', function(event) {
console.log("SW activated");
event.waitUntil(
caches.keys()
.then(function(cacheNames){
return Promise.all(cacheNames.map(function(thisCacheName){
if(thisCacheName !== cacheName){
console.log('SW Removing cached files from', thisCacheName);
return caches.delete(thisCacheName);
}
}))
})
)
});
// Fetch event
self.addEventListener('fetch', function(event) {
console.log("SW fetching", event.request.url);
event.respondWith(
caches.match(event.request)
.then(function(response){
console.log('Fetching new files');
return response || fetch(event.request);
})
);
});
This is the error I'm getting:
Uncaught (in promise) TypeError: Request failed (sw.js:1)
I don't understand why it fails to cache my files online (github pages) when it works locally. Can someone help me understand?
Thank you.
EDIT: I tried to deploy the site via Netlify and it works there. So it has to be something to do with Github pages. I would still like to know what it is, if anyone can shed any light.
As mentioned in Service Worker caches locally but fails online, when deploying to gh-pages, your web app's content will normally be accessed from a subpath, not in the top-level path, for the domain.
For instance, if your files are in the gh-pages branch of https://github.com/<user>/<repo>, then your web content can be accessed from https://<user>.github.io/<repo>/.
All of the URLs in your cacheFiles array are prefixed with /, which isn't what you want, given that all of your content is accessible under /<repo>/. For instance, / is interpreted as https://<user>.github.io/, which is different from https://<user>.github.io/<repo>/.
The solution to your problem, which will lead to a configuration that works regardless of what the base URL is for your hosting environment, is to prepend each of your URLs with ./ rather than /. For instance:
const cacheFiles = [
'./',
'./css/styles.css',
// etc.
];
The ./ means that the URL is relative, with the location of the service worker file used as the base. Your service worker file will be deployed under https://<user>.github.io/<repo>/, so that will end up being the correct base URL to use for the rest of your content as well.

Manifest start_url is not cached by a Service Worker

I'm using Lighthouse to audit my webapp. I'm working through the failures, but I'm stuck on this one:
Failures: Manifest start_url is not cached by a Service Worker.
In my manifest.json I have
"start_url": "index.html",
In my worker.js I am caching the following:
let CACHE_NAME = 'my-site-cache-v1';
let urlsToCache = [
'/',
'/scripts/app.js',
'/index.html'
];
Which lines up with what I see in the Application tab in Chrome Dev tools:
So... why is it telling me start_url is not cached?
Here is my full worker.js file:
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/worker.js').then(function(registration) {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}, function(err) {
console.log('ServiceWorker registration failed: ', err);
});
});
}
let CACHE_NAME = 'my-site-cache-v1.1';
let urlsToCache = [
'/',
'/scripts/app.js',
'/index.html'
];
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});
Let's look at Lighthouse's source code
static assessOfflineStartUrl(artifacts, result) {
const hasOfflineStartUrl = artifacts.StartUrl.statusCode === 200;
if (!hasOfflineStartUrl) {
result.failures.push('Manifest start_url is not cached by a service worker');
}
}
We can notice, that it's not checking your cache, but response of the entry point. The reason for that must be that your service worker is not sending proper Response on fetch.
You'll know that it's working, if in DevTools, in your first request, there'll be (from ServiceWorker) in size column:
There're two problems with the code you've provided:
First one is that you're messing service worker code with service worker registration code. Service worker registration code should be the code executed on your webpage.
That code should be included on your page:
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/worker.js').then(function(registration) {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}, function(err) {
console.log('ServiceWorker registration failed: ', err);
});
});
}
and the rest of what you've pasted should be your worker.js code. However service worker get installed, because you've files in cache, so I suspect you just pasted this incorrectly.
The second (real) problem is that service worker is not returning this cached files. As I've proved earlier, that error from lighthouse means that service worker is not returning start_url entry file.
The most basic code to achieve that is:
self.addEventListener('fetch', function(event) {
event.respondWith(caches.match(event.request));
});
Service worker is event-driven, so when your page wants to get some resource, service worker reacts, and serves the one from cache. In real world, you really don't want to use it like that, because you need some kind of fallback. I strongly recommend reading section Serving files from the cache here
Edit: I've created pull request in Lighthouse source code to clarify that error message
It seems to be that Chrome lighthouse (chrome v62) performs a generic fetch(). See discussion on https://github.com/GoogleChrome/lighthouse/issues/2688#issuecomment-315394447
In my case, an offline.html is served after an "if (event.request.mode === 'navigate'){".
Due to the use of lighthouse´s generic fetch(), lighthouse will not get served this offline.html, and shows the "Manifest start_url is not cached by a Service Worker" error.
I solved this problem by replacing:
if (event.request.mode === 'navigate'){
with
if (event.request.method === 'GET' ){

Service Worker and transparent cache updates

I am trying to install a ServiceWorker for a simple, yet old, Django web app. I started working with the example read-through caching example from the Chrome team
This works well but isn't ideal because I want to update the cache, if needed. There are two recommended ways to do this based on reading all the other service-worker answers here.
Use some server-side logic to know when the stuff you show has updated and then update your service worker to change what is precached. This is what sw-precache does, for example.
Just update the cache version in the service worker JS file (see comments in the JS file on the caching example above) whenever resources you depend on update.
Neither are great solutions for me. First, this is a dumb, legacy app. I don't have the application stack that sw-precache relies on. Second, someone else updates the data that will be shown (it is basically a list of things with a details page).
I wanted to try out the "use cache, but update the cache from network" that Jake Archibald suggested in his offline cookbook but I can't quite get it to work.
My original thinking was I should just be able to return the cached version in my service worker, but queue a function that would update the cache if the network is available. For example, something like this in the fetch event listener
// If there is an entry in cache, return it after queueing an update
console.log(' Found response in cache:', response);
setTimeout(function(request, cache){
fetch(request).then(function(response){
if (response.status < 400 && response.type == 'basic') {
console.log("putting a new response into cache");
cache.put(request, response);
}
})
},10, request.clone(), cache);
return response;
But this doesn't work. The page gets stuck loading.
whats wrong with the code above? Whats the right way to get to my target design?
It sounds like https://jakearchibald.com/2014/offline-cookbook/#stale-while-revalidate is very close to what you're looking for
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.open('mysite-dynamic').then(function(cache) {
return cache.match(event.request).then(function(response) {
var fetchPromise = fetch(event.request).then(function(networkResponse) {
// if we got a response from the cache, update the cache
if (response) {
cache.put(event.request, networkResponse.clone());
}
return networkResponse;
});
// respond from the cache, or the network
return response || fetchPromise;
});
})
);
});
On page reload you can refresh your service worker with new version meanwhile old one will take care of request.
Once everything is done and no page is using old service worker, It will using newer version of service worker.
this.addEventListener('fetch', function(event){
event.responseWith(
caches.match(event.request).then(function(response){
return response || fetch(event.request).then(function(resp){
return caches.open('v1').then(function(cache){
cache.put(event.request, resp.clone());
return resp;
})
}).catch(function() {
return caches.match('/sw/images/myLittleVader.jpg');
});
})
)
});
I recommend you to walk through below link for detailed functionality
https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers

Resources