Skip to content

Commit 158074a

Browse files
committed
Implemented better API handling
1 parent d5422b2 commit 158074a

File tree

3 files changed

+86
-24
lines changed

3 files changed

+86
-24
lines changed

lib/providers/app_state_provider.dart

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,17 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
830830

831831
debugLog('[APP] Stage 1 failed: ${result['message'] ?? 'Unknown error'}');
832832

833+
// If Stage 1 failed due to GPS issues, Stage 2 will also fail with same bad data
834+
final stage1Reason = result['reason'] as String?;
835+
if (stage1Reason == 'gps_inaccurate' || stage1Reason == 'gps_stale') {
836+
debugError('[APP] Stage 1 failed for GPS reason ($stage1Reason), skipping Stage 2');
837+
return {
838+
'success': false,
839+
'reason': stage1Reason,
840+
'message': result['message'] as String?,
841+
};
842+
}
843+
833844
// ============================================================
834845
// STAGE 2: Auth failed, attempt registration via signed contact_uri
835846
// ============================================================
@@ -873,11 +884,13 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
873884
}
874885

875886
if (registerResult['success'] != true) {
876-
debugError('[APP] Stage 2 failed: registration rejected by server');
887+
final serverReason = registerResult['reason'] as String? ?? 'registration_failed';
888+
final serverMessage = registerResult['message'] as String?;
889+
debugError('[APP] Stage 2 failed: $serverReason - ${serverMessage ?? 'no message'}');
877890
return {
878891
'success': false,
879-
'reason': 'registration_failed',
880-
'message': 'Companion not found in backend and failed to register via API'
892+
'reason': serverReason,
893+
'message': serverMessage ?? 'Registration rejected by server',
881894
};
882895
}
883896

@@ -1318,7 +1331,8 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
13181331
if (parts.length > 1) {
13191332
final errorParts = parts[1].split(':');
13201333
final reason = errorParts.isNotEmpty ? errorParts[0] : 'unknown';
1321-
_connectionError = _getErrorMessage(reason, null);
1334+
final serverMessage = errorParts.length > 1 ? errorParts.sublist(1).join(':') : null;
1335+
_connectionError = _getErrorMessage(reason, serverMessage);
13221336
} else {
13231337
_connectionError = 'Authentication failed';
13241338
}
@@ -2493,7 +2507,8 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
24932507
}
24942508

24952509
// Upload the batch
2496-
final success = await _apiService.uploadBatch(pings);
2510+
final result = await _apiService.uploadBatch(pings);
2511+
final success = result == UploadResult.success;
24972512
if (success) {
24982513
// Delete the session file on successful upload
24992514
await _offlineSessionService.deleteSession(filename);
@@ -2579,8 +2594,8 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
25792594

25802595
for (var i = 0; i < pings.length; i += batchSize) {
25812596
final batch = pings.skip(i).take(batchSize).toList();
2582-
final success = await _apiService.uploadBatch(batch);
2583-
if (success) {
2597+
final result = await _apiService.uploadBatch(batch);
2598+
if (result == UploadResult.success) {
25842599
uploadedCount += batch.length;
25852600
debugLog('[APP] Uploaded batch ${(i ~/ batchSize) + 1}: ${batch.length} pings');
25862601
} else {
@@ -2814,6 +2829,7 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
28142829
const zoneErrors = {
28152830
'outside_zone',
28162831
'zone_full',
2832+
'zone_disabled',
28172833
};
28182834

28192835
// Handle errors that require disconnect
@@ -3064,9 +3080,17 @@ class AppStateProvider extends ChangeNotifier with WidgetsBindingObserver {
30643080
} else if (reason == 'gps_stale') {
30653081
logError('GPS Stale Error\n$message');
30663082
_scheduleZoneCheckRetry(seconds: 5, error: message, reason: 'gps_stale');
3083+
} else if (reason == 'zone_disabled') {
3084+
final errorMsg = _getErrorMessage(reason, message);
3085+
logError(errorMsg);
3086+
_scheduleZoneCheckRetry(seconds: 30, error: errorMsg, reason: reason!);
3087+
} else if (reason == 'bad_key' || reason == 'invalid_request') {
3088+
final errorMsg = _getErrorMessage(reason, message);
3089+
logError(errorMsg);
3090+
_scheduleZoneCheckRetry(seconds: 60, error: errorMsg, reason: reason!);
30673091
} else {
3068-
// 500 server errors, database errors, or unknown failures
3069-
_scheduleZoneCheckRetry(seconds: 15, error: 'MeshMapper server error', reason: 'server_error');
3092+
// Unknown server errors — use server message
3093+
_scheduleZoneCheckRetry(seconds: 15, error: message, reason: 'server_error');
30703094
}
30713095

30723096
return;

lib/services/api_queue_service.dart

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -425,16 +425,22 @@ class ApiQueueService {
425425
debugLog('[API QUEUE] Uploading ${items.length} items...');
426426

427427
// Attempt upload
428-
final success = await _apiService.uploadBatch(pings);
428+
final result = await _apiService.uploadBatch(pings);
429429

430-
if (success) {
430+
if (result == UploadResult.success) {
431431
final uploadedCount = items.length;
432432
// Remove successful items
433433
for (final item in items) {
434434
await item.delete();
435435
}
436436
debugLog('[API QUEUE] Upload SUCCESS: deleted $uploadedCount items');
437437
onUploadSuccess?.call(uploadedCount);
438+
} else if (result == UploadResult.nonRetryable) {
439+
// Data is permanently invalid (bad GPS, invalid request, etc.) — discard
440+
for (final item in items) {
441+
await item.delete();
442+
}
443+
debugWarn('[API QUEUE] Discarded ${items.length} items (non-retryable error)');
438444
} else {
439445
// Mark items as retried
440446
for (final item in items) {

lib/services/api_service.dart

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import 'package:http/http.dart' as http;
66
import '../models/repeater.dart';
77
import '../utils/debug_logger_io.dart';
88

9+
/// Result of a batch upload attempt
10+
enum UploadResult { success, retryable, nonRetryable }
11+
912
/// MeshMapper API service
1013
/// Handles communication with the MeshMapper backend
1114
///
@@ -473,7 +476,7 @@ class ApiService {
473476
const criticalErrors = {
474477
'session_expired', 'session_invalid', 'session_revoked', 'bad_session',
475478
'invalid_key', 'unauthorized', 'bad_key',
476-
'outside_zone', 'zone_full',
479+
'outside_zone', 'zone_full', 'zone_disabled',
477480
};
478481
if (criticalErrors.contains(reason)) {
479482
_clearSession();
@@ -544,9 +547,28 @@ class ApiService {
544547
if (result?['success'] == true) {
545548
debugLog('[HEARTBEAT] Heartbeat successful');
546549
// Next heartbeat will be scheduled when we get new expires_at
547-
} else {
548-
debugWarn('[HEARTBEAT] Heartbeat failed: ${result?['message']}');
550+
} else if (result == null) {
551+
// Network error — transient, trigger session expiring
552+
debugWarn('[HEARTBEAT] Heartbeat failed: network error');
549553
_onSessionExpiring?.call();
554+
} else {
555+
// Server returned an error — check if critical
556+
final reason = result['reason'] as String?;
557+
final message = result['message'] as String?;
558+
debugWarn('[HEARTBEAT] Heartbeat failed: $reason - $message');
559+
560+
const criticalErrors = {
561+
'session_expired', 'session_invalid', 'session_revoked', 'bad_session',
562+
'invalid_key', 'unauthorized', 'bad_key',
563+
'outside_zone', 'zone_full', 'zone_disabled',
564+
};
565+
566+
if (criticalErrors.contains(reason)) {
567+
_clearSession();
568+
onSessionError?.call(reason, message);
569+
} else {
570+
_onSessionExpiring?.call();
571+
}
550572
}
551573
}
552574

@@ -572,28 +594,28 @@ class ApiService {
572594
/// Callback for maintenance mode detection (while connected)
573595
void Function(String message, String? url)? onMaintenanceMode;
574596

575-
/// Legacy: Upload batch (wrapper for submitWardriveData)
576-
/// Returns true on success, false on failure
597+
/// Upload batch of wardrive data
598+
/// Returns UploadResult indicating success, retryable failure, or non-retryable failure
577599
/// Triggers onSessionError callback for session-related errors
578-
Future<bool> uploadBatch(List<Map<String, dynamic>> pings) async {
579-
if (pings.isEmpty) return true;
600+
Future<UploadResult> uploadBatch(List<Map<String, dynamic>> pings) async {
601+
if (pings.isEmpty) return UploadResult.success;
580602

581603
try {
582604
final result = await submitWardriveData(pings);
583605

584606
if (result == null) {
585607
debugError('[API] Upload batch failed: no response');
586-
return false;
608+
return UploadResult.retryable;
587609
}
588610

589611
// Check for maintenance mode first
590612
if (_checkMaintenanceMode(result)) {
591-
return false;
613+
return UploadResult.retryable;
592614
}
593615

594616
if (result['success'] == true) {
595617
debugLog('[API] Upload batch SUCCESS: ${pings.length} items');
596-
return true;
618+
return UploadResult.success;
597619
}
598620

599621
// Check for session errors that require disconnect
@@ -606,7 +628,7 @@ class ApiService {
606628
// Auth errors
607629
'invalid_key', 'unauthorized', 'bad_key',
608630
// Zone errors
609-
'outside_zone', 'zone_full',
631+
'outside_zone', 'zone_full', 'zone_disabled',
610632
};
611633

612634
if (criticalErrors.contains(reason)) {
@@ -616,12 +638,22 @@ class ApiService {
616638
_clearSession();
617639
// Notify listener for auto-disconnect
618640
onSessionError?.call(reason, message);
641+
return UploadResult.nonRetryable;
642+
}
643+
644+
// Errors where the batch data itself is invalid — retrying won't help
645+
const nonRetryableErrors = {
646+
'gps_inaccurate', 'gps_stale', 'invalid_request', 'zone_disabled', 'outofdate',
647+
};
648+
if (nonRetryableErrors.contains(reason)) {
649+
debugWarn('[API] Upload batch non-retryable error: $reason - discarding batch');
650+
return UploadResult.nonRetryable;
619651
}
620652

621-
return false;
653+
return UploadResult.retryable;
622654
} catch (e) {
623655
debugError('[API] Upload batch exception: $e');
624-
return false;
656+
return UploadResult.retryable;
625657
}
626658
}
627659

0 commit comments

Comments
 (0)