Skip to content

[Bug]: Firebase Auth makes network requests when offline without evicting token #4468

@christian-byrne

Description

@christian-byrne

Frontend Version

1.24.1

Expected Behavior

ComfyUI should function offline without Firebase network requests when not using Firebase features.

Actual Behavior

Firebase Auth makes network requests that fail when offline or when Firebase is blocked (if there is a token stored from a previous or expired session):

  1. On app initialization: onAuthStateChanged (firebaseAuthStore.ts:82) attempts to validate stored tokens from firebase:authUser:[PROJECT_ID]:[DEFAULT]

  2. When queuing prompts: getIdToken() is called (app.ts:1242-1243) which attempts to refresh tokens older than 1 hour

Results in toast error messages and degraded offline experience.

Steps to Reproduce

  1. Sign in to ComfyUI with Firebase Auth
  2. Close browser
  3. Disconnect from internet (or be behind firewall that blocks Firebase)
  4. Wait 1 hour so access token expires (or change timestamp in localstorage)
  5. Open ComfyUI
  6. Queue a graph
  7. Observe network errors in console

Debug Logs

auth/network-request-failed: A network error has occurred.
Failed to refresh token: FirebaseError: Firebase: Error (auth/network-request-failed)

Browser Logs

FirebaseError: Firebase: Error (auth/network-request-failed).
    at createErrorInternal (index-6bd8d405.js:474:41)
    at _fail (index-6bd8d405.js:445:11)
    at _performFetchWithErrorHandling (index-6bd8d405.js:998:11)

Setting JSON

Standard settings with Firebase Auth enabled

What browsers do you use to access the UI?

  • Google Chrome
  • Mozilla Firefox
  • Brave

Other Information

Firebase Auth behavior:

  • ID tokens expire after 1 hour
  • browserLocalPersistence stores tokens in localStorage/IndexedDB
  • Automatic token refresh attempts on expiry
  • Exponential backoff for failed requests
  • localStorage not cleared on network failures

Proposed Solutions

1. Catch Network Errors Gracefully

const getIdToken = async (): Promise<string | null> => {
  if (currentUser.value) {
    try {
      return await currentUser.value.getIdToken()
    } catch (error) {
      if (error.code === 'auth/network-request-failed') {
        return null; // Works offline or behind firewalls
      }
      throw error
    }
  }
  return null
}

2. Lazy Token Validation

onAuthStateChanged(auth, (user) => {
  currentUser.value = user
  isInitialized.value = true
  // Skip immediate token validation
})

// app.ts - only try token if needed
const comfyOrgAuthToken = await useFirebaseAuthStore().getIdToken()
  .catch(() => undefined); // Fail silently

3. Circuit Breaker Pattern
After multiple failures, stop attempting requests for a cooldown period:

class AuthCircuitBreaker {
  private failures = 0;
  private lastFailure = 0;
  private readonly threshold = 3;
  private readonly timeout = 60000; // 1 minute

  async getToken(): Promise<string | null> {
    // If circuit is "open" (too many recent failures), return null immediately
    if (this.failures >= this.threshold && 
        Date.now() - this.lastFailure < this.timeout) {
      return null;
    }
    
    try {
      const token = await getIdToken();
      this.failures = 0; // Reset on success
      return token;
    } catch (error) {
      this.failures++;
      this.lastFailure = Date.now();
      return null;
    }
  }
}

4. Service Worker Intercept
Intercept and immediately fail Firebase requests to prevent hanging:

// In service worker
self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url);
  
  // Firebase domains that might be blocked
  const firebaseDomains = [
    'googleapis.com',
    'firebaseapp.com', 
    'firebaseio.com'
  ];
  
  if (firebaseDomains.some(domain => url.hostname.includes(domain))) {
    // Try fetch with short timeout
    event.respondWith(
      Promise.race([
        fetch(event.request),
        new Promise((_, reject) => 
          setTimeout(() => reject(new Error('Timeout')), 3000)
        )
      ]).catch(() => 
        new Response(null, { status: 503, statusText: 'Service Unavailable' })
      )
    );
  }
});

5. Offline-First Auth Wrapper
Wrapper store that tracks Firebase availability and caches state:

export const useOfflineAuthStore = () => {
  const authStore = useFirebaseAuthStore();
  const firebaseAvailable = ref(true);
  const lastValidToken = ref<string | null>(null);
  
  const checkFirebaseAvailability = async () => {
    try {
      // Quick health check to Firebase
      await fetch('https://www.googleapis.com/identitytoolkit/v3/relyingparty', {
        method: 'HEAD',
        mode: 'no-cors',
        signal: AbortSignal.timeout(2000)
      });
      firebaseAvailable.value = true;
    } catch {
      firebaseAvailable.value = false;
    }
  };
  
  return {
    ...authStore,
    getIdToken: async () => {
      if (!firebaseAvailable.value) {
        return lastValidToken.value; // Use cached token
      }
      
      try {
        const token = await authStore.getIdToken();
        lastValidToken.value = token; // Cache for offline use
        return token;
      } catch (error) {
        firebaseAvailable.value = false;
        return lastValidToken.value;
      }
    }
  };
}

┆Issue is synchronized with this Notion page by Unito

Metadata

Metadata

Labels

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions