skip to Main Content

I’m trying to use the emulator only when it is available. connectAuthEmulator does not fail if the emulator is not available (i.e.: if I haven’t ran firebase emulators:start). It fails later when I try to make a request.

To do this, I’m fetching the emulator URL (http://localhost:9099) using fetch. If the request succeeds, then I call connectAuthEmulator. Otherwise, I do nothing (uses the configured cloud service).

I have a problem where the fetch works, but connectAuthEmulator throws an error: auth/emulator-config-failed. For some weird reason, this seem to happen only when I’m logged in and I make no requests for about 15 seconds. If I spam requests however, the error never occurs.

import { initializeApp } from "firebase/app";
import { getAuth, connectAuthEmulator } from "firebase/auth";

const app = initializeApp({ /** ... */ });
const auth = getAuth(app);

if (NODE_ENV === "development") {
    (async () => {
        try {
            const authEmulatorUrl = "http://localhost:9099";
            await fetch(authEmulatorUrl);
            connectAuthEmulator(auth, authEmulatorUrl, {
                disableWarnings: true,
            });
            console.info("🎮 Firebase Auth: emulated");
        } catch (e) {
            console.info("🔥 Firebase Auth: not emulated");
        }
    })()
}

export { auth };

Any idea why this happens and how to fix it?

3

Answers


  1. Chosen as BEST ANSWER

    Solution #1

    First, try http://127.0.0.1:9099 instead of http://localhost:9099 (don't forget to set this as your emulator host in firebase.json).

    Solution #2

    On top of using solution #1, try rendering your app after everything Firebase-related has init. You can do this by creating listeners on the status of your emulator connections.

    src/config/firebase.ts

    import { initializeApp } from "firebase/app";
    import { getAuth, connectAuthEmulator } from "firebase/auth";
    
    const app = initializeApp({ /** ... */ });
    const auth = getAuth(app);
    
    if (NODE_ENV === "development") {
        // Create listener logic
        window.emulatorsEvaluated = false;
        window.emulatorsEvaluatedListeners = [];
        window.onEmulatorsEvaluated = (listener: () => void) => {
            if (window.emulatorsEvaluated) {
                listener();
            } else {
                window.emulatorsEvaluatedListeners.push(listener);
            }
        };
    
        (async () => {
            try {
                // Use 127.0.0.1 instead of localhost
                const authEmulatorUrl = "http://127.0.0.1:9099";
                await fetch(authEmulatorUrl);
                connectAuthEmulator(auth, authEmulatorUrl, {
                    disableWarnings: true,
                });
                console.info("🎮 Firebase Auth: emulated");
            } catch (e) {
                console.info("🔥 Firebase Auth: not emulated");
            }
    
            // Indicate that the emulators have been evaluated
            window.emulatorsEvaluated = true;
            window.emulatorsEvaluatedListeners.forEach(
                (listener: () => void) => {
                    listener();
                }
            );
        })()
    }
    
    export { auth };
    

    src/index.tsx

    import React from "react";
    import { createRoot } from "react-dom/client";
    import "./config/firebase";
    
    const root = createRoot(document.getElementById("root")!);
    
    const Root = () => ({
      /** Some JSX */
    });
    
    if (NODE_ENV === "development") {
      window.onEmulatorsEvaluated(() => {
        root.render(<Root />);
      });
    } else {
      root.render(<Root />);
    }
    
    

    Solution #3

    Forcibly change the value of auth._canInitEmulator ((auth as unknown as any)._canInitEmulator for TS) to true. This can have some unexpected side-effects (see answer), because some requests might go to your cloud before the emulator kicks in. This can be mitigated with solution #2 (which should prevent requests from firing)

    (auth as unknown as any)._canInitEmulator = true;
    connectAuthEmulator(auth, authEmulatorUrl, {
      disableWarnings: true,
    });
    

    Full Answer

    Part of the issue here is that I was performing connectAuthEmulator in an async function, when the documentation clearly states it should be called immediately (and synchronously) after getAuth. I was aware of this, but I did not see any other alternative to the issue of connecting to the emulator only when it is available.

    I dug into the source code for connectAuthEmulator (permalink) and found out that the error is thrown here:

      _assert(
        authInternal._canInitEmulator,
        authInternal,
        AuthErrorCode.EMULATOR_CONFIG_FAILED
      );
    

    authInternal._canInitEmulator was false when the error occurred. There is only one place where this property is set to false, in _performFetchWithErrorHandling, here (permalink). This is because it's the first time it performs an API request for the Auth service. So I concluded that Firebase disallows the use of emulators after the first request using auth is made. This is probably to prevent making some requests on your cloud, then switching over to an emulator. So my call to connectAuthEmulator was probably done after a request was already made. I couldn't figure out where though. Even with my app not being loaded (solution #2), the error would still occur.

    My conclusion was right, as I later found this error.ts file which pretty much says this:

        [AuthErrorCode.EMULATOR_CONFIG_FAILED]:
          'Auth instance has already been used to make a network call. Auth can ' +
          'no longer be configured to use the emulator. Try calling ' +
          '"connectAuthEmulator()" sooner.',
    

    This whole investigation could've been done earlier if FirebaseError had shown this message instead of just auth/emulator-config-failed. It is visible if you console.log(error.message), but I was not aware of that at the time.

    For some reason, using the localhost IP instead fixed it instantly and I never had the error again. I added the solution #2 and #3 as another measure after.


  2. try http://127.0.0.1:9099 instead of http://localhost:9099 (don’t forget to set this as your emulator host in firebase.json).

    Login or Signup to reply.
  3. I think the root cause of this problem comes from React.StrictMode. This mode will re-render twice time. It might have been initiated twice time too. I am pretty sure because when I disabled the React.StrictMode and refreshed the browser many times. The error does not show up anymore.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search