I am attempting to setup a WordPress Theme as a Progressive Web App. When I run Chromes Audit tool (lighthouse?) I get an uninformative error that I don't know what exactly the problem is. The error is:
Failures: Service worker does not successfully serve the manifest start_url. Unable to fetch start url via service worker
I have hardcoded my start url which is a valid url. Any suggestions on what the issue could be?
https://mywebsite.com/wp-content/themes/mytheme/web.manifest:
...
"scope": "/",
"start_url": "https://mywebsite.com",
"serviceworker": {
"src": "dist/assets/sw/service-worker.js",
"scope": "/aw/",
"update_via_cache": "none"
},
...
}
https://mywebsite.com/wp-content/themes/mytheme/dist/assets/sw/service-worker.js:
...
// 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 register my SW with the following code and it outputs that it has successfully registered the SW:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register(Vue.prototype.$ASSETS_PATH + 'sw/service-worker.js')
.then(function(registration) {
console.log('Registration successful, scope is:', registration.scope);
})
.catch(function(error) {
console.log('Service worker registration failed, error:', error);
});
}
Please change your start_url to
"start_url": "/"
It has to be a relative url. Please see the documentaion
Related
I'm struggling to create a simple POC for iOS PWA with a small video.
https://test-service-worker.azurewebsites.net/
I have simple service worker registration and I cache a small (700kB) video. When I'm online the page works just fine. When I turn on airplane mode and go offline, the page is still reloaded but video will not play.
This POC is based on Google Chrome example
https://googlechrome.github.io/samples/service-worker/prefetch-video/
The video from this example will not work in iOS offline for sure because it only caches 50MB. Mine is only 700kB so well below the limit.
My POC works just fine in Chrome but it won't in the latest mobile Safari (iOS 11.4).
What do I need to change in order to make this work on iOS 11.4+? Is this a bug in Safari?
It turns out, Safari is just quite strict. I'm leaving the question here - hopefully it will save someones time.
What's happening:
Safari requests only part of the video - first it will request 'range: bytes=0-1' response. It expects HTTP 206 response which will reveal size of the file
Based on the response it learns what is the length of the video and then it asks for individual byte ranges of the file (for example range: bytes=0-20000 etc.)
If your response is longer than requested Safari will immediately stop processing subsequent requests.
This is exactly what is happening in Google Chrome example and what was happening in my POC. So if you use fetch like this it will work both online & offline:
//This code is based on https://googlechrome.github.io/samples/service-worker/prefetch-video/
self.addEventListener('fetch', function(event) {
headersLog = [];
for (var pair of event.request.headers.entries()) {
console.log(pair[0]+ ': '+ pair[1]);
headersLog.push(pair[0]+ ': '+ pair[1])
}
console.log('Handling fetch event for', event.request.url, JSON.stringify(headersLog));
if (event.request.headers.get('range')) {
console.log('Range request for', event.request.url);
var rangeHeader=event.request.headers.get('range');
var rangeMatch =rangeHeader.match(/^bytes\=(\d+)\-(\d+)?/)
var pos =Number(rangeMatch[1]);
var pos2=rangeMatch[2];
if (pos2) { pos2=Number(pos2); }
console.log('Range request for '+ event.request.url,'Range: '+rangeHeader, "Parsed as: "+pos+"-"+pos2);
event.respondWith(
caches.open(CURRENT_CACHES.prefetch)
.then(function(cache) {
return cache.match(event.request.url);
}).then(function(res) {
if (!res) {
console.log("Not found in cache - doing fetch")
return fetch(event.request)
.then(res => {
console.log("Fetch done - returning response ",res)
return res.arrayBuffer();
});
}
console.log("FOUND in cache - doing fetch")
return res.arrayBuffer();
}).then(function(ab) {
console.log("Response procssing")
let responseHeaders= {
status: 206,
statusText: 'Partial Content',
headers: [
['Content-Type', 'video/mp4'],
['Content-Range', 'bytes ' + pos + '-' +
(pos2||(ab.byteLength - 1)) + '/' + ab.byteLength]]
};
console.log("Response: ",JSON.stringify(responseHeaders))
var abSliced={};
if (pos2>0){
abSliced=ab.slice(pos,pos2+1);
}else{
abSliced=ab.slice(pos);
}
console.log("Response length: ",abSliced.byteLength)
return new Response(
abSliced,responseHeaders
);
}));
} else {
console.log('Non-range request for', event.request.url);
event.respondWith(
// caches.match() will look for a cache entry in all of the caches available to the service worker.
// It's an alternative to first opening a specific named cache and then matching on that.
caches.match(event.request).then(function(response) {
if (response) {
console.log('Found response in cache:', response);
return response;
}
console.log('No response found in cache. About to fetch from network...');
// event.request will always have the proper mode set ('cors, 'no-cors', etc.) so we don't
// have to hardcode 'no-cors' like we do when fetch()ing in the install handler.
return fetch(event.request).then(function(response) {
console.log('Response from network is:', response);
return response;
}).catch(function(error) {
// This catch() will handle exceptions thrown from the fetch() operation.
// Note that a HTTP error response (e.g. 404) will NOT trigger an exception.
// It will return a normal response object that has the appropriate error code set.
console.error('Fetching failed:', error);
throw error;
});
})
);
}
});
I have implemented service worker in my web app and attempted to cache all html, css, js, and images file. My hosting service is firebase, after a successful deployment I tested if the service will work and the files will be cached, unfortunately the following error will do occur.
service-worker.js
let cacheName = 'my-tools-v1';
let filesToCache = [
'/',
'/css/app.css',
'/css/diffview.css',
'/css/json-lint.css',
'/css/materialize.css',
'/images/favicon.png',
'/images/icons/apple-touch-icon.png',
'/images/icons/apple-touch-icon-57x57.png',
'/images/icons/apple-touch-icon-72x72.png',
'/images/icons/apple-touch-icon-76x76.png',
'/images/icons/apple-touch-icon-114x114.png',
'/images/icons/apple-touch-icon-120x120.png',
'/images/icons/apple-touch-icon-144x144.png',
'/images/icons/apple-touch-icon-152x152.png',
'/images/icons/apple-touch-icon-180x180.png',
'/images/icons/icon-72x72.png',
'/images/icons/icon-96x96.png',
'/images/icons/icon-128x128.png',
'/images/icons/icon-144x144.png',
'/images/icons/icon-152x152.png',
'/images/icons/icon-192x192.png',
'/images/icons/icon-384x384.png',
'/images/icons/icon-512x512.png',
'/js/index.js',
'/js/css-lint.js',
'/js/difflib.js',
'/js/diffview.js',
'/js/ipsum-generator.js',
'/js/json2.js',
'/js/json-lint.js',
'/js/jsonlint.js',
'/js/lorem-ipsum.js',
'/js/materialize.js',
'/js/visual-difference.js',
'/bower_components/codemirror/lib/codemirror.js',
'/bower_components/codemirror/mode/javascript/javascript.js',
'/bower_components/codemirror/mode/css/css.js',
'/bower_components/codemirror/addon/edit/matchbrackets.js',
'/bower_components/codemirror/addon/comment/continuecomment.js',
'/bower_components/codemirror/addon/comment/comment.js',
'/bower_components/ft-csslint/dist/csslint.js',
'/bower_components/jquery/dist/jquery.slim.min.js',
'/bower_components/kanye-ipsum/dist/jquery.kanye-ipsum.min.js',
'/bower_components/codemirror/lib/codemirror.css',
'/index.html',
'/css-lint.html',
'/ipsum-generator.html',
'/json-lint.html',
'/visual-difference.html',
'/notfound.html',
'/404.html'
];
self.addEventListener('install', (e) => {
console.log('[ServiceWorker] Install');
e.waitUntil(
caches.open(cacheName).then((cache) => {
console.log('[ServiceWorker] Caching app shell');
return cache.addAll(filesToCache);
})
);
});
self.addEventListener('activate', (e) => {
console.log('[ServiceWorker] Activate');
e.waitUntil(
caches.keys().then(function(keyList) {
return Promise.all(keyList.map((key) => {
if (key !== cacheName) {
console.log('[ServiceWorker] Removing old cache', key);
return caches.delete(key);
}
}));
})
);
});
self.addEventListener('fetch', (e) => {
console.log('[ServiceWorker] Fetch', e.request.url);
e.respondWith(
caches.match(e.request).then((response) => {
return response || fetch(e.request);
})
);
});
Then all files were not cached because of this. But if I remove the 404.html or rename it to other name the service worker will work fine and all files will be cached. It is also weird that in my local server the service worker works and caches 404.html but it fails in firebase.
Why 404.html causes uncaught error during service worker caching? How do I resolve this?
Cache.addAll() is an all or nothing API. If any response is not in the 200 HTTP status code range, nothing will be cached.
cache.addAll will reject if any of the resources fail to cache. This means the service worker will only install if all of the resources in cache.addAll have been cached.
Firebase returns 404 Not Found for the /404.html file.
An approach to resolve this is to have a file /notfound.html like you have and then return that in fetch when needed.
You cannot add 404 error html page into cache, if it returns 404 https status (as it should).
But still you can create 404 error page with service worker and use it for sites not in cache even if website is in offline mode.
Using catch() after fetch() request and create whole new Response
minimalistic example:
// self is ServiceWorkerGlobalScope
self.addEventListener( 'fetch', function ( /** #type {FetchEvent} */ event )
{
//const caches = ( event.target.caches );
event.respondWith(
caches.match( event.request ).then( ( /** #type {Response | undefined} */ response ) =>
{
if ( response ) {
return response;
}
return fetch( event.request.clone() ).then( ( /** #type {Response} */ response ) =>
{
//…
return response;
}
).catch( () =>
{
return new Response( '<h1>404 - Not found</h1><p>more HTML here …</p>', {
status: 404,
statusText: 'Not found',
headers: new Headers( {
'Content-Type': 'text/html'
} )
} );
} );
}
)
);
} );
I have created a server side route (using iron-router). Code is as follows :
Router.route( "/apiCall/:username", function(){
var id = this.params.username;
},{ where: "server" } )
.post( function(req, res) {
// If a POST request is made, create the user's profile.
//check for legit request
console.log('post detected')
var userId = Meteor.users.findOne({username : id})._id;
})
.delete( function() {
// If a DELETE request is made, delete the user's profile.
});
This app is running on port 3000 on my local. Now I have created another dummy app running on port 5000. Frrom the dummy app, I am firing a http.post request and then listening it on the app on 3000 port. I fire the http.post request via dummy app using the below code :
apiTest : function(){
console.log('apiTest called')
HTTP.post("http://192.168.1.5:3000/apiCall/testUser", {
data: [
{
"name" : "test"
}
]
}, function (err, res) {
if(!err)
console.log("succesfully posted"); // 4
else
console.log('err',err)
});
return true;
}
But I get the following error on the callback :
err { [Error: socket hang up] code: 'ECONNRESET' }
Not able to figure out whats the problem here.
The server side route is successfully called, but the .post() method is not being entered.
Using meteor version 1.6
192.168.1.5 is my ip addr
Okay so if I use Router.map function, the issue is resolved.
Router.map(function () {
this.route("apiRoute", {path: "/apiCall/:username",
where: "server",
action: function(){
// console.log('------------------------------');
// console.log('apiRoute');
// console.log((this.params));
// console.log(this.request.body);
var id = this.params.username;
this.response.writeHead(200, {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
});
if (this.request.method == 'POST') {
// console.log('POST');
var user = Meteor.users.findOne({username : id});
// console.log(user)
if(!user){
return 'no user found'
}
else{
var userId = user._id;
}
}
});
});
It looks like the content type is not set the application/json. So you should do that...
Setting the "Content-Type" header in HTTP.call on client side in Meteor
I'm having difficulty invoking a login method, it follows
$ meteor list
Accounts-base 1.2.14 A user account system
Ecmascript 0.6.1 Compiler plugin that supports ES2015 + in all .js files
Meteor-base 1.0.4 Packages that every Meteor app needs
React 15.0.1 Everything you need to use React with Meteor.
Static-html 1.1.13 Defines static page content in .html files
/server/main.js
import { Accounts } from 'meteor/accounts-base'
Accounts.registerLoginHandler('simples', (ttt) => {
console.log(ttt);
});
/client/main.js
autenticar(){
Accounts.callLoginMethod({
methodName: 'simples',
methodArguments: [{ tipo : 'simples' }],
validateResult: function (result) {
console.log('result', result);
},
userCallback: function(error) {
if (error) {
console.log('error', error);
}
}
})
}
When calling authenticar(), I get this error:
errorClass
Details: undefined
Error: 404
ErrorType: "Meteor.Error"
Message: "Method 'simples' not found [404]"
Reason: "Method 'simples' not found"
Where is the error?
I've never used this API personally, but from a quick glance through the Meteor internals, I see a couple issues.
Accounts.registerLoginHandler only adds an additional handler to an array of built-in handlers which are called as part of the default Meteor login process.
If you are trying to plug in an additional handler into the existing process, you should call Accounts.callLoginMethod without the methodName key.
Calling Accounts.callLoginMethod with methodName will bypass the built-in handlers completely and replace them with your custom method, however this method needs to be declared separately by you with Meteor.methods, not registerLoginHandler.
So, that's probably your error -- you need to define your simples method with Meteor.methods. Also, you should check the code for the requirements of this method, see the comments in the code here:
https://github.com/meteor/meteor/blob/devel/packages/accounts-base/accounts_client.js
Only to complement and keep as a referral for someone else to get here. That way it's working
client.js
Accounts.callLoginMethod({
methodArguments: [{tipo: 'simples'}],
validateResult: (result) => {
console.log('success', result);
},
userCallback: function(error) {
if (error) {
console.log('error', error);
}
}
});
server.js
Meteor.startup(function () {
var config = Accounts.loginServiceConfiguration.findOne({
service : 'simples'
});
if (!config) {
Accounts.loginServiceConfiguration.insert({ service: 'simples' });
}
});
Accounts.registerLoginHandler((opts) => {
if(opts.tipo === 'simples'){
return Accounts.updateOrCreateUserFromExternalService ('simples', {
id: 0 // need define something
}, {
options : 'optional'
})
}
});
I'm using Firebase Cloud Messaging + Service worker to handle background push notifications.
When the notification (which contains some data + a URL) is clicked, I want to either:
Focus the window if it's already on the desired URL
Navigate to the URL and focus it if there is already an active tab open
Open a new window to the URL if neither of the above conditions are met
Points 1 and 3 work with the below SW code.
For some reason point #2 isn't working. The client.navigate() promise is being rejected with:
Uncaught (in promise) TypeError: Cannot navigate to URL: http://localhost:4200/tasks/-KMcCHZdQ2YKCgTA4ddd
I thought it might be due to a lack of https, but from my reading it appears as though localhost is whitelisted while developing with SW.
firebase-messaging-sw.js:
// Give the service worker access to Firebase Messaging.
// Note that you can only use Firebase Messaging here, other Firebase libraries
// are not available in the service worker.
importScripts('https://www.gstatic.com/firebasejs/3.5.3/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/3.5.3/firebase-messaging.js');
// Initialize the Firebase app in the service worker by passing in the
// messagingSenderId.
firebase.initializeApp({
'messagingSenderId': 'XXXX'
});
const messaging = firebase.messaging();
messaging.setBackgroundMessageHandler(payload => {
console.log('[firebase-messaging-sw.js] Received background message ', payload);
let notificationData = JSON.parse(payload.data.notification);
const notificationOptions = {
body: notificationData.body,
data: {
clickUrl: notificationData.clickUrl
}
};
return self.registration.showNotification(notificationData.title,
notificationOptions);
});
self.addEventListener('notificationclick', event => {
console.log('[firebase-messaging-sw.js] Notification OnClick: ', event);
// Android doesn’t close the notification when you click on it
// See: http://crbug.com/463146
event.notification.close();
// This looks to see if the current is already open and
// focuses if it is
event.notification.close();
let validUrls = /localhost:4200/;
let newUrl = event.notification.data.clickUrl || '';
function endsWith(str, suffix) {
return str.indexOf(suffix, str.length - suffix.length) !== -1;
}
event.waitUntil(
clients.matchAll({
includeUncontrolled: true,
type: 'window'
})
.then(windowClients => {
for (let i = 0; i < windowClients.length; i++) {
let client = windowClients[i];
if (validUrls.test(client.url) && 'focus' in client) {
if (endsWith(client.url, newUrl)) {
console.log('URL already open, focusing.');
return client.focus();
} else {
console.log('Navigate to URL and focus', client.url, newUrl);
return client.navigate(newUrl).then(client => client.focus());
}
}
}
if (clients.openWindow) {
console.log('Opening new window', newUrl);
return clients.openWindow(newUrl);
}
})
);
});
The vast majority of my SW code is taken from:
https://gist.github.com/vibgy/0c5f51a8c5756a5c408da214da5aa7b0
I'd recommend leaving out includeUncontrolled: true from your clients.matchAll().
The WindowClient that you're acting on might not have the current service worker as its active service worker. As per item 4 in the specification for WindowClient.navigate():
If the context object’s associated service worker client’s active
service worker is not the context object’s relevant global object’s
service worker, return a promise rejected with a TypeError.
If you can reproduce the issue when you're sure the client is currently controlled by the service worker, then there might be something else going on, but that's what I'd try as a first step.
This worked for me:
1- create an observable and make sure not to call the messaging API before it resolves.
2- register the service worker yourself, and check first if its already registered
3- call event.waitUntil(clients.claim()); in your service worker
private isMessagingInitialized$: Subject<void>;
constructor(private firebaseApp: firebase.app.App) {
navigator.serviceWorker.getRegistration('/').then(registration => {
if (registration) {
// optionally update your service worker to the latest firebase-messaging-sw.js
registration.update().then(() => {
firebase.messaging(this.firebaseApp).useServiceWorker(registration);
this.isMessagingInitialized$.next();
});
}
else {
navigator.serviceWorker.register('firebase-messaging-sw.js', { scope:'/'}).then(
registration => {
firebase.messaging(this.firebaseApp).useServiceWorker(registration);
this.isMessagingInitialized$.next();
}
);
}
});
this.isMessagingInitialized$.subscribe(
() => {
firebase.messaging(this.firebaseApp).usePublicVapidKey('Your public api key');
firebase.messaging(this.firebaseApp).onTokenRefresh(() => {
this.getToken().subscribe((token: string) => {
})
});
firebase.messaging(this.firebaseApp).onMessage((payload: any) => {
});
}
);
}
firebase-messaging-sw.js
self.addEventListener('notificationclick', function (event) {
event.notification.close();
switch (event.action) {
case 'close': {
break;
}
default: {
event.waitUntil(clients.claim());// this
event.waitUntil(clients.matchAll({
includeUncontrolled: true,
type: "window"
}).then(function (clientList) {
...
clientList[i].navigate('you url');
...
}
}
}
}