Service worker fetch event handleR with spring MVC - javascript

I am trying a make a spring MVC app with offline features.Everything is working fine except the fetch handler.
Problem:
I am able to successfully register my service worker but when it comes to fetch handler it ain't working.
Things I tried:
Used debugger above and below the fetch handler.I found out that it is never being called. I am unable to find out the reason behind this strange behaviour.
self.addEventListener('install', function(event) {
console.log('The service worker is being installed.');
event.waitUntil(precache());
});
self.addEventListener('fetch', function(event) {
alert("Hi");
});

"a service worker registration ties the provided script URL to a scope, which is subsequently used for navigation matching."
What you're probably doing is
navigator.serviceWorker.register( '{SOME_RESOURCE_PATH}/service-worker.js') or even setting scope: './'. The problem with this is that is it will make your fetch only trigger on https://foo.bar/{SOME_RESOURCE_PATH}/. Meaning that you will need to set the sw onto the root of your site to remove the resource path being needed. What you'll need to do is change the config to put the sw on the root of the site.
ex. of what you can do in the mvc-config.xml
<mvc:resources mapping="/**" location="classpath:/PATH_TO_SW/"/>
then registering the sw would look like
navigator.serviceWorker.register( '/service-worker.js')

Related

Using JS serviceWorker as identity proxy - trying to detect `access_token` unregister

I am evaluating using a JS ServiceWorkers as an identity proxy, injecting the access_token on fetch() calls.
const addAuthHeader = function (event) {
destURL = new URL(event.request.url);
if (whitelistedOrigins.includes(destURL.origin) && whitelistedPathRegex.test(destURL.pathname)) {
const modifiedHeaders = new Headers(event.request.headers);
if (token) {
modifiedHeaders.append('Authorization', token) //< Injection
}
const authReq = new Request(event.request, {headers: modifiedHeaders, mode: 'cors' });
event.respondWith((async () => fetch(authReq))());
}
}
// Intercept all fetch requests and add the auth header
self.addEventListener('fetch', addAuthHeader);
The token is stored in a closure variable within the serviceWorker class. Click here for more information about this approach.
One problem I am running into is that when the serviceWorker is updated, the token variable is being overwritten and the access_token is lost.
Is there a way to detect that the serviceWorker has been updated? Or, to protect the token variable? Is there a design pattern/standard you can point me towards related to using serviceWorker as an identity proxy as I have done?
I would advise against an approach that requires a service worker to be in control of a given page for anything security/identity related.
First off, users who visit your page for the first time will not have a service worker installed yet, so it won't be in control of the current page. Additionally, users who are familiar with the browser's developer tools can unregister a service worker at any time, and/or use shift-reload to visit your page without the active service worker in control. So you really can't rely on the service worker always being there.
Second, a service worker's global state is short-lived, and you can't rely on variables being present across multiple event invocations. There are more details about that in "When does code in a service worker outside of an event handler run?".
Generally speaking, you should consider the behavior inside of a service worker as progressively enhancing the core functionality of your web app, and not move any required functionality to the service worker.

Service Worker gets deleted and does not receive push event even after specifying root scope while registering

I am registering the service worker by using the Service-Worker-Allowed header with '/' value and specifying scope while registration.
Registering S/W:
navigator.serviceWorker.register('/myapp/js/serviceWorker.js', {scope: '/'})
^ This is successful.
Serving the S/W:
res.setHeader('Service-Worker-Allowed', '/');
The workflow is something like below, myapp is not directly called but is done behind the scenes without user seeing actual url.
User navigates to
www.domain.com/otherapp?callmyapp=true
and behind the scenes myapp www.domain.com/myapp is initiated where I am doing above registrations with root scope.
Once user navigates to
www.domain.com/otherapp/somethingelse&myapp=false
myapp is not available to the user to interact.
Earlier (while NOT using root scope) the service worker was getting redundant but now it is not. However it is marked deleted.
I don't receive any push event after this redirection.
serviceWorker.js
self.addEventListener("push", function (event) {
console.log('sw push event');
if (event.data) {
const notificationData = JSON.parse(event.data.text());
self.registration.showNotification(notificationData.title, notificationData);
}
});
From what i understand with scopes is if i register the service worker with root scope it should still be available to receive push event and not get marked deleted.
I see many posts around service worker and angular, might be worth mentioning that myapp is old school backbone/vanilla/dust JS while otherapp is Angular.
ANSWER:
The issue was the otherApp was un-subscribing all the service workers. Only if i knew. :/ It took a while for me to debug and conclude from the logs even after explicitly caching myApp files.
It isn't clear what you mean by "behind the scenes" or "not available to the user to interact". The best way to get help with these kinds of issues is to create a reduced example, as in something which demonstrates the bug with all the irrelevant stuff removed.
I've tried to recreate the system you're talking about. https://glitch.com/edit/#!/sw-scope-example?path=server.js:22:0.
If you visit https://sw-scope-example.glitch.me/install-service-worker/ it calls:
navigator.serviceWorker.register('/deep/path/to/sw.js', { scope: '/' });
That service worker has a header:
Service-Worker-Allowed: /
The service worker is really basic:
addEventListener('fetch', (event) => {
event.respondWith(new Response('This is a response from the service worker.'));
});
It registers fine. Now if you visit any URL on the origin, such as https://sw-scope-example.glitch.me/other-app/whatever, you'll see the response from the service worker.
This works fine, so there's some key difference between your example & mine. Can you remix the Glitch above so it demonstrates the issue you're seeing?

Force Service Worker to only return cached responses when specific page is open

I have built a portal which provides access to several features, including trouble ticket functionality.
The client has asked me to make trouble ticket functionality available offline. They want to be able to "check out" specific existing tickets while online, which are then accessible (view/edit) while the user's device is out-of-range of any internet connection. Also, they want the ability to create new tickets while offline. Then, when the connection is available, they will check in the changed/newly created tickets.
I have been tinkering with Service Workers and reviewing some good documentation on them, and I feel I have a basic understanding of how to cache the data.
However, since I only want to make the Ticketing portion of the portal available offline, I don't want the service worker caching or returning cached data when any other page of the portal is being accessed. All pages are in the same directory, so the service worker, once loaded, would by default intercept all requests from all pages in the portal.
How can I set up the service worker to only respond with cached data when the Tickets page is open?
Do I have to manually check the window.location value when fetch events occur? For example,
if (window.location == 'https://www.myurl.com/tickets')
{
// try to get the request from network. If successful, cache the result.
// If not successful, try returning the request from the cache.
}
else
{
// only try the network, and don't cache the result.
}
There are many supporting files that need to be loaded for the page (i.e. css files, js files, etc.) so it's not enough to simply check the request.url for the page name. Will 'window.location' be accessible in the service worker event, and is this a reasonable way to accomplish this?
Use service worker scoping
I know that you mentioned that you currently have all pages served from the same directory... but if you have any flexibility over your web app's URL structure at all, then the cleanest approach would be to serve your ticket functionality from URLs that begin with a unique path prefix (like /tickets/) and then host your service worker from /tickets/service-worker.js. The effort to reorganize your URLs may be worthwhile if it means being able to take advantage of the default service worker scoping and just not have to worry about pages outside of /tickets/ being controlled by a service worker.
Infer the referrer
There's information in this answer about determining what the referring window client URL is from within your service worker's fetch handler. You can combine that with an initial check in the fetch handler to see if it's a navigation request and use that to exit early.
const TICKETS = '/tickets';
self.addEventListener('fetch', event => {
const requestUrl = new URL(event.request.url);
if (event.request.mode === 'navigate' && requestUrl.pathname !== TICKETS) {
return;
}
const referrerUrl = ...; // See https://stackoverflow.com/questions/50045641
if (referrerUrl.pathname !== TICKETS) {
return;
}
// At this point, you know that it's either a navigation for /tickets,
// or a request for a subresource from /tickets.
});

Passing state info into a service worker before `install`

Background
I'm new to service workers but working on a library that is intended to become "offline-first" (really, almost "offline-only") (FWIW, the intent is to allow consumers of the library to provide JSON config representing tabular multilinear texts and get in return an app which allows their users to browse these texts in a highly customizable manner by paragraph/verse ranges.)
Other projects are to install the library as a dependency and then supply information via our JavaScript API such as the path of a JSON config file indicating the files that our app will consume to produce an (offline) app for them.
While I know we could do any of the following:
require users provide a hard-coded path from which our service worker's install script could use waitUntil with its own JSON request to retrieve the user's necessary files
skip the service worker's install step of the service worker for the JSON file, and rely on fetch events to update the cache, providing a fallback display if the user completed the install and went offline before the fetches could occur.
Post some state info from our main script to a server which the service worker, once registered, would query before completing its install event.
...but all choices seems less than ideal because, respectively:
Our library's consumers may prefer to be able to designate their own location for their JSON config.
Given that the JSON config designates files critical to showing their users anything useful, I'd rather not allow an install to complete only to say that the user has to go back online to get the rest of the files if they were not able to remain online after the install event to see all the required fetches occur.
Besides wanting to avoid more trips to the server and extra code, I'd prefer for our code to be so offline-oriented as to be able to work entirely on mere static file servers.
Question:
Is there some way to pass a message or state information into a service worker before the install event occurs, whether as part of the query string of the service worker URL, or through a messaging event? The messaging event could even technically arrive after the install event begins as long as it can occur before a waitUntil within the install is complete.
I know I could test this myself, but I'd like to know what best practices might be anyways when the critical app files must themselves be dynamically obtained as in such libraries as ours.
I'm guessing indexedDB might be the sole alternative here (i.e., saving the config info or path of the JSON config to indexedDB, registering a service worker, and retrieving the indexedDB data from within the install event)? Even this would not be ideal as I'm letting users define a namespace for their storage, but I need a way for it too to be passed into the worker, or otherwise, multiple such apps on the origin could clash.
Using a Query Parameter
If you find it useful, then yes, you can provide state during service worker installation by including a query parameter to your service worker when you register it, like so:
// Inside your main page:
const pathToJson = '/path/to/file.json';
const swUrl = '/sw.js?pathToJson=' + encodeURIComponent(pathToJson);
navigator.serviceWorker.register(swUrl);
// Inside your sw.js:
self.addEventListener('install', event => {
const pathToJson = new URL(location).searchParams.get('pathToJson');
event.waitUntil(
fetch(pathToJson)
.then(response => response.json())
.then(jsonData => /* Do something with jsonData */)
);
});
A few things to note about this approach:
If you fetch() the JSON file in your install handler (as in the code sample), that will effectively happen once per version of your service worker script (sw.js). If the contents of the JSON file change, but everything else stays the same, the service worker won't automatically detect that and repopulate your caches.
Following from the first point, if you work around that by, e.g., including hash-based versioning in your JSON file's URL, each time you change that URL, you'll end up installing a new service worker. This isn't a bad thing, per se, but you need to keep it in mind if you have logic in your web app that listens for service worker lifecycle events.
Alternative Approaches
You also might find it easier to just add files to your caches from within the context of your main page, since browsers that support the Cache Storage API expose it via window.caches. Precaching the files within the install handler of a service worker does have the advantage of ensuring that all the files have been cached successfully before the service worker installs, though.
Another approach is to write the state information to IndexedDB from the window context, and then read from IndexedDB inside of your service worker's install handler.
Update 3:
And since it is not supposed to be safe to rely on globals within the worker, my messaging solution seems even less sound. I think it either has to be Jeff Posnick's solution (in some cases, importScripts may work).
Update 2:
Although not directly related to the topic of this thread relating to "install" event, as per a discussion starting at https://github.com/w3c/ServiceWorker/issues/659#issuecomment-384919053 , there are some issues, particularly with using this message-passing approach for the activate event. Namely, the activate event may never fail, and thus never be tried again, leaving one's application in an unstable state. (A failure of install will at least not apply the new service worker to old pages, whereas activate will keep fetches on hold until the event completes, which it may never do if it is left waiting for a message that was not received, and which anything but a new worker will fail to correct since new pages won't be able to load to send that message again.)
Update:
Although I got the client from within the install script in Chrome, I wasn't able to receive the message back with navigator.serviceWorker.onmessage for some reason.
However, I was able to fully confirm the following approach in its place:
In the service worker:
self.addEventListener('install', e => {
e.waitUntil(
new Promise((resolve, reject) => {
self.addEventListener('message', ({data: {
myData
}}) => {
// Do something with `myData` here
// then when ready, `resolve`
});
})
);
});
In the calling script:
navigator.serviceWorker.register('sw.js').then((r) => {
r.installing.postMessage({myData: 100});
});
#JeffPosnick 's is the best answer for the simple case I described in the OP, but I thought I'd present my discovering that one can get messages from and into a service worker script early (tested on Chrome) by such as the following:
In the service worker:
self.addEventListener('install', e => {
e.waitUntil(self.clients.matchAll({
includeUncontrolled: true,
type: 'window'
}).then((clients) => new Promise((resolve, reject) => {
if (clients && clients.length) {
const client = clients.pop();
client.postMessage('send msg to main script');
// One should presumably be able to poll to check for a
// variable set in the SW message listener below
// and then `resolve` when set
// Despite the unreliability of setting globals in SW's
// I believe this could be safe here as the `install`
// event is to run while the main script is still open.
}
})));
});
self.addEventListener('message', e => {
console.log('SW receiving main script msg', e.data);
e.ports[0].postMessage('sw response');
});
In the calling script:
navigator.serviceWorker.addEventListener('message', (e) => {
console.log('msg recd in main script', e.data);
e.source.postMessage('sending back to sw');
});
return navigator.serviceWorker.register(
'sw.js'
).then((r) => {
// navigator.serviceWorker.ready.then((r) => { // This had been necessary at some point in my testing (with r.active.postMessage), but not working for me atm...
// Sending a subsequent message
const messageChannel = new MessageChannel();
messageChannel.port1.onmessage = (e) => {
if (e.data.error) {
console.log('err', e.data.error);
} else {
console.log('data', e.data);
}
};
navigator.serviceWorker.controller.postMessage('sending to sw', [messageChannel.port2]);
// });
});

Firebase Messaging service worker on chrome

I am trying to implement FCM chrome client on Eclipse IDE.
Using Javascript, I am trying to register the default service worker : firebase-messaging-sw.js but the path for this js file cannot be resolved by project.
The SDK is searching for this file at hosting level: /firebase-messaging-sw.js i.e. https://localhost:8080/firebase-messaging-sw.js but not at https://localhost:8080/myapp/firebase-messaging-sw.js
Therefore, I get Error :
Firebase Service worker not found while using GWT (404 Error)
How can I resolve this issue ?
Service workers by default should reside on the root of the application. To register a service worker st some other location than the default location, you can use the following code.
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw-test/sw.js', {scope: '/sw-test/'})
.then(function(reg) {
// registration worked
console.log('Registration succeeded. Scope is ' + reg.scope);
}).catch(function(error) {
// registration failed
console.log('Registration failed with ' + error);
});
}
From MDN,
The outer block performs a feature detection test to make sure service workers are supported before trying to register one.
Next, we use the ServiceWorkerContainer.register() function to register the service worker for this site, which is just a JavaScript
file residing inside our app (note this is the file's URL relative to
the origin, not the JS file that references it.)
The scope parameter is optional, and can be used to specify the subset of your content that you want the service worker to control. In
this case, we have specified '/sw-test/', which means all content
under the app's origin. If you leave it out, it will default to this
value anyway, but we specified it here for illustration purposes.
The .then() promise function is used to chain a success case onto our promise structure. When the promise resolves successfully, the
code inside it executes.
Finally, we chain a .catch() function onto the end that will run if the promise is rejected.
This registers a service worker, which runs in a worker context, and
therefore has no DOM access. You then run code in the service worker
outside of your normal pages to control their loading.

Categories

Resources