Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auth service worker doesn't store firebaseConfig options between restarts #295

Open
rlw87 opened this issue May 27, 2024 · 18 comments · May be fixed by #307
Open

Auth service worker doesn't store firebaseConfig options between restarts #295

rlw87 opened this issue May 27, 2024 · 18 comments · May be fixed by #307
Assignees

Comments

@rlw87
Copy link

rlw87 commented May 27, 2024

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

@ESRuth
Copy link

ESRuth commented May 27, 2024

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.

@ysaied631
Copy link

Any luck with solving this?

@Shahzad6077
Copy link

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

@rlw87
Copy link
Author

rlw87 commented Jun 5, 2024

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.

@fsiatama
Copy link

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.

@timoteioros
Copy link

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.

@rashid4lyf
Copy link

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

@KirillSkomarovskiy
Copy link

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

@heckchuckman
Copy link

heckchuckman commented Jul 30, 2024

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!

@piotrsliwka333
Copy link

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

@leandroz
Copy link

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

@pashpashpash
Copy link

Same problem here!

@pashpashpash
Copy link

pashpashpash commented Aug 16, 2024

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
Copy link

pashpashpash commented Aug 16, 2024

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.

@adrolc
Copy link

adrolc commented Aug 19, 2024

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));
});
//...

@chrisstayte
Copy link

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

@alex0916
Copy link

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 👍🏽

@RonakDoshiTMI
Copy link

+1

@jamesdaniels jamesdaniels linked a pull request Sep 5, 2024 that will close this issue
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.