Blazor - Service worker not installing due to integrity check failure - javascript

I'm trying to setup PWA for my blazor application. I followed the instructions on: https://learn.microsoft.com/en-us/aspnet/core/blazor/progressive-web-app?view=aspnetcore-6.0&tabs=visual-studio
But when I open the deployed website the following error occurs:
Failed to find a valid digest in the 'integrity' attribute for resource 'domain/manifest.json' with computed SHA-256 integrity 'uDWnAIEnaz9hFx7aEpJJVS1a+QB/W7fMELDfHWSOFkQ='. The resource has been blocked.
Unknown error occurred while trying to verify integrity.
service-worker.js:22
Uncaught (in promise) TypeError: Failed to fetch
at service-worker.js:22:54
at async onInstall (service-worker.js:22:5)
In the source file this happens here:
async function onInstall(event) {
console.info('Service worker: Install');
// Fetch and cache all matching items from the assets manifest
const assetsRequests = self.assetsManifest.assets
.filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url)))
.filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url)))
.map(asset => new Request(asset.url, { integrity: asset.hash, cache: 'no-cache' }));
await caches.open(cacheName).then(cache => cache.addAll(assetsRequests));
}
I think the error is happening since the entry in assetsRequests has a wrong hash and the resource is blocked. If I remove the file from the service-worker-assets.js, the service worker installs and the PWA can be used. But I think this is not a reliable solution.
This also happens sometimes for the appsettings.json. In the service-worker-assets.js I can find the following entry:
{
"hash": "sha256-+Py0\/ezc+0k1sm\/aruGPrVhS1jOCTfPKMhOSS+bunek=",
"url": "manifest.json"
},
So the hash does not seem to match. Where does the browser take the wrong hash from? How can I fix this so it does match?
Also it seems that the app is caching older files sometimes. Even when I do a "Reset Cache & Hard Reload" in Chrome the service-worker.js file is still an older version. Any idea how to fix this as well, since it might be related?
Edit: I was also checking this solution: https://stackoverflow.com/a/69935118/11385442. But in the mentioned blazor.boot.json I cannot find any reference to the manifest.json or the appsettings.json. Only Dlls are listed. So the problem only seems to relate to files not listed in blazor.boot.json.
Edit2: What I can see on the webserver is that the following files are published:
appsettings.json
appsettings.json.br
appsettings.json.gzip
So it seems like compressed version are added. Also the appsettings.json has a different size than the one in the solution. My guess is that somewhere in the build or release pipeline (Azure) the files are modified. But even when I copy the appsettings.json manually to the webserver the error still occurs. I was following Information provided here: https://learn.microsoft.com/en-us/aspnet/core/blazor/host-and-deploy/webassembly?view=aspnetcore-5.0
(Diagnosing integrity problems)

My guess was right. The appsettings.json was modified probably due to the xml transformation in the azure pipeline. My current solution is to exclude integrity validation for such resources as described in the following answer: Error loading appsettings.Production.json due to digest integrity issue
Also I changed the "sw-registrator.js" mentioned in the original posts comments to work correctly, because it didn't load the new files into the cache:
function invokeServiceWorkerUpdateFlow(registration) {
if (confirm("New version available, reload?") == true) {
if (registration.waiting) {
console.info(`Service worker registrator: Post skip_waiting...`);
// let waiting Service Worker know it should became active
registration.waiting.postMessage('SKIP_WAITING')
}
}
}
function checkServiceWorkerUpdate(registration) {
setInterval(() => {
console.info(`Service worker registrator: Checking for update... (scope: ${registration.scope})`);
registration.update();
}, 60 * 1000); // 60000ms -> check each minute
}
// check if the browser supports serviceWorker at all
if ('serviceWorker' in navigator) {
// wait for the page to load
window.addEventListener('load', async () => {
// register the service worker from the file specified
const registration = await navigator.serviceWorker.register('/service-worker.js');
// ensure the case when the updatefound event was missed is also handled
// by re-invoking the prompt when there's a waiting Service Worker
if (registration.waiting) {
invokeServiceWorkerUpdateFlow(registration);
}
// detect Service Worker update available and wait for it to become installed
registration.addEventListener('updatefound', () => {
if (registration.installing) {
// wait until the new Service worker is actually installed (ready to take over)
registration.installing.addEventListener('statechange', () => {
if (registration.waiting) {
// if there's an existing controller (previous Service Worker), show the prompt
if (navigator.serviceWorker.controller) {
invokeServiceWorkerUpdateFlow(registration);
} else {
// otherwise it's the first install, nothing to do
console.log('Service worker registrator: Initialized for the first time.')
}
}
});
}
});
checkServiceWorkerUpdate(registration);
let refreshing = false;
// detect controller change and refresh the page
navigator.serviceWorker.addEventListener('controllerchange', () => {
console.info(`Service worker registrator: Refreshing app... (refreshing: ${refreshing})`);
if (!refreshing) {
window.location.reload();
refreshing = true
}
});
});
}
else
{
console.error(`Service worker registrator: This browser doesn't support service workers.`);
}
Also I had to add this in service-worker.js:
self.addEventListener('message', (event) => {
console.info('Service worker: Message received');
if (event.data === 'SKIP_WAITING') {
// Cause the service worker to update
self.skipWaiting();
}
});
This code was mostly taken from https://whatwebcando.today/articles/handling-service-worker-updates/

Related

ReactJS ServiceWorker storing the same code in multiple cache files

I am trying to add a serviceworker to an existing React app with this filesystem layout:
Filesystem
Basically a bit of initialization code is stored in the public folder, and all code of importance is in the src folder. In the serviceWorker.js file, I made an array of filenames to cache and call that array in the 'install' event listener, and if I check DevTools I can see that the filenames are present in the cache: when I preview the data in Chrome DevTools however, I see that the code inside the cached files is all from index.html. In fact, I can add anything I want to the filename array and I will find it in cached storage only to find that it is storing the index.html code. It seems like no matter what file I try to add to the cache, only index.html gets loaded.
ServiceWorker.js:
let CACHE_NAME = "MG-cache-v2";
const urlsToCache = [
'/',
'/index.html',
'/src/App.js',
'/monkey'
];
self.addEventListener('install', function (event) {
//perform install steps
event.waitUntil(
caches.open(CACHE_NAME).then(function (cache) {
console.log('Opened MG_Cache');
return cache.addAll(urlsToCache);
}).catch(function (error) {
console.error("Error loading cache files: ", error);
})
);
self.skipWaiting();
});
self.addEventListener('fetch', function (event) {
event.respondWith(caches.match(event.request).then(function (response) {
if (response) {
return response;
}
return fetch(event.request);
})
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(async function () {
const cacheNames = await caches.keys();
await Promise.all(
cacheNames.filter((cacheName) => {
//Return true if you want to remove this cache,
//but remember that caches are shared across the whole origin
return;
}).map(cacheName => caches.delete(cacheName))
);
})
})
Portion of index.html:
<script>
if ('serviceWorker' in navigator)
{
window.addEventListener('load', function () {
navigator.serviceWorker.register('serviceWorker.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>
Google Devtools Preview:
All files are the same
I have tried a variety of naming strategies in the filename array but all have ended with the same result. At this point I'm at a complete loss.
EDIT: While this does not solve my problem, I found an answer to another problem that gives a little guidance. It seems like the server never finds the file I request and thus returns index.html. I tried placing the serviceWorker.js file in the src folder and moving the service worker registration to App.js and got an error:
`DOMException: Failed to register a ServiceWorker for scope ('http://localhost:3000/src/') with script ('http://localhost:3000/src/serviceWorker.js'): The script has an unsupported MIME type ('text/html'). `
This suggests that the server somehow doesn't have access to the src folder, only public. Any idea why that may be?
An important piece of information I left out it that I'm using Create-React-App. Because of the enforced layout of the filesystem, the serviceWorker must be placed in the public folder: at the same time, the scope of service workers by default is the folder that they are placed in. According to this answer, changing the scope of the service worker to be a level above the folder that it is in requires adding to the HTTP header response of the service worker (not entirely sure what that means), and doing something like that assumes you have some form of a local server set up. Alas, thus far I have just been using npm start to test my app and pushing onto nsmp to make the site live, thus have negleted to do any form of server implementation myself (I know, not very smart of me).
My hotfix was to create a new temporary app with the npx create-react-app my-app --template cra-template-pwa command, copy all files pertaining to service workers from that app (serviceWorkerRegistration.js, service-worker.js, index.js, potentially setupTests.js), and paste them into my app. Then I could simply follow this tutorial. Now my site works offline.

Can I catch 404s in a service worker?

Basically, I have an online app that uses a htaccess file to silently redirect all requests in a given /folder/ to the same html. Then, to decide what to show the user, the page calls
var page_name = location.href.split('/').pop();
This works well online, but could I use a ServiceWorker to support this folder/file model while the page is offline? Or will I always get the page cannot be found error unless I explicitly cache the URLs?
What you describe can be accomplished using the App Shell model.
Your service worker's exact code might look a little different, and tools like Workbox can automate some of this for you, but a very basic, "vanilla" example of a service worker that accomplishes this is:
self.addEvenListener('install', (event) => {
const cacheShell = async () => {
const cache = await caches.open('my-cache');
await cache.add('/shell.html');
};
event.waitUntil(cacheShell());
});
self.addEventListener('fetch', (event) => {
// If this is a navigation request...
if (event.request.mode === 'navigate') {
// ...respond with the cached shell HTML.
event.respondWith(caches.match('/shell.html'));
return;
}
// Any other caching/response logic can go here.
});
Regardless of what the location.href value is, when this service worker is in control, the App Shell HTML will be used to fulfill all navigation requests.

JavaScript (PWA) - What is the correct way to initialize service-worker?

What is the better way to initialize service-worker used for PWA caching?
Method 1:
I expect that in this case will be installed new SW instance. I guess that's not necessary and I want to re-activate existing SW already running in the browser - am I right?
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('sw.js').then(registration => {
// Registration success
}, function(err) {
// Registration failed
});
});
}
Method 2: In this case I'm checking that there SW already registered and I'm installing registering a new one only if there is non registered.
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.getRegistrations().then(registrations => {
const isServiceWorkerNotRegistered = registrations.length === 0;
if (isServiceWorkerNotRegistered) {
navigator.serviceWorker.register('sw.js').then(registration => {
// Registration success
}).catch(error => {
// Registration failed
});
} else {
console.log('Service worker already registered.');
}
});
});
}
On top of that... Is it really necessary to wait for window to load (line 2)? Since SW is running asynchronously in the browser I guess I don't need the app to be completely loaded.
Waiting for the load event is so that assets being downloaded by the service worker don't take bandwidth and device resources from rendering the currently requested page. See improving the boilerplate.
For your registration question, use the simple first version.
Unless you change the URL of the service worker script, navigator.serviceWorker.register() is effectively a no-op during subsequent visits.
subsequent visits
If the service worker is not registered, register will register it. If a service worker is already registered, register will basically not do anything.

Avoid caching start_url with service worker

I'm having some problems on setting up a service worker for my website.
I only want to cache css/js/fonts and some images/svg, I don't want to cache the HTML since all of it is updated every minute.
It kinda works, but trying on my smartphone I keep getting the notification "Add to homescreen" even when I've already added it. And on the Chrome Dev app I don't get the Add button.
Also with the Lighthouse I get the following errors:
"Does not respond with a 200 when offline"
"User will not be prompted to Install the Web App, Failures: Manifest start_url is not cached by a Service Worker."
Right now my sw.js is like this. As you can see I commented the fetch part because it was caching the HTML and also the Cookies weren't working.
Is there around a simple Service Worker "template" to use?
const PRECACHE = 'app-name';
const RUNTIME = 'runtime';
// A list of local resources we always want to be cached.
const PRECACHE_URLS = [
'/css/file.css',
'/js/file.js',
'/images/logo.png',
'/fonts/roboto/Roboto-Regular.woff2'
]
// The install handler takes care of precaching the resources we always need.
self.addEventListener('install', event => {
event.waitUntil(
caches.open(PRECACHE)
.then(cache => cache.addAll(PRECACHE_URLS))
.then(self.skipWaiting())
);
});
// The activate handler takes care of cleaning up old caches.
self.addEventListener('activate', event => {
const currentCaches = [PRECACHE, RUNTIME];
event.waitUntil(
caches.keys().then(cacheNames => {
return cacheNames.filter(cacheName => !currentCaches.includes(cacheName));
}).then(cachesToDelete => {
return Promise.all(cachesToDelete.map(cacheToDelete => {
return caches.delete(cacheToDelete);
}));
}).then(() => self.clients.claim())
);
});
// The fetch handler serves responses for same-origin resources from a cache.
// If no response is found, it populates the runtime cache with the response
// from the network before returning it to the page.
self.addEventListener('fetch', event => {
// Skip cross-origin requests, like those for Google Analytics.
// if (event.request.url.startsWith(self.location.origin)) {
// event.respondWith(
// caches.match(event.request).then(cachedResponse => {
// if (cachedResponse) {
// return cachedResponse;
// }
// return caches.open(RUNTIME).then(cache => {
// return fetch(event.request).then(response => {
// // Put a copy of the response in the runtime cache.
// return cache.put(event.request, response.clone()).then(() => {
// return response;
// });
// });
// });
// })
// );
// }
});
I'm not sure why the install banner appears but the two errors given by lighthouse are related to the missing caching of the very start_url, propably index.html. So Lighthouse will always be telling you about those if you follow the caching strategy you described here.
I suggest you could try Workbox and their runtime caching. Runtime caching, in a nutshell, works like so: you specify urls like *.svg, *.css etc. and the SW caches them once the client first asks them. In the future, when the files are already cached, the SW serves them from the cache to the client. Basically you tell the SW to cache this and that kind of urls when it encounters them and not in advance.
Runtime caching could very well be accompanied by precaching (that may also be found from Workbox!) to cache a bunch of files.
Check it out here: https://workboxjs.org
They have a couple of examples and plugins for build tooling.

Refresh page after load on cache-first Service Worker

I'm currently considering adding service workers to a Web app I'm building.
This app is, essentially, a collection manager. You can CRUD items of various types and they are usually tightly linked together (e.g. A hasMany B hasMany C).
sw-toolbox offers a toolbox.fastest handler which goes to the cache and then to the network (in 99% of the cases, cache will be faster), updating the cache in the background. What I'm wondering is how you can be notified that there's a new version of the page available. My intent is to show the cached version and, then, if the network fetch got a newer version, to suggest to the user to refresh the page in order to see the latest edits. I saw something in a YouTube video a while ago but the presenter gives no clue of how to deal with this.
Is that possible? Is there some event handler or promise that I could bind to the request so that I know when the newer version is retrieved? I would then post a message to the page to show a notification.
If not, I know I can use toolbox.networkFirst along with a reasonable timeout to make the pages available even on Lie-Fi, but it's not as good.
I just stumbled accross the Mozilla Service Worker Cookbooj, which includes more or less what I wanted: https://serviceworke.rs/strategy-cache-update-and-refresh.html
Here are the relevant parts (not my code: copied here for convenience).
Fetch methods for the worker
// On fetch, use cache but update the entry with the latest contents from the server.
self.addEventListener('fetch', function(evt) {
console.log('The service worker is serving the asset.');
// You can use respondWith() to answer ASAP…
evt.respondWith(fromCache(evt.request));
// ...and waitUntil() to prevent the worker to be killed until the cache is updated.
evt.waitUntil(
update(evt.request)
// Finally, send a message to the client to inform it about the resource is up to date.
.then(refresh)
);
});
// Open the cache where the assets were stored and search for the requested resource. Notice that in case of no matching, the promise still resolves but it does with undefined as value.
function fromCache(request) {
return caches.open(CACHE).then(function (cache) {
return cache.match(request);
});
}
// Update consists in opening the cache, performing a network request and storing the new response data.
function update(request) {
return caches.open(CACHE).then(function (cache) {
return fetch(request).then(function (response) {
return cache.put(request, response.clone()).then(function () {
return response;
});
});
});
}
// Sends a message to the clients.
function refresh(response) {
return self.clients.matchAll().then(function (clients) {
clients.forEach(function (client) {
// Encode which resource has been updated. By including the ETag the client can check if the content has changed.
var message = {
type: 'refresh',
url: response.url,
// Notice not all servers return the ETag header. If this is not provided you should use other cache headers or rely on your own means to check if the content has changed.
eTag: response.headers.get('ETag')
};
// Tell the client about the update.
client.postMessage(JSON.stringify(message));
});
});
}
Handling of the "resource was updated" message
navigator.serviceWorker.onmessage = function (evt) {
var message = JSON.parse(evt.data);
var isRefresh = message.type === 'refresh';
var isAsset = message.url.includes('asset');
var lastETag = localStorage.currentETag;
// ETag header usually contains the hash of the resource so it is a very effective way of check for fresh content.
var isNew = lastETag !== message.eTag;
if (isRefresh && isAsset && isNew) {
// Escape the first time (when there is no ETag yet)
if (lastETag) {
// Inform the user about the update.
notice.hidden = false;
}
//For teaching purposes, although this information is in the offline cache and it could be retrieved from the service worker, keeping track of the header in the localStorage keeps the implementation simple.
localStorage.currentETag = message.eTag;
}
};

Categories

Resources