Lighthouse PWA audit returns a "start_url does not respond with a 200 when offline" error - progressive-web-apps

I'm having an issue with Lighthouse's PWA audit. I'm using a Service Worker sw.js that successfully caches both the offline.html fallback (for when the user has no network connection), and the start.html (which is defined as start_url in the manifest.json and is displayed if the user opens the website from the Homescreen icon).
The issue happens when I use Lighthouse to validate the PWA checklist, which throws this (only) error:
start_url does not respond with a 200 when offline
The start_url did respond, but not via a service worker.
I find this odd, because start.html is properly cached upon the service worker install process. My only guess is that the validator is trying to access start.html in offline mode before the service worker can actually cache it to the browser storage.
So, how can I validate that particular issue in Lighthouse's PWA checklist?
Here's my current code:
manifest.json
{
"name": "My Basic Example",
"short_name": "Example",
"icons": [
{
"src": "https://example.com/static/ico/manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
}
],
"start_url": "https://example.com/start.html",
"scope": "/",
"display": "standalone",
"orientation": "portrait",
"background_color": "#2196f3",
"theme_color": "#2196f3"
}
core.js
if('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js', {
scope: '/'
}).then(function(registration) {
}).catch(function(err) {
});
navigator.serviceWorker.ready.then(function(registration) {
});
}
sw.js
//cache container
const CACHE_VERSION = 1;
const CACHE_NAME = 'cache-v' + CACHE_VERSION;
//resources
const URL_OFFLINE = 'offline.html';
const URL_START = 'start.html';
//install
self.addEventListener('install', (event) => {
event.waitUntil(
(async () => {
const cache = await caches.open(CACHE_NAME);
await Promise.all([
cache.add(new Request(URL_OFFLINE, { cache: 'reload' })),
cache.add(new Request(URL_START, { cache: 'reload' }))
]);
})()
);
//force the waiting service worker to become the active service worker
self.skipWaiting();
});
//activate
self.addEventListener('activate', (event) => {
event.waitUntil(
(async () => {
//enable navigation preload if it is supported.
//https://developers.google.com/web/updates/2017/02/navigation-preload
if('navigationPreload' in self.registration) {
await self.registration.navigationPreload.enable();
}
})()
);
//tell the active service worker to take control of the page immediately
self.clients.claim();
});
//fetch
self.addEventListener('fetch', (event) => {
//we only want to call event.respondWith() if this is a navigation request for an HTML page
if(event.request.mode === 'navigate') {
event.respondWith((async () => {
try {
//first, try to use the navigation preload response if it's supported
const preload_response = await event.preload_response;
if(preload_response) {
return preload_response;
}
//always try the network first
const network_response = await fetch(event.request);
return network_response;
} catch (error) {
//catch is only triggered if an exception is thrown, which is likely due to a network error
const cache = await caches.open(CACHE_NAME);
if(event.request.url.includes(URL_START)) {
return await cache.match(URL_START);
}
return await cache.match(URL_OFFLINE);
}
})());
}
});
Any ideas? Thanks!

#PeteLe, I tried your sample and got following failure:
Navigated to https://basic-pwa-so-1.glitch.me/
The service worker navigation preload request failed with network error: net::ERR_INTERNET_DISCONNECTED.
Looks like something was updated in Lighthouse generating the error message

Related

vue3 App.vue mounted not running first before a compoment View being created

I created FunA in App.vue Mounted(). I created FunB in HomeView.vue Mounted().
I go to HomeView Page and Refresh the page, FunB run before FunA.
My Question is isn't App.vue Stuff run first before the compoment stuff ? I created FunC in App.vue beforeCreate(), and go back HomeView page and refresh again. I saw that Fun B still run before FunC and FunA.
If I am using FunA as a global function to setup Axios Auth Headers, all compoment Mounted functions with Axios get will return 403 error.
All stuff is working correctly if I enter the view by router-view.
What should I do except adding the auth headers to all Axios Request.
App.vue
async mounted() {
await this.getServerTokenAuth()
},
methods: {
async getServerTokenAuth(){
await this.$mainApi.post('api/auth/', {
"username": "qweqwe",
"password": "qweqweqwe"
})
.then(response => {
console.log(response.data.token)
this.$store.commit("setServerToken")
this.$mainApi.defaults.headers.common['Authorization'] = "Token " + response.data.token;
this.$mainApi.defaults.headers.common['Accept-Language'] = this.$store.state.language;
if(this.$store.state.isAuthenticated === true){
this.$mainApi.defaults.headers.common['CUSTOM_HEADERS'] = "qweqweqwe";
console.log("User Data Added to Headers")
}
})
},
HomeView
async mounted() {
await this.getBackendData() // return 403 error
},
methods: {
async getBackendData(){
// try to console log the Auth Headers, return undefined
await this.$mainApi.get('api/product/list/')
.then(response => {
if(response.data.status === 80){
this.tabulator.setData(response.data.details)
this.totalProduct = response.data.details.length
}
})
}
},
I think using AwaitLock is a solution in this case. The HomeView mounted hook will be locked until the App mounted hook is done.
I found the method to solve.
How to use async/await in vue lifecycle hooks with vuex?
Just Add a flag in App.vue's api Function.
async getServerTokenAuth(){
await this.$mainApi.post('api/auth/', {
"username": "qweqwe",
"password": "qweqwe"
})
.then(response => {
...
this.doneApiSetting = true // ADD THIS
In App.vue Template
<template>
// Add a Flag here, so this part will run after the API setting is complete
<div class="main" v-if="doneApiSetting">
<router-view/>
</div>
</template>

After successfully deploying Next.js app on AWS Amplify, https://www.example.com/api/any-route showing below error in console

The app is deployed successfully but the API routes (/pages/api) are not working as expected, showing below error in the console.
Build is successful and deployed on aws-amplify, I have added environment variables correctly, don't know why this is happening?
Does aws-amplify doesn't support serverless functions writer inside /api folder??
{
"error": {
"message": "connect ECONNREFUSED 127.0.0.1:80",
"name": "Error",
"stack": "Error: connect ECONNREFUSED 127.0.0.1:80\n at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1148:16)",
"config": {
"url": "undefined/campaigns",
"method": "get",
"headers": {
"Accept": "application/json, text/plain, */*",
"User-Agent": "axios/0.21.4"
},
"auth": {},
"transformRequest": [null],
"transformResponse": [null],
"timeout": 0,
"xsrfCookieName": "XSRF-TOKEN",
"xsrfHeaderName": "X-XSRF-TOKEN",
"maxContentLength": -1,
"maxBodyLength": -1,
"transitional": {
"silentJSONParsing": true,
"forcedJSONParsing": true,
"clarifyTimeoutError": false
}
},
"code": "ECONNREFUSED"
}
}
Here is the code
import axios from 'axios';
import Cors from 'cors';
import rateLimit from '../../../utils/rate-limit';
import initMiddleware from '../../../lib/init-middleware';
// Initialize the cors middleware
const cors = initMiddleware(
Cors({
methods: ['POST'],
origin: ['https://www.example.com', /\.example\.com$/],
})
);
const limiter = rateLimit({
interval: 60 * 1000, // 60 seconds
uniqueTokenPerInterval: 500, // Max 500 users per second
});
const handler = async (req, res) => {
await cors(req, res);
if (req.method === 'GET') {
try {
await limiter.check(res, 50, 'CACHE_TOKEN');
const { data } = await axios.get(`${process.env.BASE_URL}/campaigns`, {
auth: {
username: process.env.MAIL_SERVER_USERNAME,
password: process.env.MAIL_SERVER_PASSWORD,
},
});
return res.status(200).json(data);
} catch (error) {
return res.status(429).json({ error });
}
} else {
try {
await limiter.check(res, 10, 'CACHE_TOKEN'); // 10 requests per minute
return res.status(200).json('not allowed');
} catch (err) {
return res.status(429).json({ error: 'Rate limit exceeded' });
}
}
};
export default handler;
I figured out that environment variables are not getting reflected, so did some googling; found this solution and it worked for me.
Add your desired environment variable in the Amplify Console-like normal (steps)
Update (or create) your next.config.js file with the environment variable you added in the Amplify Console. E.g if you created an environment variable named MY_ENV_VAR in the console in step 1) above, then you would add the following:
module.exports = {
env: {
MY_ENV_VAR: process.env.MY_ENV_VAR
}
};
Now after your next build you will be able to reference your environment variable (process.env.MY_ENV_VAR) in your SSR lambdas!
Here is the link: Github
Ran into the same problem Amplify only supports NextJS 11 at the moment. If you go with the default settings it will use the latest NextJS 12 and /api routes wont work, they return a 503 with a cloudformation permission error.
Specify next 11.1.3 in your package.json
https://aws.amazon.com/about-aws/whats-new/2021/08/aws-amplify-hosting-support-next-js-version-11/
I also faced that problem. Amplify does not support v12 of next. Simply downgrade your next.js version in package.json to v1.1.3 and your routes will work as normal.
Best regrets.
Artem Meshkov

Firebase rules test does not pass even when using lockdown

I'm trying to test my firebase rules, but they seem to not pass even when I use lock down mode. I followed the guide at https://firebase.google.com/docs/firestore/security/test-rules-emulator
const firebase = require('#firebase/rules-unit-testing');
const fs = require('fs');
const projectId = 'test-judge';
function getAuthedFirestore(auth) {
return firebase.initializeAdminApp({
projectId: projectId,
auth: auth
}).firestore();
}
beforeEach(async () => {
await firebase.clearFirestoreData({ projectId });
});
before(async () => {
const rules = fs.readFileSync('firestore.rules', 'utf8');
await firebase.loadFirestoreRules({
projectId: projectId,
rules: rules
});
});
after(async () => {
await Promise.all(firebase.apps().map(app => app.delete()));
});
describe('locked down', () => {
it("require users to log in before creating a profile", async () => {
const db = getAuthedFirestore(null);
const profile = db.collection("users").doc("alice");
await firebase.assertFails(profile.set({ birthday: "January 1" }));
});
});
here is my firebase.json
{
"firestore": {
"rules": "firestore.rules",
"indexes": "firestore.indexes.json"
}
}
and my package.json
{
"devDependencies": {
"firebase-admin": "^9.11.0",
"#firebase/app": "^0.6.29",
"#firebase/rules-unit-testing": "^1.3.12",
"mocha": "^9.0.3",
"fs-extra": "^10.0.0"
},
"scripts": {
"test": "mocha"
}
}
and here is firestore.rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
}
}
It doesn't seem to me like I'm doing anything wrong, but if I run npm test the test fails. I expect it to pass since asserFails is used and in the rules I return false
I should not be able to set the value the test should pass
here is my output
Warning: FIRESTORE_EMULATOR_HOST not set, using default value localhost:8080
locked down
1) require users to log in before creating a profile
0 passing (324ms)
1 failing
1) locked down
require users to log in before creating a profile:
Error: Expected request to fail, but it succeeded.
at C:\Users\Moneer\Desktop\judge_rules\node_modules\#firebase\rules-unit-testing\dist\index.cjs.js:581:31
at async Context.<anonymous> (test\test.js:33:9)
npm ERR! Test failed. See above for more details.
This is expected behavior, Security Rules are what protect your backend services from malicious Client requests. The "AdminApp" is in relation to the Admin-SDK which is a service tool that interacts with the Firebase services directly behind the Security Rules.
For reference, you will notice that the Admin-SDK would normally require Service Account credentials which allows the SDK to authenticate with the GCP IAM services
Just realized I should be using initializeTestApp not initializeAdminApp

Service workers "sync" operation is working while its offline?

I have a PWA project where I send the data to server. During this process, if the user is offline then the data is stored in indexedDb and a sync tag is registered. So, then when the user comes online that data can sent to the server.
But In my case the sync event gets executed immediately when the we register a sync event tag, which means the data is tried to be sent to server while its offline, which is not going to work.
I think the sync event supposed to fire while its online only, what could be issue here ?
The service worker's sync event works accordingly when I tried to enable and disable the offline option of chrome devtools, and also works correctly in my android phone.
This is how I register my sync tag
function onFailure() {
var form = document.querySelector("form");
//Register the sync on post form error
if ('serviceWorker' in navigator && 'SyncManager' in window) {
navigator.serviceWorker.ready
.then(function (sw) {
var post = {
datetime1: form.datetime1.value,
datetime: form.datetime.value,
name: form.name.value,
image: form.url.value,
message: form.comment.value
};
writeData('sync-comments', post)
.then(function () {
return sw.sync.register('sync-new-comment');
})
.then(function () {
console.log("[Sync tag registered]");
})
.catch(function (err) {
console.log(err);
});
});
}
}
And this is how the sync event is called
self.addEventListener('sync', function (event) {
console.log("[Service worker] Sync new comment", event);
if (event.tag === 'sync-new-comment') {
event.waitUntil(
readAllData('sync-comments')
.then(function (data) {
setTimeout(() => {
data.forEach(async (dt) => {
const url = "/api/post_data/post_new_comment";
const parameters = {
method: 'POST',
headers: {
'Content-Type': "application/json",
'Accept': 'application/json'
},
body: JSON.stringify({
datetime: dt.datetime,
name: dt.name,
url: dt.image,
comment: dt.message,
datetime1: dt.datetime1,
})
};
fetch(url, parameters)
.then((res) => {
return res.json();
})
.then(response => {
if (response && response.datetimeid) deleteItemFromData('sync-comments', response.datetimeid);
}).catch((error) => {
console.log('[error post message]', error.message);
})
})
}, 5000);
})
);
}
});
you mention
The service worker's sync event works accordingly when I tried to enable and disable the offline option of chrome devtools, and also works correctly in my android phone.
So I'm not sure which case is the one failing.
You are right that the sync will be triggered when the browser thinks the user is online, if the browser detects that the user is online at the time of the sync registration it will trigger the sync:
In true extensible web style, this is a low level feature that gives you the freedom to do what you need. You ask for an event to be fired when the user has connectivity, which is immediate if the user already has connectivity. Then, you listen for that event and do whatever you need to do.
Also, from the workbox documentation
Browsers that support the BackgroundSync API will automatically replay failed requests on your behalf at an interval managed by the browser, likely using exponential backoff between replay attempts.

Service Worker not working when hosted, but works on localhost

I'm working on a PWA and I'm facing an issue with the service worker and I can't figure out what's wrong.
So when I run the lighthouse audit on localhost, it passes every criteria except for the HTTPS one. You can view it below;
However, when I publish the code to my github pages, and run the same audit there, the service worker is never activated. It gives me the error. The status becomes 'redundant' when I run the audit online.
Link: https://ibr4h1m.github.io/MAD5/index.html
Below I'll show the code, which is the exact same on the website that I've mentioned above.
main.js:
//Simple ServiceWorker
if('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js');
};
sw.js
const cacheName = 'tourguide-site';
const appShellFiles = ['index.html',
'help.html',
'destinations.html',
'contact.html',
'js/main.js',
'css/style.css',
'sw.js'
];
self.addEventListener('install', (e) => {
console.log('[Service Worker] Install');
e.waitUntil((async () => {
const cache = await caches.open(cacheName);
console.log('[Service Worker] Caching all: app shell and content');
await cache.addAll(appShellFiles);
})());
});
// Simple Activate since the other one is BS
self.addEventListener('activate', function () {
console.log('SW Activated');
});
self.addEventListener('fetch', (e) => {
e.respondWith((async () => {
const r = await caches.match(e.request);
console.log(`[Service Worker] Fetching resource: ${e.request.url}`);
if (r) { return r; }
const response = await fetch(e.request);
const cache = await caches.open(cacheName);
console.log(`[Service Worker] Caching new resource: ${e.request.url}`);
cache.put(e.request, response.clone());
return response;
})());
});
Online audit:
const appShellFiles = ['index.html',
'help.html',
'destinations.html',
'contact.html',
'js/main.js',
'css/style.css',
'sw.js'
];
Remove the sw.js from your appShellFiles