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.
This commit is contained in:
cogwheel
2026-02-05 19:26:16 +05:30
parent c75898546a
commit 601defa3ee

View File

@@ -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<bool> _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<bool> silentLogin() async {
// Coalesce concurrent calls (e.g., UI + interceptor retry)