Merge pull request #368 from cogwheel0/improve-network-readiness-auth
fix(auth): Improve network readiness handling on cold start
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user