Should service worker script be loaded first? - service-worker

To initialize a Service Worker one has to include in thead <head> something like:
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/my-sw.js');
}
</script>
I found that in some example pages this isn't the first script (e.g. flipkart.com) of the page. Shouldn't it be the first one so that pre-caching and other features can be used ASAP? Does or doesn't the order actually matter for this script and it could well be at the bottom of the page outside </head>?

Registering a service worker will cause the browser to start the service worker install step in the background. If successful, the service worker is executed in a ServiceWorkerGlobalScope; this is basically a special kind of worker context, running off the main script execution thread, with no DOM access.
Being specific with your question, The order of service worker registration script doesn't matter, As an after a service worker is installed and the user navigates to a different page or refreshes, the service worker will begin to receive fetch events.

Related

Service worker will not intercept fetches

I am serving my service worker from /worker.js and want it to intercept fetches to /localzip/*, but the fetch event is never fired.
I register it like this:
navigator.serviceWorker.register(
"worker.js",
{ scope: "/localzip/" }
);
And I claim all clients when it activates, so that I can start intercepting fetches from the current page immediately. I am sure that the service worker is activating and that clients.claim() is succeeding.
self.addEventListener("activate", (e) => {
// Important! Start processing fetches for all clients immediately.
//
// MDN: "When a service worker is initially registered, pages won't use it
// until they next load. The claim() method causes those pages to be
// controlled immediately."
e.waitUntil(clients.claim());
});
Chrome seems happy with it and the scope appears correct:
My fetch event handler is very simple:
self.addEventListener("fetch", (e) => {
console.log("Trying to make fetch happen!");
});
From my application, after the worker is active, I try to make a request, e.g.,
const response = await fetch("/localzip/lol.jpg");
The fetch does not appear to trigger the above event handler, and the browser instead tries to make a request directly to the server and logs GET http://localhost:3000/localzip/lol.jpg 404 (Not Found).
I have tried:
Making sure the latest version of my worker code is running.
Disabling / clearing caches to make sure the fetch isn't being handled by the browser's cache.
Hosting from an HTTPS server. (Chrome is supposed to support service workers on plaintext localhost for development.)
What more does it want from me?
Live demo: https://rgov.github.io/service-worker-zip-experiment/
Note that the scope is slightly different, and the fetch is performed by creating an <img> tag.
First, let's confirm you are not using hard-reload while testing your code. If you use hard-reload, all requests will not go through the service worker.
See https://web.dev/service-worker-lifecycle/#shift-reload
I also checked chrome://serviceworker-internals/ in Chrome, and your service worker has fetch handler.
Then, let's check the codes in detail.
After trying your demo page, I found a network request is handled by the service worker after clicking "Display image from zip archive" button since I can see this log:
Service Worker: Serving archive/maeby.jpg from zip archive
Then, the error is thrown:
Failed to load ‘https://rgov.github.io/localzip/maeby.jpg’. A ServiceWorker passed a promise to FetchEvent.respondWith() that rejected with ‘TypeError: db is undefined’.
This is caused by db object is not initialized properly. It would be worth confirming whether you see the DB related issue as I see in your demo. If not, my following statement might be incorrect.
I try to explain some service worker mechanism alongside my understanding of your code:
Timing of install handler
Your DB open code happens in the install handler only. This means DB object will be assigned only when the install handler is executed.
Please notice the install handler will be executed only when it's necessary. If a service worker exists already and does not need to update, the install handler won't be called. Hence, the db object in your code might not be always available.
Stop/Start Status
When the service worker does not handle events for a while (how long it would be is by browser's design), the service worker will go to stop/idle state.
When the service worker is stopped/idle (you can check the state in the devtools) and started again, the global db object will be undefined.
In my understanding, this is why I see the error TypeError: db is undefined’.
Whenever the service worker wakes up, the whole worker script will be executed. However, the execution of event handlers will depend on whether the events are coming.
How to prevent stop/idle for debugging?
Open your devtools for the page then the browser will keep it being alive.
Once you close the devtool, the service worker might go to "stop" soon.
Why does the service worker stops?
The service worker is designed for handling requests. If no request/event should be handled by a service worker, the service worker thread is not necessary to run for saving resources.
Please notice both fetch and message events (but not limited to) will awake the service worker.
See Prevent Service Worker from automatically stopping
Solution for the demo page
If the error is from the DB, this means the getFromZip function should open the DB when db is unavailable.
By the way, even without any change, your demo code works well in the following steps:
As a user, I open the demo page at the first time. (This is for ensuring that the install handler is called.)
I open the devtools ASAP once I see the page content. (This is for preventing the service worker goes to "stop" state)
I click "Download zip archive to IndexedDB" button.
I click "Display image from zip archive" button.
Then I can see the image is shown properly.
Jake Archibald pointed out that I was confused about the meaning of the service worker's scope, explained here:
We call pages, workers, and shared workers clients. Your service worker can only control clients that are in-scope. Once a client is "controlled", its fetches go through the in-scope service worker.
In other words:
The scope affects which pages (clients) get placed under the service worker's control and not which fetched URLs get intercepted.

New build deployed in a domain (https://example.com) is not getting reflected as the previous build has a service worker running [duplicate]

I'm playing with the service worker API in my computer so I can grasp how can I benefit from it in my real world apps.
I came across a weird situation where I registered a service worker which intercepts fetch event so it can check its cache for requested content before sending a request to the origin.
The problem is that this code has an error which prevented the function from making the request, so my page is left blank; nothing happens.
As the service worker has been registered, the second time I load the page it intercepts the very first request (the one which loads the HTML). Because I have this bug, that fetch event fails, it never requests the HTML and all I see its a blank page.
In this situation, the only way I know to remove the bad service worker script is through chrome://serviceworker-internals/ console.
If this error gets to a live website, which is the best way to solve it?
Thanks!
I wanted to expand on some of the other answers here, and approach this from the point of view of "what strategies can I use when rolling out a service worker to production to ensure that I can make any needed changes"? Those changes might include fixing any minor bugs that you discover in production, or it might (but hopefully doesn't) include neutralizing the service worker due to an insurmountable bug—a so called "kill switch".
For the purposes of this answer, let's assume you call
navigator.serviceWorker.register('service-worker.js');
on your pages, meaning your service worker JavaScript resource is service-worker.js. (See below if you're not sure the exact service worker URL that was used—perhaps because you added a hash or versioning info to the service worker script.)
The question boils down to how you go about resolving the initial issue in your service-worker.js code. If it's a small bug fix, then you can obviously just make the change and redeploy your service-worker.js to your hosting environment. If there's no obvious bug fix, and you don't want to leave your users running the buggy service worker code while you take the time to work out a solution, it's a good idea to keep a simple, no-op service-worker.js handy, like the following:
// A simple, no-op service worker that takes immediate control.
self.addEventListener('install', () => {
// Skip over the "waiting" lifecycle state, to ensure that our
// new service worker is activated immediately, even if there's
// another tab open controlled by our older service worker code.
self.skipWaiting();
});
/*
self.addEventListener('activate', () => {
// Optional: Get a list of all the current open windows/tabs under
// our service worker's control, and force them to reload.
// This can "unbreak" any open windows/tabs as soon as the new
// service worker activates, rather than users having to manually reload.
self.clients.matchAll({type: 'window'}).then(windowClients => {
windowClients.forEach(windowClient => {
windowClient.navigate(windowClient.url);
});
});
});
*/
That should be all your no-op service-worker.js needs to contain. Because there's no fetch handler registered, all navigation and resource requests from controlled pages will end up going directly against the network, effectively giving you the same behavior you'd get without if there were no service worker at all.
Additional steps
It's possible to go further, and forcibly delete everything stored using the Cache Storage API, or to explicitly unregister the service worker entirely. For most common cases, that's probably going to be overkill, and following the above recommendations should be sufficient to get you in a state where your current users get the expected behavior, and you're ready to redeploy updates once you've fixed your bugs. There is some degree of overhead involved with starting up even a no-op service worker, so you can go the route of unregistering the service worker if you have no plans to redeploy meaningful service worker code.
If you're already in a situation in which you're serving service-worker.js with HTTP caching directives giving it a lifetime that's longer than your users can wait for, keep in mind that a Shift + Reload on desktop browsers will force the page to reload outside of service worker control. Not every user will know how to do this, and it's not possible on mobile devices, though. So don't rely on Shift + Reload as a viable rollback plan.
What if you don't know the service worker URL?
The information above assumes that you know what the service worker URL is—service-worker.js, sw.js, or something else that's effectively constant. But what if you included some sort of versioning or hash information in your service worker script, like service-worker.abcd1234.js?
First of all, try to avoid this in the future—it's against best practices. But if you've already deployed a number of versioned service worker URLs already and you need to disable things for all users, regardless of which URL they might have registered, there is a way out.
Every time a browser makes a request for a service worker script, regardless of whether it's an initial registration or an update check, it will set an HTTP request header called Service-Worker:.
Assuming you have full control over your backend HTTP server, you can check incoming requests for the presence of this Service-Worker: header, and always respond with your no-op service worker script response, regardless of what the request URL is.
The specifics of configuring your web server to do this will vary from server to server.
The Clear-Site-Data: response header
A final note: some browsers will automatically clear out specific data and potentially unregister service workers when a special HTTP response header is returned as part of any response: Clear-Site-Data:.
Setting this header can be helpful when trying to recover from a bad service worker deployment, and kill-switch scenarios are included in the feature's specification as an example use case.
It's important to check the browser support story for Clear-Site-Data: before your rely solely on it as a kill-switch. As of July 2019, it's not supported in 100% of the browsers that support service workers, so at the moment, it's safest to use Clear-Site-Data: along with the techniques mentioned above if you're concerned about recovering from a faulty service worker in all browsers.
You can 'unregister' the service worker using javascript.
Here is an example:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistrations().then(function (registrations) {
//returns installed service workers
if (registrations.length) {
for(let registration of registrations) {
registration.unregister();
}
}
});
}
That's a really nasty situation, that hopefully won't happen to you in production.
In that case, if you don't want to go through the developer tools of the different browsers, chrome://serviceworker-internals/ for blink based browsers, or about:serviceworkers (about:debugging#workers in the future) in Firefox, there are two things that come to my mind:
Use the serviceworker update mechanism. Your user agent will check if there is any change on the worker registered, will fetch it and will go through the activate phase again. So potentially you can change the serviceworker script, fix (purge caches, etc) any weird situation and continue working. The only downside is you will need to wait until the browser updates the worker that could be 1 day.
Add some kind of kill switch to your worker. Having a special url where you can point users to visit that can restore the status of your caches, etc.
I'm not sure if clearing your browser data will remove the worker, so that could be another option.
I haven't tested this, but there is an unregister() and an update() method on the ServiceWorkerRegistration object. you can get this from the navigator.serviceWorker.
navigator.serviceWorker.getRegistration('/').then(function(registration) {
registration.update();
});
update should then immediately check if there is a new serviceworker and if so install it. This bypasses the 24 hour waiting period and will download the serviceworker.js every time this javascript is encountered.
For live situations you need to alter the service worker at byte-level (put a comment on the first line, for instance) and it will be updated in the next 24 hours. You can emulate this with the chrome://serviceworker-internals/ in Chrome by clicking on Update button.
This should work even for situations when the service worker itself got cached as the step 9 of the update algorithm set a flag to bypass the service worker.
We had moved a site from godaddy.com to a regular WordPress install. Client (not us) had a serviceworker file (sw.js) cached into all their browsers which completely messed things up. Our site, a normal WordPress site, has no service workers.
It's like a virus, in that it's on every page, it does not come from our server and there is no way to get rid of it easily.
We made a new empty file called sw.js on the root of the server, then added the following to every page on the site.
<script>
if (navigator && navigator.serviceWorker && navigator.serviceWorker.getRegistration) {
navigator.serviceWorker.getRegistration('/').then(function(registration) {
if (registration) {
registration.update();
registration.unregister();
}
});
}
</script>
In case it helps someone else, I was trying to kill off service workers that were running in browsers that had hit a production site that used to register them.
I solved it by publishing a service-worker.js that contained just this:
self.globalThis.registration.unregister();

How to manually terminate a running Service Worker

I have developed a somewhat stateful Service Worker (which is admittedly not the best practice) and I have a need to manually terminate it in order to reset it.
The need arises when I am changing the user within my client application. As such I don't close the browser tab though I do however navigate outside of the Service Worker's scope (within the same site).
I cannot afford to wait the ~60 seconds or so before the Service Worker automatically terminates (having navigated outside of its scope or closed the client tab).
I cannot close the client browser tab (this is not a good user experience in my scenario).
I am not looking to uninstall/unregister the service worker.
It seems like the ServiceWorkerGlobalScope does not inherit the close() method from WorkerGlobalScope.
It seems like navigator.serviceWorker does not inherit terminate() method from WebWorker.
How can I manually stop the Service Worker from running and to clear its state?
you can terminate a service worker with this:
navigator.serviceWorker
.getRegistration('/miSwScope')
.then(function(registration) {
if (registration) {
registration.unregister();
}
});
or clear all:
navigator.serviceWorker.getRegistrations().then(function(regs) {
regs.forEach(reg => {
reg.unregister();
});
});
but to clear state and init state again you need implement your own logic depending of your app.

Service worker and CSP configuration change

On our project we have CSP configured and passed in Response Headers. Also we have simple Service Worker which checking if it's possible to navigate to another page and if not redirecting to cached offline html page.
This is code of part of Service Worker for fetch event
self.addEventListener('fetch', function (event) {
event.respondWith(
// Try to find requested resource in the cache
caches
.match(event.request).then(function (response) {
// Fallback to network if it's not in cache
return response || fetch(event.request);
})
.catch(getFallbackResponse(event))
);
});
But when CSP configuration is changed and Service Worker was installed before this changes in CSP configuration we get
Refused to load the script '[url]' because it violates the following Content Security Policy directive: ... error. And as soon as we update or unregister Service Worker, new CSP configuration is applied.
Is it expected behaviour?
It seems that you need the service worker to update immediately, if you take a look at MDN's description of skip​Waiting():
forces the waiting service worker to become the active service worker.
Use this method with Clients.claim() to ensure that updates to the underlying service worker take effect immediately for both the current client and all other active clients.
Clients.claim() is to make sure that the service worker in potentially other opened tabs is updated as well.

How to deploy updates to service workers running on customers' sites?

Suppose I provide a push notification service used by different websites. This service requires a service worker to be installed on my customers' sites. I want the architecture to have a few properties:
Completely static resources. The process of installing the service worker files and configuring the JS snippet, etc. only needs to be done once.
The ability to update the service worker at any time. I want to be able to update the service worker at any time to fix bugs, deploy improvements, etc.
Satisfying both of these constraints is difficult because the browser only installs a new version of the service worker if the content of service worker script itself changes. (That is, without considering dependencies specified via importScripts().)
If you can't change the contents of service worker itself, consider creating a "new" service worker by appending a hash to the service worker URL. This will cause the browser to install the "new" service worker every time the hash changes.
That is, replace code like
navigator.serviceWorker.register("/sw.js");
with
navigator.serviceWorker.register(`/sw.js?hash=${HASH}`);
When the "new" service worker is installed, the browser will re-check all imported scripts. (This applies even if the "new" service worker is byte-for-byte identical to the "old" one, because the URLs are different.)
How to generate the hash?
There's a few different ways to generate the hash. A completely random HASH will lead to the browser updating the service worker on every page load, which is unlikely to be what you want.
Two different approaches:
(Best) You know when the imported script changes. In this case, only change HASH when the imported script changes. Ideally, HASH would be a hash of the contents of the imported script itself.
(Okay) Derive the hash from the time. Math.floor(Date.now() / (3600 * 1000)) will cause a "new" service worker to be installed every hour, which will also result in the dependencies being checked. (You'll probably also want to apply some jitter to avoid all clients updating at the same time.)
Suggested architecture
If you provide a service-worker backed service to other websites (e.g. a push notification service), you can provide a completely static service worker and JS install snippet to your customers which allows you to control and trigger updates completely from your site.
Code for customer.com (all static):
JS snippet for customer to include on all HTML pages their site (static):
<script src="https://provider.com/register-sw.html?customer_id=8932e4288bc8">
</script>
Service worker for customer to install at https://example.com/sw.js (static):
importScripts("https://provider.com/imported-sw.js?customer_id=8932e4288bc8");
Code for your site (can be dynamic):
Service worker registration code on your site at https://provider.com/register-sw.html?customer_id=8932e4288bc8 (dynamic):
const HASH = hash_of_file("imported-sw.js");
navigator.serviceWorker.register(`/sw.js?hash=${HASH}`);
"Real" service worker on your site at https://provider.com/imported-sw.js?customer_id=8932e4288bc8 (dynamic):
// when service worker is updated, all clients receive
// update because service worker itself is "new"
self.addEventListener(...);
NOTE: The byte-for-byte requirement is in the process of being changed so that this check extends to imported scripts by default (not just the registered URL itself), but as of April 2017 no browser implements this behavior.

Resources