ServiceWorker not intercepting calls immediately after installation - service-worker

I am playing around with ServiceWorkers, and I noticed that even after a successful registration, the service worker is not intercepting calls
my sw.js:
self.addEventListener('install', (event) => {
console.log('install')
})
self.addEventListener('activate', (event) => {
console.log('activate')
})
self.addEventListener('fetch', (event) => {
console.log('fetch')
})
in my index.html:
...
<head>
...
<script>
navigator.serviceWorker.register('./sw.js', { scope: './' }).then(() => {
// just delay it so we're sure sw is active before we load that script
setTimeout(() => {
console.log('load jsx script to be intercepted')
const script = document.createElement('script')
script.setAttribute('src', 'test.js')
document.head.appendChild(script)
}, 2000)
})
</script>
...
</head>
...
Result:
My console output from the first page load, when service worker is not yet installed, is then (in that order):
install
activate
load jsx script to be intercepted
Uncaught SyntaxError: Unexpected token '<'
The syntax error is because I'm trying to load a JSX script, and I would like to intercept that loading in the service worker, to compile it first before it gets executed.
But on the initial load, the service worker seems to not intercept, as I don't see the log output 'fetch', although I clearly see, when I attempt to load the jsx script, by that time the service worker is already active.
When I now reload the page, I get following console output:
fetch (<-- that's the loading of the index.html itself)
load jsx script that should be intercepted
fetch (<-- that's the loading of the test.js)
So from then on, things work out as planned, but not on the initial load. Why is that the case?

After some research it seems I was able to solve the problem using the related post here, plus the documentation of the "claim" api.
So if I add the following to my 'active' handler, things seem to work:
self.addEventListener('activate', (event) => {
event.waitUntil(self.clients.claim())
I would appreciate any feedback on whether this is indeed the right way to go about it :-)

Related

Service Worker: serviceWorker.ready + event.waitUntil not orchestrating as expected

I have a service worker that intercepts calls to compile jsx if needed, and cache the result, and want my website to bootstrap only once the service worker got active.
Looking at the documentation I found the navigator.serviceWorker.ready property, which is a promise that resolves once the worker got active.
For some reason, the promise resolves before the activation should really be finished. Here is my code:
index.html
<!DOCTYPE html>
<html>
<head>
<script type="application/javascript">
navigator.serviceWorker.register('./sw.js', { scope: './' }).then((reg) => {
console.log('registered successfully')
}).catch(console.error)
navigator.serviceWorker.ready.then((registration) => {
console.log('worker active, bootstrapping')
// ... bootstrapping the app now (loading jsx scripts etc.)
})
</script>
...
</head>
....
</html>
sw.js
self.addEventListener('activate', (event) => {
console.log('activating')
event.waitUntil(self.clients.claim().then(() => console.log('active')))
})
Resulting log output:
registered successfully
activating
worker active, bootstrapping
active
Consequently, loading the app scripts as part of the bootstrapping will not be reliably intercepted by the service worker at the beginning, which will make the app fail.
I cleared the entire site data in Chrome including unregistering existing service workers, before I loaded the page.
Am I using the APIs wrong, e.g. misunderstanding the waitUntil?

What to change to prevent double request from 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
});

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

service worker install event is called before register event is completed

I have attached install event to service worker as below. But Install event fired before register event is completed. See code snippets and console logs below.
My concern is how install event is fired before register event is completed?
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./service-worker.js',{scope : '/'}).then(function(registration) {
// Registration was successful
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}).catch(function(err) {
// registration failed :(
console.log('ServiceWorker registration failed: ', err);
});
}
var cacheName = 'V-1';
var filesToCache = [
'/', '/index.html',
'/css/all.css', '/css/material.min.css',
'/js/all.js', '/js/material.min.js',
'/images/service-worker-1.png','/images/service-worker-2.png','/images/service-worker-3.png',
];
self.addEventListener('install', function(e) {
console.log('[ServiceWorker] Installing');
e.waitUntil(
caches.open(cacheName).then(function(cache) {
console.log('[ServiceWorker] Caching app shell');
return cache
.addAll(filesToCache) //this is atomic in nature i.e. if any of the file fails the entire cache step fails.
.then(() => {console.log('[ServiceWorker] App shell Caching Successful');})
.catch(() => {console.log('[ServiceWorker] App shell Caching Failed');})
})
);
});
navigator.serviceWorker.register() is not an event. It's a function that returns a promise, and then promise will resolve with a ServiceWorkerRegistration object that corresponds to the registration.
The actual service worker logic is executed in a different thread/process, and the lifecycle events that the service worker handles, like the install event, happen independently of the web page that registered the service worker. What you're seeing in your console.log() output is expected.
If you want to keep track of the state of the service worker from your web page, you can add event listeners to the ServiceWorkerRegistration object. There's an example of this at https://googlechrome.github.io/samples/service-worker/registration-events/index.html
If you want to write code that will cause your web page to wait until there's an active service worker before it takes some action, you could make use of the navigator.serviceWorker.ready promise.

Best practices for calling intuit.ipp.anywhere.setup()?

This is a question about best practices for making the JavaScript call that generates the standard "Connect to QuickBooks" button (for establishing a connection to QuickBooks Harmony via Intuit's v3 REST API).
If I follow Intuit's example, I would:
Reference https://appcenter.intuit.com/Content/IA/intuit.ipp.anywhere.js in a script tag.
Place the <ipp:connectToIntuit></ipp:connectToIntuit> tagset where I want the "Connect to QuickBooks" button to display
Cross my fingers and hope that intuit.ipp.anywhere.js isn't redirecting to a downtime message, again still exists
Make my call to intuit.ipp.anywhere.setup()
See the "Connect to QuickBooks" button
... which works (for many values of "works"), but feels pretty fragile:
If intuit.ipp.anywhere.js is redirecting to a downtime message (read: not JavaScript) or is otherwise unavailable, I'll get a script error.
If I get a script error (or something else goes wrong with Intuit's copy of the script), there isn't any feedback to the user, just a blank space where the "Connect to QuickBooks" button should be.
To make this all a little more resilient, I'm combining the reference to intuit.ipp.anywhere.js and the call to intuit.ipp.anywhere.setup() into a JQuery .ajax() call:
$.ajax({
url: 'https://appcenter.intuit.com/Content/IA/intuit.ipp.anywhere.js',
type: 'GET',
dataType: 'script',
timeout: 4000,
success: function(response) {
if (typeof intuit !== 'undefined') {
intuit.ipp.anywhere.setup({
menuProxy: 'MYMENUPROXYURL.aspx',
grantUrl: 'MYGRANTURL.aspx'
});
}
},
error: function(x, t, m) {
// show some friendly error message about Intuit downtime
}
});
... which also works (for a few more values of "works"):
My call to setup() is wrapped inside the success handler (and an additional check on the existence of the intuit Object), so I shouldn't get a script error if things go wrong.
If the GET of Intuit's script times out (after 4000ms) or returns something that isn't script, I'll show a friendly error message to the user.
Has anyone else taken a different approach?
And is Intuit back online?
That's similar to how we've handled it. We had wrapped it in jQuery.getScript call, but apparently the .fail handler doesn't work with cross domain script tags. Our solution is as follows:
<script type="text/javascript>
var timeoutID;
timeoutID = window.setTimeout(function () {
$("#ippConnectToIntuit").replaceWith('<p class="error-message">There was a problem communicating with QuickBooks. The service may be down or in heavy use. Try again later.</p>');
}, 5000);
$.getScript("https://appcenter.intuit.com/Content/IA/intuit.ipp.anywhere.js")
.done(function () {
window.clearTimeout(timeoutID);
intuit.ipp.anywhere.setup({
menuProxy: '/path/to/our/menu/proxy',
grantUrl: '/path/to/our/grant/url'
});
});
</script>
<div id="ippConnectToIntuit"><ipp:connecttointuit></ipp:connecttointuit></div>

Resources