I have a service worker that I use to enable an offline version of my website. This works great. I also have an Android app that is basically just a wrapper around a webview that loads my website.
All was fine and dandy until about 2-3 weeks ago when the Fetch() request started immediately failing. It is only failing when running through the Android webview. Running through a browser works fine. If the resource is cached already (i.e. via the install event) then it works great, it's only when I get a page that is not cached.
The code in my service worker:
self.addEventListener("fetch", function (event) {
if (event.request.method !== 'GET'
|| event.request.url.toLowerCase().indexOf('//ws') > -1
|| event.request.url.toLowerCase().indexOf('localws') > -1) {
// Don't intercept requests made to the web service
// If we don't block the event as shown below, then the request will go to
// the network as usual.
return;
}
event.respondWith(async function () {
// override the default behavior
var oCache = await caches.open('cp_' + version);
var cached = await oCache.match(event.request.url);
if (cached && cached.status < 300) {
return cached;
}
// Need to make a call to the network
try {
var oResp = await fetch(event.request); // THIS LINE CAUSES THE PROBLEM!!!
return oResp;
} catch (oError) {
console.log('SW WORKER: fetch request to network failed.', event.request);
return new Response('<h1>Offline_sw.js: An error has occured. Please try again.</h1><br><h2>Could not load URL: ' + event.request.url + '</h2>', {
status: 503,
statusText: 'Service Unavailable',
headers: new Headers({
'Content-Type': 'text/html'
})
});
}
}()); // event.respondwith
}); // fetch
The line:
var oResp = await fetch(event.request);
is called once I've determined it is not cached and seems to be the culprit. When it errors out I get the following error in my catch(): 'Failed to fetch'
This seems pretty generic and not helpful. Again, this works when going through a browser and so I know it's not a CORS issue, service worker in the wrong directory, etc. Again, it worked until about 3 weeks ago and now I'm getting reports from customers that it's not working.
Here's a screen shot of the actual event.request that I'm sending off:
In the chrome developer tools (used to debug the webview) I see the following:
Am I doing something wrong or is this a bug in the webview / chrome that was released recently? (I say that as chrome powers the webview)
Looks like it was a bug in chromium. See bugs.chromium.org/p/chromium/issues/detail?id=977784. Should be fixed in v 76.
As a work around (as the link mentioned), you can add the following to your android code:
ServiceWorkerController oSWController = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
oSWController = ServiceWorkerController.getInstance();
oSWController.setServiceWorkerClient(new ServiceWorkerClient(){
#Nullable
#Override
public WebResourceResponse shouldInterceptRequest(WebResourceRequest request){
return super.shouldInterceptRequest(request);
}
});
}
Related
I'm working on integrating web-push notifications in my web-application. Everything works fine for Chrome and Firefox on desktop and Chrome on Android, but not for Firefox for Android. This question seems to discuss the same issue but has no responses.
I used this tutorial as a base for the service worker registration script. I have added some more prints/checks but it is mostly the same.
So, when calling the registerServiceWorker method from a button press on FF Android, the serviceWorker is installed, the subscribeUser function is called, but the pushManager.subscribe method will fail with the following error message:
DOMException: User denied permission to use the Push API.
This is not correct, even while paused on the error print line Notification.permission will return "granted".
Doing the same thing on the nightly build results in slightly different, but still incorrect behaviour. The pushManager.subscribe method does not throw an error. Instead the callback is ran but with a null value for the subscription argument. Therefore, the process still fails.
Service worker registration script:
'use strict';
function updateSubscriptionOnServer(subscription, apiEndpoint) {
return fetch(apiEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
subscription_json: JSON.stringify(subscription)
})
});
}
function subscribeUser(swRegistration, applicationServerPublicKey, apiEndpoint) {
// It seems browsers can take the base64 string directly.
// const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey);
console.log(`Subscribing pushManager with appkey ${applicationServerPublicKey}`);
swRegistration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: applicationServerPublicKey
})
.then(function(subscription) {
console.log('User is subscribed.');
console.log(`Sending subscription data to server (${apiEndpoint})`, subscription);
return updateSubscriptionOnServer(subscription, apiEndpoint);
})
.then(function(response) {
if (!response.ok) {
throw new Error('Bad status code from server.');
}
return response.json();
})
.then(function(responseData) {
console.log(responseData);
if (responseData.status!=="success") {
throw new Error('Bad response from server.');
}
})
.catch(function(err) {
// FF Android says "User denied permission to use the Push API."
console.log('Failed to subscribe the user: ', err);
console.log(err.stack);
});
}
function registerServiceWorker(serviceWorkerUrl, applicationServerPublicKey, apiEndpoint){
let swRegistration = null;
if ('serviceWorker' in navigator && 'PushManager' in window) {
console.log('Service Worker and Push is supported');
console.log(`Current Notification.permission = ${Notification.permission}`);
swRegistration = navigator.serviceWorker.register(serviceWorkerUrl)
.then(function(swReg) {
console.log('Service Worker is registered', swReg);
console.log(`Current Notification.permission = ${Notification.permission}`); // Will give "granted"
subscribeUser(swReg, applicationServerPublicKey, apiEndpoint);
})
.catch(function(error) {
console.error('Service Worker Error', error);
});
} else {
console.warn('Push messaging is not supported');
return false;
}
return swRegistration;
}
I cannot figure out how to get a working push-subscription. As said before, all other browsers that I have tried work fine. I hope someone can point me in the right direction. Is this a bug in Firefox Android or in my code?
Showing notifications manually using
new Notification("Hi there!");
does work, proving in principle that permissions are not the issue.
UPDATE:
FF Fenix team confirmed a bug while displaying the notifications
Feel free to track it here
Got curious regarding service worker support for Firefox mobile browser.
Tried hard to find a debug tool for Mobile Firefox as plugin or a 3rd party tool with no luck.
However, I've tested my web push application on Mobile Firefox and may totally confirm there is an issue with Service Worker Registration state.
In order to discard any issues within my code, I've used Matt Gaunt's Webpush Book
And I can tell Matt's service worker returns registration as simply as that:
async function registerSW() {
await navigator.serviceWorker.register('/demos/notification-examples/service-worker.js');
}
function getSW() {
return navigator.serviceWorker.getRegistration('/demos/notification-examples/service-worker.js');
}
Firefox for Android successfully requests permission for notification, but it doesn't display push whenever you launch the .showNotification function.
Here's an example of showNotification method within the Badge example in Matt's website:
async function onBadgeClick() {
const reg = await getSW();
/**** START badgeNotification ****/
const title = 'Badge Notification';
const options = {
badge: '/demos/notification-examples/images/badge-128x128.png'
};
reg.showNotification(title, options);
/**** END badgeNotification ****/
}
Looks fine and should be working fine, but for Firefox Mobile it doesn't work at all.
I guess this issue should be escalated to Mozilla.
UPDATE
New bug report was created on Github:
I am using a service worker to provide caching for my site's assets (HTML, JS, CSS).
When I use Firefox my sw.js is installed correctly and the required files cached. If I go into offline mode I get the site styled correctly with everything present but the data (which is correct as the data is not being cached).
However when I use Chrome I'm getting a TypeError: Failed to fetch error. I'm really unsure why I'm getting this error since it works in Firefox. In addition I'm getting the same error thrown whenever the fetch event fires and the request if for an asset that is not in the cache (and the fetch function is being called).
If I pass an empty array to the cache.addAll function I don't get any errors until attempting to actually handling the fetch event.
It's maybe worth noting that none of the files I'm caching are all coming from localhost and not any other origin so I can't see this being a cross-domain issue.
This is the console output when installing the service worker:
This is the console output when refreshing the page after installing the service worker:
This is the code for my service worker:
const CACHE_NAME = 'webapp-v1';
const CACHE_FILES = [
'/',
'/public/app.css',
'/public/img/_sprites.png',
'/public/js/app.min.js',
'/public/js/polyfills.min.js'
];
self.addEventListener('install', event => {
console.log("[sw.js] Install event.");
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(CACHE_FILES))
.then(self.skipWaiting())
.catch(err => console.error("[sw.js] Error trying to pre-fetch cache files:", err))
);
});
self.addEventListener('activate', event => {
console.log("[sw.js] Activate event.");
event.waitUntil(
self.clients.claim()
);
});
self.addEventListener('fetch', event => {
if (!event.request.url.startsWith(self.location.origin)) return;
console.log("[sw.js] Fetch event on", event.request.url);
event.respondWith(
caches.match(event.request).then(response => {
console.info("[sw.js] Responded to ", event.request.url, "with", response ? "cache hit." : "fetch.");
return response || fetch(event.request);
}).catch(err => {
console.error("[sw.js] Error with match or fetch:", err);
})
);
});
Any help would be great.
cache.addAll(CACHE_FILES)
will fail when 1 of the file is not accessible (HTTP 400,401 etc, also 5XX and 3XX sometimes) to avoid failing all when 1 fail use individual catch statement in a map loop like here https://github.com/GrosSacASac/server-in-the-browser/blob/master/client/js/service_worker.js#L168
the fact that it does not fail with empty array probably means you have an inaccessible resource in CACHE_FILES.
Maybe firefox is less restrective and caches the body of the 400 response.
Inside your fetch handler you try to use caches.match directly but I think that is not legal. you must open the caches first and then from an opened cache you can do cache.match. See https://github.com/GrosSacASac/server-in-the-browser/blob/master/client/js/service_worker.js#L143
With a PWA, we can handle when the device connection is down with offline mode. But how do we detect a fixed network connection and automatically reload/re-activate the application?
You could monitor the offline and online events, which are widely supported. Further, you could test connectivity by attempting to fetch HEAD from the target server URL:
// Test this by running the code snippet below and then
// use the "Offline" checkbox in DevTools Network panel
window.addEventListener('online', handleConnection);
window.addEventListener('offline', handleConnection);
function handleConnection() {
if (navigator.onLine) {
isReachable(getServerUrl()).then(function(online) {
if (online) {
// handle online status
console.log('online');
} else {
console.log('no connectivity');
}
});
} else {
// handle offline status
console.log('offline');
}
}
function isReachable(url) {
/**
* Note: fetch() still "succeeds" for 404s on subdirectories,
* which is ok when only testing for domain reachability.
*
* Example:
* https://google.com/noexist does not throw
* https://noexist.com/noexist does throw
*/
return fetch(url, { method: 'HEAD', mode: 'no-cors' })
.then(function(resp) {
return resp && (resp.ok || resp.type === 'opaque');
})
.catch(function(err) {
console.warn('[conn test failure]:', err);
});
}
function getServerUrl() {
return document.getElementById('serverUrl').value || window.location.origin;
}
<fieldset>
<label>
<span>Server URL for connectivity test:</span>
<input id="serverUrl" style="width: 100%">
</label>
</fieldset>
<script>document.getElementById('serverUrl').value = window.location.origin;</script>
<p>
<i>Use Network Panel in DevTools to toggle Offline status</i>
</p>
One technique of handling this:
Offline event
show offline icon/status
enable only features that are available offline (via cached data)
Online event
show online icon/status
enable all features
Be careful with the online event, that only tells the device if connected. It can be connected to a WiFi hotspot without actual Internet connectivity (because of credentials for example).
A common practice in PWAs is to follow the Application Shell approach to your application. This would allow you to cache the Application Shell upon entry, and then load the data based upon the connection.
The most common method for caching and serving in this approach is to serve from cache with fallback to network, where whenever the resource requested is not available in the cache then you send the request via the network and cache the response. Then serve from the cache.
This allows for a more graceful degradation when you are on a spotty connection, such as on the train.
An example of implementing this:
const cacheName = "my-cache-v1"
self.addEventListener('fetch', (event) => {
if (event.request.method === 'GET') {
event.respondWith(
caches.match(event.request).then((response) => {
if (response) {
return response;
}
return fetch(event.request).then((response) => {
return caches.open(cacheName).then((cache) => {
cache.put(event.request.url, response.clone());
return response;
});
});
})
);
}
});
In the above example (only one of the required steps in a Service Worker life cycle), you would also need to delete outdated cache entries.
Most of the services I've seen use the following practice: with an increasing to a certain value timeout, trying to contact the server. When the maximum timeout value is reached, an indicator with a manual recconect button appears which indicates in how many time the next attempt of reconnect will occur
Cache all requests from an app without explicitly specifying urlsToCache. So I will cache stuff under fetch event.
To respond to requests from the cache.
Update the cache when fetch is success.
Initially,
this.addEventListener('fetch', function(event) {
var fetchReq = event.request.clone(),
cacheReq = event.request.clone();
event.respondWith(fetch(fetchReq).then(function(response) {
var resp = response.clone();
caches.open(CACHE_NAME).then(function(cache) {
req = event.request.clone();
cache.put(req, resp);
});
return response;
}).catch(function() {
return caches.match(cacheReq);
}));
});
The offline situations were handled perfectly well. But the problem here was with the slow connections. The user has to wait till fetch times out or throws an error to get the response from cache.
self.addEventListener('fetch', function(event) {
var cacheRequest = event.request.clone();
event.respondWith(caches.match(cacheRequest).then(function(response) {
if(response) return response;
var fetchRequest = event.request.clone();
return fetch(fetchRequest).then(function(response) {
var responseToCache = response.clone();
caches.open(cache_name).then(function(cache) {
var cacheSaveRequest = event.request.clone();
cache.put(cacheSaveRequest, responseToCache);
});
return response;
});
}));
});
With the cache taking precedence, the responses served were fine. But the problem here is that when the code updates. When /public/main.css served via sw is updated, on page reload only the cache is served, the updated content is not served.
I also tried modifying the cache_name to cache-v2 from cache-v1 (so that sw binary diff exists and sw is updated and that old cache can be cleared), and cleared cache-v1 on activate event. But it gave rise to new problems where two service workers were running at the same time under the same Registration ID. More on this is in this other SO question: How to stop older service workers?
Two service workers running at the same time are not technically a problem—it's working as designed. (See my answer to How to stop older service workers?) Make sure that you close other tabs that might have an older version of your service worker active.
You're running into the inevitable tradeoffs between the different cache vs. network scenarios here. If you haven't yet read through the offline cookbook, it's a great starting point when trying to decide which caching strategy works best for your specific resources.
We've spent a better part of yesterday trying to get this resolved. So as a last ditch effort i'm posting here.
Our setup is a Node.js / Express backend. With Socket.io on the same port as express.
app.io = io.listen(server);
app.io.set('origin', '*');
app.io.set('log level', '2');
app.io.enable('browser client minification');
app.io.set('transports', [
'websocket',
'flashsocket',
'htmlfile',
'xhr-polling',
'jsonp-polling'
]);
I've explicitly enabled all of the transports. We were hoping either jsonp or flash sockets would play nice on our least favorite browsers...
But, I guess there is a reason that they're our least favorite. ;-)
Client side we've got an Angularjs application with socket.io. Very loosely based on this tutorial
window.WEB_SOCKET_SWF_LOCATION = 'https://api.ourdomain.com/socket.io/static/flashsocket/WebSocketMain.swf';
var socket = io.connect('https://api.ourdomain.com', {
'reconnect' : true,
'reconnection delay' : 1500
});
We've tried adding the SWF location as seen here to get flashsockets working. It is serving up the policy and getting the files.. So no luck on that.
Anyways on ie7-9 when it attempts to connect via jsonp polling we get this error:
Object doesn't support this property or method
Contents of the jsonp message will be something like this:
io.j[1]("1::");
Occasionally with more content in it.
io.j seems to be being set as an array in the main socket.io.js file.
When I put this in the developer tools console, it throws the same error.
I've tried moving all the meta tags before the scripts like suggested here. That didn't change anything.
XHR-polling doesn't seem to work either. We've seen some posts about changing settings in ie.. But obviously we can't require our clients to go and request their IT department to change their browser settings just to visit our site. So we've ditched that method.
Also tried creating a blank page, and connecting.. That works. So what would be interfering?
Hopefully you guys have something?
We were unable to resolve this issue by simply making Socket.io work. I don't know if this is a Socket.io issue or if this is a combo of Angularjs and Socket.io.
We did however resolve this issue by adding our own fallback. In our Socket.io service we check for the existence of a feature present in ie9+ as well as webkit and firefox browsers.
var use_socketIO = true;
/*
I'd like to detect websocket, or jsonp.
But those transport methods themselves work.
Just not reliably enough to actually use
*/
if (typeof(window.atob) === 'undefined') {
console.log('browser appears to be < ie10.');
use_socketIO = false;
}
if (use_socketIO) {
var socket = io.connect('https://api.ourdomain.com', {
'reconnect' : true,
'reconnection delay' : 1500
});
}
// Fall back http method.
function httpReq(method, url, data) {
var defer = $q.defer();
$http({
method: method,
url: '//www.domain.com/api'+url,
data: data
}).success(function (data, status, headers, config) {
defer.resolve(data)
});
return defer.promise;
}
// Socket.io method.
function socketReq(method, url, data) {
var defer = $q.defer();
socket.emit(method, {url: url, data: data}, function (response) {
try {
var data = JSON.parse(response);
defer.resolve(data);
} catch (e) {
$log.error('Failed to parse JSON');
$log.info(response);
defer.reject(e);
}
});
return defer.promise;
}
// Main Request method
function request(method, url, data) {
if (use_socketIO) {
return socketReq(method, url, data || {});
} else {
return httpReq(method, url, data || {});
}
}
Then we simply just call request('get', '/url');
Server side we do some magic to build a req and res object if its Socket.io and then forward it to express to handle, otherwise express just handles it like normal.