friendlyeats-web icon indicating copy to clipboard operation
friendlyeats-web copied to clipboard

Auth service worker doesn't store firebaseConfig options between restarts

Open rlw87 opened this issue 1 year ago • 18 comments

Description If the auth service worker is restarted, it loses the firebaseConfig options that it was given at registration time and throws an exception.

Reproduction Steps

  1. Open the web page for the first time. You'll see the log "Service worker installed with Firebase config" in the console
  2. Open developer tools, and under the Applications > Service workers tab, within auth-service-worker.js click "Stop"
  3. Refresh the page

Expected The service worker starts up again and the website loads as normal

Actual You see the following error in the console and the page doesn't load

FirebaseError: Firebase: Need to provide options, when not being deployed to hosting via source. (app/no-options).

I'm new to working with service workers so I might be missing something, but it looks to me as though the service worker should be storing the firebaseConfig somewhere other than in memory, so it can be reloaded on restart of the service worker. It looks like due to the firebaseConfig variable only being populated on 'install' event, it will never get the required configuration again unless you either manually unregister it and let it re-install, or it is updated to a newer version.

https://github.com/firebase/friendlyeats-web/blob/41b0a4dbfca6c106d926fd5e65db53577f99ea75/nextjs-end/auth-service-worker.js#L6C5-L6C19

rlw87 avatar May 27 '24 14:05 rlw87

I used the Development Tool to "Update on reload" which fixed the refresh issue, but that isn't a setting I can have my users change.

ESRuth avatar May 27 '24 15:05 ESRuth

Any luck with solving this?

ysaied631 avatar Jun 03 '24 00:06 ysaied631

Hey @ESRuth, would you able to resolve this issue? Thanks

Shahzad6077 avatar Jun 04 '24 14:06 Shahzad6077

I've just included the firebase config in the service worker itself, rather than having the app pass it the config when it's installed. None of the values should change so I don't see why it should be a problem.

rlw87 avatar Jun 05 '24 12:06 rlw87

I am experiencing problems with the service worker, which fails upon page reload, sometimes after a few hours, or when the page is opened in a new browser tab.

fsiatama avatar Jun 14 '24 20:06 fsiatama

Having the same problem. Didn't clone the friendlyeats project, I copy pasted the auth code into my Next.js proiect and I have the same issue with the service worker. Still searching for a permanent solution.

timoteioros avatar Jun 22 '24 15:06 timoteioros

Having the same issue here as well. Hardcoding the values stops it from crashing however, obviously this is not ideal.

rashid4lyf avatar Jun 29 '24 19:06 rashid4lyf

I used Cache API, and it solved the issue of retaining the firebase config between runs.

import { initializeApp } from "firebase/app";
import { getAuth, getIdToken } from "firebase/auth";
import { getInstallations, getToken } from "firebase/installations";

// region variables
const CACHE_NAME = 'config-cache-v1';
/** @type {FirebaseOptions | undefined} */
let CONFIG;
// endregion
// region listeners
self.addEventListener('install', (event) => {
  // extract firebase config from query string
  const serializedFirebaseConfig = new URL(location).searchParams.get('firebaseConfig');

  if (!serializedFirebaseConfig) {
    throw new Error('Firebase Config object not found in service worker query string.');
  }

  self.skipWaiting();
  event.waitUntil(saveConfig(serializedFirebaseConfig));
});

self.addEventListener("activate", (event) => {
  event.waitUntil(clients.claim());
});

self.addEventListener("fetch", (event) => {
  const { origin } = new URL(event.request.url);

  if (origin !== self.location.origin) return;

  event.respondWith(fetchWithFirebaseHeaders(event.request));
});
// endregion
// region functions
/**
 * @return string
 * */
function getConfigUrl() {
  return `${self.location.origin}/firebase-config`;
}

/**
 * @param {string} config
 *
 * return Promise<void>
 * */
async function saveConfig(config) {
  const cache = await caches.open(CACHE_NAME);

  const response = new Response(config, {
    headers: { 'Content-Type': 'application/json' }
  });

  await cache.put(getConfigUrl(), response);
}

/**
 * @param {Request} request
 *
 * @return Response
 * */
async function fetchWithFirebaseHeaders(request) {
  const config = await getConfig();

  if (!config) {
    return await fetch(request);
  }

  const app = initializeApp(config);
  const auth = getAuth(app);
  const installations = getInstallations(app);
  const headers = new Headers(request.headers);
  const [authIdToken, installationToken] = await Promise.all([
    getAuthIdToken(auth),
    getToken(installations),
  ]);
  headers.append("Firebase-Instance-ID-Token", installationToken);

  if (authIdToken) headers.append("Authorization", `Bearer ${authIdToken}`);

  const newRequest = new Request(request, { headers });

  return await fetch(newRequest);
}

/**
 * @param {Auth} auth
 *
 * @return Promise<string | undefined>
 * */
async function getAuthIdToken(auth) {
  await auth.authStateReady();

  if (!auth.currentUser) return;

  return await getIdToken(auth.currentUser);
}

/**
 * @return FirebaseOptions | undefined
 * */
async function getConfig() {
  if (CONFIG) return CONFIG;

  const cache = await caches.open(CACHE_NAME);
  const configResponse = await cache.match(getConfigUrl());

  if (!configResponse) {
    return;
  }

  const config = await configResponse.json();
  CONFIG = config;

  return CONFIG;
}
// endregion

KirillSkomarovskiy avatar Jul 25 '24 14:07 KirillSkomarovskiy

Hi @KirillSkomarovskiy - thanks for this. I'm still getting the error:

FirebaseError: Firebase: Need to provide options, when not being deployed to hosting via source. (app/no-options).

@rlw87 can you add a code snippet of your approach?

Thanks all!

heckchuckman avatar Jul 30 '24 17:07 heckchuckman

Same problem from my side, just by coping codelab-friendlyeats-web and deploying it.

piotrsliwka333 avatar Aug 14 '24 13:08 piotrsliwka333

Same here, it is crazy how hard it is to find a working example.

leandroz avatar Aug 16 '24 01:08 leandroz

Same problem here!

pashpashpash avatar Aug 16 '24 23:08 pashpashpash

Same problem from my side, just by coping codelab-friendlyeats-web and deploying it.

Not inspiring much confidence in using firebase app hosting for nextjs...

pashpashpash avatar Aug 16 '24 23:08 pashpashpash

import { initializeApp } from "firebase/app";
import { getAuth, getIdToken } from "firebase/auth";
import { getInstallations, getToken } from "firebase/installations";


// old code (dont include this
// // this is set during install
// let firebaseConfig;

// self.addEventListener('install', event => {
//   // extract firebase config from query string
//   const serializedFirebaseConfig = new URL(location).searchParams.get('firebaseConfig');
  
//   if (!serializedFirebaseConfig) {
//     throw new Error('Firebase Config object not found in service worker query string.');
//   }
  
//   firebaseConfig = JSON.parse(serializedFirebaseConfig);
//   console.log("Service worker installed with Firebase config", firebaseConfig);
// });


// Default hardcoded Firebase configuration -- put your config here

let firebaseConfig = {
  apiKey: "xxxxxxxxxxxxxxxxxx",
  authDomain: "xxxxxxxxxxxxxxxxx",
  projectId: "xxxxxxxxxxxxxxxxxxx",
  storageBucket: "xxxxxxxxxxxxxxxxxxx",
  messagingSenderId: "xxxxxxxxxxxxxxxxxxxxxxx",
  appId: "xxxxxxxxxxxxxxxxxxxxxxx",
  measurementId: "xxxxxxxxxxxxxxxxxxxxxxxxxxx"
};

// Handle the 'install' event and extract the firebaseConfig from the query string if present
self.addEventListener('install', event => {
  const serializedFirebaseConfig = new URL(location).searchParams.get('firebaseConfig');

  if (serializedFirebaseConfig) {
    try {
      firebaseConfig = JSON.parse(serializedFirebaseConfig);
      console.log("Service worker installed with Firebase config from query string", firebaseConfig);
    } catch (error) {
      console.error("Failed to parse Firebase config from query string", error);
    }
  } else {
    console.log("Service worker installed with hardcoded Firebase config", firebaseConfig);
  }
});

self.addEventListener("fetch", (event) => {
  console.log("Fetching with Firebase config:", firebaseConfig);
  const { origin } = new URL(event.request.url);
  if (origin !== self.location.origin) return;
  event.respondWith(fetchWithFirebaseHeaders(event.request));
});

async function fetchWithFirebaseHeaders(request) {
  const app = initializeApp(firebaseConfig);
  const auth = getAuth(app);
  const installations = getInstallations(app);
  const headers = new Headers(request.headers);
  const [authIdToken, installationToken] = await Promise.all([
    getAuthIdToken(auth),
    getToken(installations),
  ]);
  headers.append("Firebase-Instance-ID-Token", installationToken);
  if (authIdToken) headers.append("Authorization", `Bearer ${authIdToken}`);
  const newRequest = new Request(request, { headers });
  return await fetch(newRequest);
}

async function getAuthIdToken(auth) {
  await auth.authStateReady();
  if (!auth.currentUser) return;
  return await getIdToken(auth.currentUser);
}

Then run

npx esbuild auth-service-worker.js --bundle --outfile=public/auth-service-worker.js

to compile the new service worker file and put it in public so that it works locally. For prod, you don't need to worry about this step, it will auto run this as part of the build process.

pashpashpash avatar Aug 16 '24 23:08 pashpashpash

The problem is that the configuration is initialized during the install event. When the service worker is stoped and then resumed, the install event will not be triggered again, leaving firebaseConfig undefined. You need to check in the fetch event whether firebaseConfig is set, if not, extract the config from the query string again:

self.addEventListener("fetch", (event) => {
  if (!firebaseConfig) {
    const serializedFirebaseConfig = new URL(location).searchParams.get(
      "firebaseConfig"
    );
    firebaseConfig = JSON.parse(serializedFirebaseConfig);
  }
  // rest of code
});

But I think you could get rid of the install event and just initialize it globally:

const serializedFirebaseConfig = new URL(location).searchParams.get(
  "firebaseConfig"
);

if (!serializedFirebaseConfig) {
  throw new Error(
    "Firebase Config object not found in service worker query string."
  );
}

const firebaseConfig = JSON.parse(serializedFirebaseConfig);

self.addEventListener("fetch", (event) => {
  const { origin } = new URL(event.request.url);
  if (origin !== self.location.origin) return;
  event.respondWith(fetchWithFirebaseHeaders(event.request));
});
//...

adrolc avatar Aug 19 '24 17:08 adrolc

Did this work for you @adrolc I am having the same issues right now as well.

chrisstayte avatar Aug 22 '24 18:08 chrisstayte

The problem is that the configuration is initialized during the install event. When the service worker is stoped and then resumed, the install event will not be triggered again, leaving firebaseConfig undefined. You need to check in the fetch event whether firebaseConfig is set, if not, extract the config from the query string again:

self.addEventListener("fetch", (event) => {
  if (!firebaseConfig) {
    const serializedFirebaseConfig = new URL(location).searchParams.get(
      "firebaseConfig"
    );
    firebaseConfig = JSON.parse(serializedFirebaseConfig);
  }
  // rest of code
});

But I think you could get rid of the install event and just initialize it globally:

const serializedFirebaseConfig = new URL(location).searchParams.get(
  "firebaseConfig"
);

if (!serializedFirebaseConfig) {
  throw new Error(
    "Firebase Config object not found in service worker query string."
  );
}

const firebaseConfig = JSON.parse(serializedFirebaseConfig);

self.addEventListener("fetch", (event) => {
  const { origin } = new URL(event.request.url);
  if (origin !== self.location.origin) return;
  event.respondWith(fetchWithFirebaseHeaders(event.request));
});
//...

This worked for me 👍🏽

alex0916 avatar Aug 29 '24 18:08 alex0916

+1

RonakDoshiTMI avatar Sep 05 '24 06:09 RonakDoshiTMI