Guillaume Prévost
Guillaume Prévost

Reputation: 413

"Firebase Error : Firestore has already been started and its settings can no longer be changed." connecting Firebase v9 with Firestore Emulator

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 :

What I've already read/tried :

Upvotes: 11

Views: 7468

Answers (4)

Soreng
Soreng

Reputation: 31

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

Upvotes: 1

nelson6e65
nelson6e65

Reputation: 1109

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.

Upvotes: 3

Millan Singh
Millan Singh

Reputation: 121

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

Upvotes: 11

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.

Upvotes: 0

Related Questions