8

I have updated to Firebase v9 a few weeks ago and I have an issue when trying to connect my Firebase App to Firestore Emulator.

firebase.js (my VueJS plugin, where I setup Firebase) :

import { initializeApp, getApps } from "firebase/app"
import { getAuth, connectAuthEmulator, onAuthStateChanged } from "firebase/auth";
import { getFirestore, connectFirestoreEmulator } from "firebase/firestore"
import { getStorage, connectStorageEmulator } from "firebase/storage";
import { getFunctions, connectFunctionsEmulator } from 'firebase/functions';
import { isSupported, getAnalytics } from "firebase/analytics";

export default async ({ app }, inject) => {

  const firebaseConfig = {
    apiKey: process.env.FIREBASE_API_KEY,
    authDomain: process.env.FIREBASE_AUTH_DOMAIN,
    databaseURL: process.env.FIREBASE_DATABASE_URL,
    projectId: process.env.FIREBASE_PROJECT_ID,
    storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
    messagingSenderId: process.env.FIREBASE_MESSAGING_SERVICE_ID,
    appId: process.env.FIREBASE_APP_ID,
    measurementId: process.env.FIREBASE_MEASUREMENT_ID,
  }
  // I've checked, the values of firebaseConfig are all set here.

  // This IF statement is here to avoid initializing the app several times
  const apps = getApps();
  let firebaseApp = null;
  if (!apps.length) {
    firebaseApp = initializeApp(firebaseConfig);
  }
  else {
    firebaseApp = apps[0];
  }

  // INIT AUTH
  const auth = getAuth();
  auth.languageCode = 'fr';
  onAuthStateChanged(auth, async authUser => {
    const claims = authUser ? (await authUser.getIdTokenResult(true)).claims : null;
    await app.store.dispatch('onAuthStateChanged', { authUser, claims });
  },
  (error) => {
    console.error("Firebase Auth onAuthStateChanged ERROR", error)
  });
  
  // Get other services
  const firestore = getFirestore(firebaseApp);
  const storage = getStorage(firebaseApp);
  const functions = getFunctions(firebaseApp, process.env.FIREBASE_REGION);

  // Setup analytics if supported
  let analytics = null;
  const analyticsSupported = await isSupported()
  if (analyticsSupported) {
    analytics = getAnalytics();
    analytics.automaticDataCollectionEnabled = false;
  }

  // Connecting to emulators
  if (process.client && process.env.APP_ENV === 'local') {
    console.log("LOCAL ENVIRONMENT, CONNECTING TO EMULATORS...");
    connectAuthEmulator(auth, "http://localhost:9099");
    connectFirestoreEmulator(firestore, 'localhost', 8080);
    connectStorageEmulator(storage, "localhost", 9199);
    connectFunctionsEmulator(functions, "localhost", 5001);
  }

  Inject firebase objects into my VueJS app
  const fire = { auth, firestore, storage, functions, analytics }
  inject('fire', fire);
}

Here is the error I get, caused by this line : connectFirestoreEmulator(firestore, 'localhost', 8080);

enter image description here

FirebaseError Firestore has already been started and its settings can no longer be changed. You can only modify settings before calling any other methods on a Firestore object.

I am not trying to modify Firestore object's settings property myself, so it has to be the method connectFirestoreEmulator.

The problem can be narrowed down to the following code :

import { initializeApp } from "firebase/app"
import { getFirestore, connectFirestoreEmulator } from "firebase/firestore"

export default async ({ app }, inject) => {

  const firebaseConfig = {
    apiKey: process.env.FIREBASE_API_KEY,
    authDomain: process.env.FIREBASE_AUTH_DOMAIN,
    databaseURL: process.env.FIREBASE_DATABASE_URL,
    projectId: process.env.FIREBASE_PROJECT_ID,
    storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
    messagingSenderId: process.env.FIREBASE_MESSAGING_SERVICE_ID,
    appId: process.env.FIREBASE_APP_ID,
    measurementId: process.env.FIREBASE_MEASUREMENT_ID,
  }

  firebaseApp = initializeApp(firebaseConfig);
  const firestore = getFirestore(firebaseApp);
  if (process.env.APP_ENV === 'local') {
    connectFirestoreEmulator(firestore, 'localhost', 8080);
  }

  const fire = { auth, firestore, storage, functions, analytics };
  inject('fire', fire);
}

I've managed to avoid triggering the error by adding process.client so it doesn't connect to emulators on server-side (SSR) :

  if (process.client && process.env.APP_ENV === 'local') {

However when I add that, the emulators are not connected when code is executed server-side (SSR) on the first page load, and initial Firestore data is being read from the real Firebase App instead of the emulators.

Any idea what can be done to manage proper connection to Firestore emulator on SSR ?

Is this a Firebase bug ?

Versions I use :

  • In my App : Firebase JS SDK v9.6.9
  • Emulators : firebase-tools v10.4.0 for the emulators

What I've already read/tried :

  • Hi Guillaume, I edited the answer for Firebase JS SDK – Jose German Perez Sanchez Mar 29 '22 at 22:33
  • 2
    I stumbled upon the same problem using Next.js with Firebase emulator. Narrowed it down to the `connectFirestoreEmulator` function that causes the issue. Pretty sure it's a bug. Did you find a workaround? – JasonK Oct 20 '22 at 16:55

4 Answers4

5

It's been a while, but I ran into a similar issue, and after a lot of hairpulling, I ended up with a solution (though it feels a little hacky).

Before running the connectFirestoreEmulator line, check if firestor._settingsFrozen is false. So you only run that line basically if Firestore hasn't already been initialized. You can check that firestore is getting initialized with the emulator settings by logging out the firestore variable before the connectFirestoreEmulator line and seeing what the settings say there--if it says port is 8080 and host is localhost, then you're good.

Here's my code for comparison (slightly different setup from yours but I believe we were running into the same issue):

import { initializeApp } from 'firebase/app';
import { connectAuthEmulator, getAuth } from 'firebase/auth';
import { connectFirestoreEmulator, getFirestore } from 'firebase/firestore';

const firebaseConfig = {
  apiKey: "XXXXXXXXX",
  authDomain: "XXXXXXXXX",
  projectId: "XXXXXXXXX",
  storageBucket: "XXXXXXXXX",
  messagingSenderId: "XXXXXXXXX",
  appId: "XXXXXXXXX",
  measurementId: "XXXXXXXXX",
};

const app = initializeApp(firebaseConfig);

export const auth = getAuth(app);
export const db = getFirestore(app);

export default (context) => {
  if (context.env.appEnv === 'dev') {
    connectAuthEmulator(auth, `http://127.0.0.1:${context.env.authPort}`);

    if (!db._settingsFrozen) {
      connectFirestoreEmulator(db, '127.0.0.1', parseInt(context.env.firestorePort));
    }
  }
}
Millan Singh
  • 61
  • 1
  • 6
2

You can try checking the setting.host value of your firebase object in order to check if it is already 'localhost', so you can skip calling the connectFirestoreEmulator() function.

This did happen to me in an Angular application using Hot Module Replacement. I tried to use a global constant, but did not work.

In my case, I'm using AngularFire (https://github.com/angular/angularfire), so I had to do something like this:

// ...

const firestore = getFirestore();

const host = (firestore.toJSON() as { settings?: { host?: string } }).settings?.host ?? '';

// console.log({ host });

if (process.env.APP_ENV === 'local' && !host.startsWith('localhost')) {
 connectFirestoreEmulator(firestore, 'localhost', 8080);
}

// ...

In my case I had to use firestore.toJSON() in order to access the settings property, check how it is in your case.

nelson6e65
  • 917
  • 11
  • 14
0

Reviewing Firebase JS SDK issues related, it seems that the issue is because the Firestore instance (which is initialized like this: firestore = getFirestore(firebaseApp)) is called after the emulator (connectFirestoreEmulator) has been started.

After calling the "connectFirestoreEmulator" method, "firestore" variable is being used in the constant variable "fire = { auth, firestore, storage, functions, analytics }"

If you use "const fire" before connecting to the emulator, the problem may be solved.

Here is a code example that might help you:

firebaseApp = initializeApp(firebaseConfig);

 const fire = { auth, firestore, storage, functions, analytics };

  const firestore = getFirestore(firebaseApp);
  if (process.env.APP_ENV === 'local') {
    connectFirestoreEmulator(firestore, 'localhost', 8080);
  }

As a reference, I used this github repository.

  • Thank you for your reply ! I was talking about the Firebase JS SDK and not Firebase Admin, which are a bit different. – Guillaume Prévost Mar 24 '22 at 13:29
  • Thanks for the update, but unfortunately this solution doesn't work out (I tried it just to be sure and still get the error). I don't think the assignation of `const fire = ...` is responsible : it seems to come from what happens INSIDE the call to `connectFirestoreEmulator` : whenever it's called on server-side I get the error. The issue still remains unsolved :( – Guillaume Prévost Mar 30 '22 at 15:26
  • HMR resets the constant values. You can check the `settings.host` value of `firestore` object before to call `connectFirestoreEmulator()`. I'm using AngularFire, so in my case is something like `const currentHost = (firestore.toJSON() as { settings?: { host?: string } }).settings?.host ?? ''` and then check `if (process.env.APP_ENV === 'local' && !currentHost.startsWith('localhost'))`. – nelson6e65 Dec 08 '22 at 08:27
  • @GuillaumePrévost Check this answer as an example from my Angular application: https://stackoverflow.com/a/74727587/7242535 – nelson6e65 Dec 08 '22 at 08:46
0

Try to check if the enableIndexedDbPersistence is running in a browser environment like this:

const app = initializeApp(firebaseConfig);
const db = getFirestore(app);


if (typeof window !== "undefined") {
  enableIndexedDbPersistence(db)
  .catch((err) => {
    if (err.code === "failed-precondition") {
      // Multiple tabs open, persistence can only be enabled
      // in one tab at a a time.
      // ...
    } else if (err.code === "unimplemented") {
      // The current browser does not support all of the
      // features required to enable persistence
      // ...
    }
  });
}
Soreng
  • 21
  • 3