From 601defa3ee12c1ec6d5384982d744d48b690e194 Mon Sep 17 00:00:00 2001 From: cogwheel <172976095+cogwheel0@users.noreply.github.com> Date: Thu, 5 Feb 2026 19:26:16 +0530 Subject: [PATCH] fix(auth): Improve network readiness handling on cold start Add robust network readiness gate for authentication to prevent race conditions with Cloudflare tunnels. Implement retry mechanism for API connectivity checks and silent login attempts when credentials are available. Enhance error handling and logging for network-related authentication challenges. --- lib/core/auth/auth_state_manager.dart | 122 ++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/lib/core/auth/auth_state_manager.dart b/lib/core/auth/auth_state_manager.dart index 0360008..d1f5d67 100644 --- a/lib/core/auth/auth_state_manager.dart +++ b/lib/core/auth/auth_state_manager.dart @@ -226,6 +226,73 @@ class AuthStateManager extends _$AuthStateManager { // Fast path: trust token format to avoid blocking startup on network final formatOk = _isValidTokenFormat(token); if (formatOk) { + // Network readiness gate: Wait for API to be reachable before + // transitioning to authenticated state. This prevents race conditions + // on cold starts with Cloudflare tunnels where the tunnel connection + // may not be established yet. + final apiReady = await _waitForApiReadiness(); + if (!apiReady) { + DebugLogger.auth( + 'API not reachable on cold start - keeping loading state', + ); + // Keep loading state and retry via silent login if we have creds + final hasCreds = await storage.hasCredentials(); + if (hasCreds) { + DebugLogger.auth( + 'Has credentials - attempting silent login after API ready', + ); + // Schedule a delayed retry that will wait for network. + // Allow time for network stack to stabilize after initial failure. + unawaited( + Future.delayed(const Duration(milliseconds: 500), () async { + if (!ref.mounted) return; + try { + final retryReady = await _waitForApiReadiness( + timeout: const Duration(seconds: 10), + ); + if (!ref.mounted) return; + if (retryReady) { + await _performSilentLogin(); + } else { + _update( + (current) => current.copyWith( + status: AuthStatus.error, + error: 'Unable to connect to server', + isLoading: false, + ), + ); + } + } catch (e, stack) { + if (!ref.mounted) return; + DebugLogger.error( + 'delayed-retry-failed', + scope: 'auth/state', + error: e, + stackTrace: stack, + ); + _update( + (current) => current.copyWith( + status: AuthStatus.error, + error: 'Connection retry failed', + isLoading: false, + ), + ); + } + }), + ); + return; + } + // No credentials - show error + _update( + (current) => current.copyWith( + status: AuthStatus.error, + error: 'Unable to connect to server', + isLoading: false, + ), + ); + return; + } + _update( (current) => current.copyWith( status: AuthStatus.authenticated, @@ -652,6 +719,61 @@ class AuthStateManager extends _$AuthStateManager { } } + /// Wait for the API to be reachable (network readiness gate). + /// + /// On cold starts with Cloudflare tunnels or other proxy setups, the network + /// connection may not be established immediately. This method performs a + /// health check with retries to ensure we don't show the wrong screen due to + /// a race condition between auth state initialization and network readiness. + /// + /// Returns true if the API is reachable within the timeout, false otherwise. + Future _waitForApiReadiness({ + Duration timeout = const Duration(seconds: 3), + Duration retryDelay = const Duration(milliseconds: 300), + }) async { + final stopwatch = Stopwatch()..start(); + + // First ensure the API service provider is available + await _ensureApiServiceAvailable(timeout: const Duration(seconds: 1)); + + while (stopwatch.elapsed < timeout) { + if (!ref.mounted) return false; + + final api = ref.read(apiServiceProvider); + if (api == null) { + await Future.delayed(retryDelay); + continue; + } + + try { + // Use checkHealth which hits the /health endpoint + final healthy = await api.checkHealth(); + if (healthy) { + DebugLogger.auth( + 'API readiness confirmed in ${stopwatch.elapsedMilliseconds}ms', + ); + return true; + } + } catch (e) { + DebugLogger.auth( + 'API readiness check failed (${stopwatch.elapsedMilliseconds}ms): $e', + ); + } + + // Wait before retrying + if (stopwatch.elapsed + retryDelay < timeout) { + await Future.delayed(retryDelay); + } else { + break; + } + } + + DebugLogger.auth( + 'API readiness timed out after ${stopwatch.elapsedMilliseconds}ms', + ); + return false; + } + /// Perform silent auto-login with saved credentials Future silentLogin() async { // Coalesce concurrent calls (e.g., UI + interceptor retry)