Skip to content

Error Handling

Comprehensive error handling and recovery strategies for robust device-side API implementation.

Standard HTTP status codes and recommended device actions:

CodeStatusMeaningDevice Action
200OKRequest processed successfullyContinue normal operation
202AcceptedRequest queued for processingMonitor for completion
400Bad RequestInvalid request formatLog error, don’t retry
401UnauthorizedAuthentication failedRefresh token, retry once
403ForbiddenAccess deniedCheck permissions, log error
404Not FoundEndpoint not foundCheck API version, don’t retry
409ConflictResource conflictResolve conflict, retry
413Payload Too LargeRequest too largeSplit request, retry
429Rate LimitedToo many requestsBackoff and retry
500Server ErrorTemporary server issueExponential backoff retry
502Bad GatewayGateway errorExponential backoff retry
503Service UnavailableServer maintenanceQueue locally, retry later
504Gateway TimeoutRequest timeoutRetry with backoff
struct RetryPolicy {
int max_attempts = 3;
unsigned long initial_delay = 1000; // 1 second
float backoff_multiplier = 2.0;
unsigned long max_delay = 60000; // 60 seconds
std::vector<int> retry_codes = {429, 500, 502, 503, 504};
};
class RetryManager {
private:
RetryPolicy policy;
public:
bool shouldRetry(int httpCode, int attempt) {
if (attempt >= policy.max_attempts) {
return false;
}
return std::find(policy.retry_codes.begin(),
policy.retry_codes.end(),
httpCode) != policy.retry_codes.end();
}
unsigned long getDelayMs(int attempt) {
unsigned long delay = policy.initial_delay *
pow(policy.backoff_multiplier, attempt);
return min(delay, policy.max_delay);
}
};
class ExponentialBackoff {
private:
unsigned long lastAttempt = 0;
int currentAttempt = 0;
RetryManager retryManager;
public:
bool canRetry(int httpCode) {
if (!retryManager.shouldRetry(httpCode, currentAttempt)) {
reset();
return false;
}
unsigned long delay = retryManager.getDelayMs(currentAttempt);
if (millis() - lastAttempt < delay) {
return false; // Still in backoff period
}
currentAttempt++;
lastAttempt = millis();
return true;
}
void reset() {
currentAttempt = 0;
lastAttempt = 0;
}
void recordSuccess() {
reset();
}
};
class ConnectionManager {
private:
bool networkAvailable = false;
unsigned long lastConnectivityCheck = 0;
int consecutiveFailures = 0;
public:
bool isNetworkAvailable() {
// Check connectivity every 30 seconds
if (millis() - lastConnectivityCheck > 30000) {
checkConnectivity();
lastConnectivityCheck = millis();
}
return networkAvailable;
}
void recordConnectionFailure() {
consecutiveFailures++;
// If too many failures, mark network as unavailable
if (consecutiveFailures >= 5) {
networkAvailable = false;
triggerConnectivityCheck();
}
}
void recordConnectionSuccess() {
consecutiveFailures = 0;
networkAvailable = true;
}
private:
void checkConnectivity() {
// Test with multiple approaches
bool wifiConnected = (WiFi.status() == WL_CONNECTED);
bool dnsWorks = testDNSResolution();
bool internetAccess = testInternetConnectivity();
networkAvailable = wifiConnected && dnsWorks && internetAccess;
if (!networkAvailable) {
attemptNetworkRecovery();
}
}
bool testDNSResolution() {
IPAddress ip;
return WiFi.hostByName("google.com", ip);
}
bool testInternetConnectivity() {
HTTPClient http;
http.begin("http://httpbin.org/status/200");
http.setTimeout(5000);
int httpCode = http.GET();
http.end();
return (httpCode == 200);
}
void attemptNetworkRecovery() {
Serial.println("Attempting network recovery...");
// Try WiFi reconnection
if (WiFi.status() != WL_CONNECTED) {
WiFi.disconnect();
delay(1000);
WiFi.reconnect();
// Wait up to 10 seconds for connection
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
attempts++;
}
}
}
};
class OfflineQueue {
private:
struct QueuedRequest {
String endpoint;
String method;
String payload;
unsigned long timestamp;
int priority; // 1=high, 2=medium, 3=low
int attempts;
};
std::vector<QueuedRequest> queue;
const size_t MAX_QUEUE_SIZE = 1000;
const size_t MAX_MEMORY_MB = 10;
public:
void queueRequest(String endpoint, String method, String payload, int priority = 2) {
// Check memory usage
if (getQueueMemoryUsage() > MAX_MEMORY_MB * 1024 * 1024) {
purgeOldestLowPriority();
}
// Check queue size
if (queue.size() >= MAX_QUEUE_SIZE) {
purgeOldestLowPriority();
}
QueuedRequest request;
request.endpoint = endpoint;
request.method = method;
request.payload = payload;
request.timestamp = millis();
request.priority = priority;
request.attempts = 0;
// Insert based on priority
insertByPriority(request);
Serial.printf("Queued request: %s %s (priority %d)\n",
method.c_str(), endpoint.c_str(), priority);
}
void processQueue(ConnectionManager& connMgr) {
if (!connMgr.isNetworkAvailable()) {
return;
}
// Process up to 10 requests per call to avoid blocking
int processed = 0;
for (auto it = queue.begin(); it != queue.end() && processed < 10;) {
QueuedRequest& request = *it;
if (sendQueuedRequest(request)) {
// Success - remove from queue
Serial.printf("Successfully sent queued request: %s %s\n",
request.method.c_str(), request.endpoint.c_str());
it = queue.erase(it);
processed++;
} else {
// Failed - increment attempts
request.attempts++;
// Remove if too many attempts
if (request.attempts >= 5) {
Serial.printf("Dropping request after 5 attempts: %s %s\n",
request.method.c_str(), request.endpoint.c_str());
it = queue.erase(it);
} else {
++it;
}
}
}
if (processed > 0) {
Serial.printf("Processed %d queued requests\n", processed);
}
}
size_t getQueueSize() {
return queue.size();
}
float getQueueMemoryUsage() {
size_t totalSize = 0;
for (const auto& request : queue) {
totalSize += request.payload.length();
}
return totalSize;
}
private:
void insertByPriority(const QueuedRequest& request) {
auto pos = std::upper_bound(queue.begin(), queue.end(), request,
[](const QueuedRequest& a, const QueuedRequest& b) {
return a.priority < b.priority;
});
queue.insert(pos, request);
}
void purgeOldestLowPriority() {
// Remove oldest low priority items
for (auto it = queue.begin(); it != queue.end();) {
if (it->priority == 3) { // Low priority
it = queue.erase(it);
} else {
++it;
}
}
// If still full, remove oldest medium priority
if (queue.size() >= MAX_QUEUE_SIZE) {
for (auto it = queue.begin(); it != queue.end();) {
if (it->priority == 2) { // Medium priority
it = queue.erase(it);
break; // Only remove one at a time
} else {
++it;
}
}
}
}
bool sendQueuedRequest(const QueuedRequest& request) {
HTTPClient https;
https.begin(API_BASE_URL + request.endpoint);
https.addHeader("Content-Type", "application/json");
https.addHeader("Authorization", "Bearer " + deviceToken);
https.setTimeout(10000); // Longer timeout for queued requests
int httpCode;
if (request.method == "POST") {
httpCode = https.POST(request.payload);
} else if (request.method == "PUT") {
httpCode = https.PUT(request.payload);
} else if (request.method == "GET") {
httpCode = https.GET();
} else {
return false; // Unsupported method
}
https.end();
return (httpCode >= 200 && httpCode < 300);
}
};
class TokenManager {
private:
String currentToken;
unsigned long tokenExpiry = 0;
bool refreshInProgress = false;
int refreshAttempts = 0;
public:
bool isTokenValid() {
// Check if token exists and hasn't expired
return !currentToken.isEmpty() &&
millis() < tokenExpiry - 300000; // 5 min buffer
}
bool handleAuthError(int httpCode) {
if (httpCode == 401 && !refreshInProgress) {
return refreshToken();
}
return false;
}
String getToken() {
if (!isTokenValid()) {
refreshToken();
}
return currentToken;
}
private:
bool refreshToken() {
if (refreshInProgress) {
return false;
}
refreshInProgress = true;
refreshAttempts++;
// Too many refresh attempts - need re-registration
if (refreshAttempts > 5) {
Serial.println("Too many token refresh attempts - re-registration required");
triggerReRegistration();
refreshInProgress = false;
return false;
}
// Attempt token refresh
HTTPClient https;
https.begin(API_BASE_URL + "/auth/refresh");
https.addHeader("Content-Type", "application/json");
https.addHeader("Authorization", "Bearer " + currentToken);
StaticJsonDocument<128> doc;
doc["device_id"] = getDeviceId();
String payload;
serializeJson(doc, payload);
int httpCode = https.POST(payload);
if (httpCode == 200) {
String response = https.getString();
StaticJsonDocument<256> responseDoc;
deserializeJson(responseDoc, response);
currentToken = responseDoc["device_token"].as<String>();
tokenExpiry = millis() + (24 * 60 * 60 * 1000); // 24 hours
// Save token to secure storage
saveTokenToStorage();
refreshAttempts = 0;
refreshInProgress = false;
Serial.println("Token refreshed successfully");
return true;
} else {
Serial.printf("Token refresh failed: HTTP %d\n", httpCode);
refreshInProgress = false;
return false;
}
https.end();
}
void triggerReRegistration() {
// Clear stored credentials
clearStoredCredentials();
// Set flag for re-registration
setReRegistrationRequired(true);
// Could also queue a re-registration attempt
}
};
class DataIntegrityManager {
public:
struct ValidationResult {
bool valid;
String error;
String correctedData;
};
ValidationResult validatePayload(const String& payload) {
ValidationResult result;
result.valid = true;
// Parse JSON to check structure
StaticJsonDocument<1024> doc;
DeserializationError error = deserializeJson(doc, payload);
if (error) {
result.valid = false;
result.error = "Invalid JSON: " + String(error.c_str());
return result;
}
// Validate required fields
if (!doc.containsKey("device_id")) {
result.valid = false;
result.error = "Missing device_id field";
return result;
}
if (!doc.containsKey("timestamp")) {
result.valid = false;
result.error = "Missing timestamp field";
return result;
}
// Validate timestamp format
String timestamp = doc["timestamp"];
if (!isValidISO8601(timestamp)) {
result.valid = false;
result.error = "Invalid timestamp format";
// Try to correct timestamp
String correctedTimestamp = getCurrentISO8601();
doc["timestamp"] = correctedTimestamp;
serializeJson(doc, result.correctedData);
}
// Validate data ranges for BCG data
if (doc.containsKey("bcg_data")) {
JsonObject bcg = doc["bcg_data"];
if (bcg.containsKey("pulse_rate")) {
int pulse = bcg["pulse_rate"];
if (pulse < 30 || pulse > 200) {
result.valid = false;
result.error = "Pulse rate out of range: " + String(pulse);
}
}
if (bcg.containsKey("respiratory_rate")) {
int breathing = bcg["respiratory_rate"];
if (breathing < 5 || breathing > 40) {
result.valid = false;
result.error = "Respiratory rate out of range: " + String(breathing);
}
}
}
return result;
}
String calculatePayloadChecksum(const String& payload) {
// Calculate SHA-256 checksum
mbedtls_sha256_context ctx;
mbedtls_sha256_init(&ctx);
mbedtls_sha256_starts(&ctx, 0);
mbedtls_sha256_update(&ctx, (const unsigned char*)payload.c_str(), payload.length());
unsigned char hash[32];
mbedtls_sha256_finish(&ctx, hash);
mbedtls_sha256_free(&ctx);
// Convert to hex string
String checksum;
for (int i = 0; i < 32; i++) {
if (hash[i] < 16) checksum += "0";
checksum += String(hash[i], HEX);
}
return checksum;
}
};
class APIErrorHandler {
private:
RetryManager retryManager;
ConnectionManager connectionManager;
TokenManager tokenManager;
OfflineQueue offlineQueue;
DataIntegrityManager integrityManager;
public:
struct APIResponse {
bool success;
int httpCode;
String response;
String error;
bool shouldRetry;
};
APIResponse sendRequest(String endpoint, String method, String payload, int priority = 2) {
APIResponse result;
// Validate payload integrity
auto validation = integrityManager.validatePayload(payload);
if (!validation.valid) {
result.success = false;
result.error = "Payload validation failed: " + validation.error;
// Use corrected data if available
if (validation.correctedData.length() > 0) {
payload = validation.correctedData;
Serial.println("Using corrected payload");
} else {
result.shouldRetry = false;
return result;
}
}
// Check network availability
if (!connectionManager.isNetworkAvailable()) {
Serial.println("Network unavailable - queuing request");
offlineQueue.queueRequest(endpoint, method, payload, priority);
result.success = false;
result.error = "Network unavailable - queued for later";
result.shouldRetry = false;
return result;
}
// Check token validity
if (!tokenManager.isTokenValid()) {
Serial.println("Token invalid - attempting refresh");
if (!tokenManager.refreshToken()) {
result.success = false;
result.error = "Authentication failed";
result.shouldRetry = false;
return result;
}
}
// Attempt to send request
ExponentialBackoff backoff;
do {
result = attemptRequest(endpoint, method, payload);
if (result.success) {
backoff.recordSuccess();
connectionManager.recordConnectionSuccess();
return result;
}
// Handle specific error types
if (result.httpCode == 401) {
if (tokenManager.handleAuthError(result.httpCode)) {
continue; // Retry with new token
}
}
if (result.httpCode == 413) {
// Payload too large - try to split
if (trySplitPayload(endpoint, method, payload, priority)) {
result.success = true;
return result;
}
}
// Record connection issues
if (result.httpCode == 0 || result.httpCode >= 500) {
connectionManager.recordConnectionFailure();
}
// Check if we should retry
if (!backoff.canRetry(result.httpCode)) {
break;
}
} while (true);
// All retries failed - queue for offline processing if appropriate
if (shouldQueueOffline(result.httpCode)) {
offlineQueue.queueRequest(endpoint, method, payload, priority);
result.error += " - queued for retry";
}
return result;
}
void processOfflineQueue() {
offlineQueue.processQueue(connectionManager);
}
private:
APIResponse attemptRequest(String endpoint, String method, String payload) {
APIResponse result;
HTTPClient https;
https.begin(API_BASE_URL + endpoint);
https.addHeader("Content-Type", "application/json");
https.addHeader("Authorization", "Bearer " + tokenManager.getToken());
https.setTimeout(15000);
if (method == "POST") {
result.httpCode = https.POST(payload);
} else if (method == "PUT") {
result.httpCode = https.PUT(payload);
} else if (method == "GET") {
result.httpCode = https.GET();
} else {
result.success = false;
result.error = "Unsupported method: " + method;
return result;
}
result.response = https.getString();
result.success = (result.httpCode >= 200 && result.httpCode < 300);
if (!result.success) {
result.error = "HTTP " + String(result.httpCode) + ": " + result.response;
}
https.end();
return result;
}
bool shouldQueueOffline(int httpCode) {
// Queue for temporary server errors, rate limiting, and network issues
return (httpCode == 0 || httpCode == 429 || httpCode >= 500);
}
bool trySplitPayload(String endpoint, String method, String payload, int priority) {
// Implementation for splitting large payloads
// This would be specific to the payload type
return false; // Simplified for example
}
};
  1. Always Validate: Check data integrity before sending
  2. Implement Backoff: Use exponential backoff for retries
  3. Queue Offline: Store failed requests for later transmission
  4. Monitor Connectivity: Continuously check network status
  5. Handle Authentication: Automatically refresh expired tokens
  6. Log Everything: Comprehensive logging for debugging
  7. Graceful Degradation: Continue operation when possible
  8. Resource Management: Limit queue size and memory usage
  9. Priority Handling: Process critical data first
  10. Recovery Mechanisms: Implement automatic recovery procedures