navigator.serviceWorker is never ready - service-worker

I registered a service worker successfully, but then the code
navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) {
// Do we already have a push message subscription?
....
hangs -- the function is never called. Why?

The problem was that the service-worker.js file was stored in an assets sub-directory.
Don't do that: store the service-worker.js in the root of your app (or higher). That way your app can access the service-worker.
See HTML5Rocks article --
One subtlety with the register method is the location of the service worker file. You'll notice in this case that the service worker file is at the root of the domain. This means that the service worker's scope will be the entire origin. In other words, this service worker will receive fetch events for everything on this domain. If we register the service worker file at /example/sw.js, then the service worker would only see fetch events for pages whose URL starts with /example/ (i.e. /example/page1/, /example/page2/).

Like said in the accepted answer, the problem is, indeed, probably because your service worker JS file is in a different path than your current page.
By default, the scope of the service worker is the path to its JS file. If your JS file is reachable at http://www.example.com/assets/js/service-worker.js, your service worker will only work/"be ready" for URL starting with /assets/js/.
But, you can change the scope of the service worker. First, you need to register if using the scope option:
navigator.serviceWorker.register('http://www.example.com/assets/js/service-worker.js', {
scope: '/',
});
If you do, just this, you will get errors in the Chrome console:
The path of the provided scope ('/') is not under the max scope
allowed ('/assets/js/'). Adjust the scope, move the Service Worker
script, or use the Service-Worker-Allowed HTTP header to allow the
scope.
/admin/#/builder:1 Uncaught (in promise) DOMException: Failed to
register a ServiceWorker for scope ('http://www.example.com/') with
script
('http://www.example.com/assets/js/service-worker.js'): The path of
the provided scope ('/') is not under the max scope allowed
('/assets/js/'). Adjust the scope, move the Service Worker script, or
use the Service-Worker-Allowed HTTP header to allow the scope.
You then need to create an .htacess file at the root of your website with the following content:
<IfModule mod_headers.c>
<Files ~ "service-worker\.js">
Header set Service-Worker-Allowed: /
</Files>
</IfModule>

Had the same issue, but putting service worker and installing script in the same directory didn't solve this. For me the solution was to add a "/" to the end of the url.
So i had:
http://localhost:9105/controller/main.js - installing script
http://localhost:9105/controller/sw.js - service worker
http://localhost:9105/controller/index.html - page
And when the url in the browser was http://localhost:9105/controller service worker have never been ready, but when url is http://localhost:9105/controller/ it works fine.
I used code below to control this
if (!window.location.href.endsWith('/')) {
window.location.assign(window.location.href + '/')
}

Add service-worker.js file to your project root directory
You can download service-worker.js file from Link
And use below code to register service worker.
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('./service-worker.js', { scope: './' })
.then(function (registration) {
console.log("Service Worker Registered");
})
.catch(function (err) {
console.log("Service Worker Failed to Register", err);
})
}

Related

are network requests due to code after a service worker is registered guaranteed to be served by the service worker fetch() handler?

Registering a service worker is done in index.html with (eg):
<script>
navigator.serviceWorker.register('/sw.js').then(function(registration) {
// Registration was successful
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}, function(err) {
// registration failed :(
console.log('ServiceWorker registration failed: ', err);
});
</script>
If that code is followed by something that requests a resource, eg:
<script src="a.js"></script>
is that request guaranteed to trigger the 'fetch' event handler in the service worker (and so, potentially, be served from a cache)?
Or, should any code that causes a network access in index.html be added dynamically in the then() callback of the register() function (and, is THAT then guaranteed to be served by the service worker's 'fetch' event handler)?
I would recommend reading through "The Service Worker Lifecycle" for more general information.
The answer to most of your questions is "no," since what you're talking about is the initial registration of the service worker. Registering a service worker kicks off an installation and activation process that's independent from the promise returned by register(). The only thing you could infer from that promise is whether starting the process succeeded or not.
What you're asking about—whether a fetch handler will be invoked or not—relies on a service worker being in control of the current page.
In terms of JavaScript, if you want to answer the question "is this page controlled by (any) service worker?", you can do that by checking whether or not navigator.serviceWorker.controller is undefined.
If you want to write code that will only execute once there's a service worker in control of the current page (with the caveat that it might never execute, if something prevented the service worker from properly activating), you could do that by creating a promise that will resolve immediately if there's already a controller, and will otherwise resolve once the controllerchange event fires:
const p = new Promise(r => {
if (navigator.serviceWorker.controller) return r();
navigator.serviceWorker.addEventListener('controllerchange', e => r());
});
// Later, if you want code to execute only if the page is controlled:
p.then(() => {
// There's a SW in control at this point.
});
Inside your service worker, you can add the following to your activate handler to ensure that as soon as a newly installed service worker activates, it takes control of any open pages (including the page that registered it for the first time):
self.addEventListener('activate', () => self.clients.claim());
If you don't include self.clients.claim() inside your service worker's activate handler, then the page that starts out uncontrolled will never start being controlled, even though it's registered a service worker that has activated.

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

PWA Service Worker (Workbox) setting's '/' stand for?

I want to create PWA's service worker file by using workbox.
According to workbox document, precaching setting of workbox is something like this:
service-worker.js
workbox.precaching.precacheAndRoute([
'/styles/example.ac29.css',
{ url: '/index.html', revision: 'abcd1234' },
// ... other entries ...
]);
But what is the actual meaning of /index.html or /styles/example.ac29.css?
It is server root? or, the root of PWA's scope?
For example, if service-worker.js is served in https://example.com/hoge/fuga/service-worker.js, and manifest.json is also served in https://example.com/hoge/fuga/manifest.json with content:
{
"name": "Great PWA site",
"background_color": "#f6f0d3",
"icons": [...],
"start_url": "https://example.com/hoge/fuga/",
"scope":"/hoge/fuga/",
"display": "standalone"
}
In such case, /index.html in workbox setting means https://example.com/index.html? Or, https://example.com/hoge/fuga/index.html?
Within Workbox's precache manifest, /index.html is resolved to a full URL using the server root as the base. It does not user the service worker scope as the base. (After Googling, I guess it's technically called a "root-relative" URL, though I've never really used that phrase before.)
If you had a relative URL like ./index.html, it would be resolved to a full URL using the location of the service worker script as the base.
In general, if you're curious as to what a URL will resolve to, you can run the following from the ServiceWorkerGlobalScope to see:
(new URL('some-URL.html', self.location.href)).href
The easiest way to do this is to open up Chrome's DevTools while on a page you're curious about that has a service worker, go to the Console panel, and choose the service worker's scope in the popup menu, and then enter the code above.

pwa - Service worker does not successfully serve the manifest's start_url

I am trying to add PWA functionality to an existing website that is hosted on Azure and uses Cloudflare CDN.
I have run the lighthouse testing tool on the site and it passes everything in the PWA section (e.g. service worker installed, served over https, manifest installed etc.) except:
"Service worker does not successfully serve the manifest's start_url."
My manifest.json has '/' as the start URL and "/" as the scope.
The '/' is actually default.aspx which I have cached as well.
My service worker caches '/', e.g.
var cacheShellFiles = [
'/',
'/manifest.json',
'/index.html',
'/scripts/app.js',
'/styles/inline.css'
...
]
// install - cache the app shell
self.addEventListener('install', function (event) {
console.log('From SW install: ', event);
// calling skipWatiing() means the sw will skip the waiting state and immediately
// activate even if other tabs open that use the previous sw
self.skipWaiting();
event.waitUntil(
caches.open(CACHE_NAME_SHELL)
.then(function (cache) {
console.log('Cache opened');
return cache.addAll(cacheShellFiles);
})
);
});
When I view the Cache Storage files in dev tools however, the Content-Length of the / and the .css and .js files is 0:
Image of Chrome Developer tools showing cache storage with Content-Length=0
Is the Content-Length = 0 the reason that it is saying it can't serve the manifest's start URL ?
This is an issue with your service worker's scope (different from the scope option in manifest.json).
Your start_url is set to /, but most likely your service worker file is served from a deeper path, e.g. /some-path/service-worker.js. In this case, your service worker's scope is /some-path/, therefore it will not be able to handle requests to paths outside of it, such as the root path /.
To fix this, you need to make sure that your service worker's scope covers your start_url. I can think of two ways to do this:
In your case, serve the service worker file directly from the root path, e.g. /service-worker.js.
Use the Service-Worker-Allowed response header, which overrides the service worker's scope, so that it wouldn't matter from which path the service worker file is served from.
Choose the one that is more appropriate to your setup.

How do I use ServiceWorker without a separate JS file?

We create service workers by
navigator.serviceWorker.register('sw.js', { scope: '/' });
We can create new Workers without an external file like this,
var worker = function() { console.log('worker called'); };
var blob = new Blob( [ '(' , worker.toString() , ')()' ], {
type: 'application/javascript'
});
var bloburl = URL.createObjectURL( blob );
var w = new Worker(bloburl);
With the approach of using blob to create ServiceWorkers, we will get a Security Error as the bloburl would be blob:chrome-extension..., and the origin won't be supported by Service Workers.
Is it possible to create a service worker without external file and use the scope as / ?
I would strongly recommend not trying to find a way around the requirement that the service worker implementation code live in a standalone file. There's a very important of the service worker lifecycle, updates, that relies on your browser being able to fetch your registered service worker JavaScript resource periodically and do a byte-for-byte comparison to see if anything has changed.
If something has changed in your service worker code, then the new code will be considered the installing service worker, and the old service worker code will eventually be considered the redundant service worker as soon as all pages that have the old code registered and unloaded/closed.
While a bit difficult to wrap your head around at first, understanding and making use of the different service worker lifecycle states/events are important if you're concerned about cache management. If it weren't for this update logic, once you registered a service worker for a given scope once, it would never give up control, and you'd be stuck if you had a bug in your code/needed to add new functionality.
One hacky way is to use the the same javascript file understand the context and act as a ServiceWorker as well as the one calling it.
HTML
<script src="main.js"></script>
main.js
if(!this.document) {
self.addEventListener('install', function(e) {
console.log('service worker installation');
});
} else {
navigator.serviceWorker.register('main.js')
}
To prevent maintaining this as a big file main.js, we could use,
if(!this.document) {
//service worker js
importScripts('sw.js');
else {
//loadscript document.js by injecting a script tag
}
But it might come back to using a separate sw.js file for service worker to be a better solution. This would be helpful if one'd want a single entry point to the scripts.

Resources