Skip to main content

Error Handling & Retry Logic

Antigravity Manager implements intelligent error handling and automatic retry mechanisms to provide a seamless experience even when upstream APIs encounter issues.

Error Classification

Location: src-tauri/src/error.rs and src-tauri/src/proxy/mappers/error_classifier.rs

Error Types

pub enum AppError {
    Database(rusqlite::Error),
    Network(String, Option<u16>),  // message, status code
    Io(std::io::Error),
    OAuth(String),
    Config(String),
    Account(String),
    Unknown(String),
}

HTTP Status Classification

Status CodeCategoryRetry StrategyExample
401Auth expiredAuto-refresh token → retryInvalid grant
403ForbiddenMark account → switchValidation blocked
404Not foundShort retry → rotateModel not available
429Rate limitExponential backoff → rotateToo many requests
500-503Server errorRetry with backoffOverloaded
4xxClient errorNo retryInvalid request

Error Classifier

pub fn classify_stream_error(e: &impl Display) -> (&'static str, String, &'static str) {
    let error_str = e.to_string();
    
    // Rate limit detection
    if error_str.contains("429") || error_str.contains("quota") {
        return (
            "rate_limit_error",
            "请求过于频繁,系统将自动切换账号重试".to_string(),
            "error.rate_limit"
        );
    }
    
    // Network issues
    if error_str.contains("timeout") || error_str.contains("connection") {
        return (
            "network_error",
            "网络连接不稳定,请检查您的网络或代理设置".to_string(),
            "error.network"
        );
    }
    
    // Auth errors
    if error_str.contains("401") || error_str.contains("unauthorized") {
        return (
            "authentication_error",
            "账号授权已过期,系统将自动刷新".to_string(),
            "error.auth"
        );
    }
    
    // Fallback
    ("api_error", error_str, "error.unknown")
}

Automatic Retry Logic

Location: Throughout proxy handlers

429 Rate Limit Handling

When a 429 Too Many Requests is received:
  1. Immediate account rotation:
    if status == 429 {
        tracing::warn!("[429] Rate limited on account {}, rotating...", account_id);
        
        // Get next available account
        let next_token = token_manager.get_next_available_token()?;
        
        // Retry request immediately with new account
        return retry_with_account(request, next_token).await;
    }
    
  2. Smart cooldown:
    // Soft lock the rate-limited account for 5 seconds
    token_manager.set_account_cooldown(account_id, Duration::from_secs(5));
    
  3. Quota refresh:
    // Trigger background quota refresh to update status
    tokio::spawn(async move {
        let _ = account::fetch_account_quota(account_id).await;
    });
    

401 Token Expiry

When a 401 Unauthorized is received:
  1. Silent token refresh:
    if status == 401 {
        tracing::info!("[401] Token expired for {}, refreshing...", account_id);
        
        // Attempt to refresh access token
        match oauth::refresh_access_token(account_id).await {
            Ok(new_token) => {
                // Update account with new token
                account::update_account_token(account_id, &new_token)?;
                
                // Retry original request with refreshed token
                return retry_request(request, account_id).await;
            }
            Err(e) => {
                // Mark account as disabled if refresh fails
                account::mark_account_disabled(
                    account_id,
                    &format!("Token refresh failed: {}", e)
                )?;
                
                // Rotate to next account
                let next_token = token_manager.get_next_available_token()?;
                return retry_with_account(request, next_token).await;
            }
        }
    }
    
  2. Automatic re-enablement: When token refresh succeeds, account is automatically re-enabled.

403 Validation Block

When a 403 Forbidden is received:
  1. Account marking:
    if status == 403 {
        let validation_url = extract_validation_url(&response_body);
        
        account::mark_account_forbidden(
            account_id,
            validation_url,
            Utc::now().timestamp()
        )?;
        
        tracing::warn!(
            "[403] Account {} blocked. Validation URL: {:?}",
            account_id,
            validation_url
        );
    }
    
  2. Immediate rotation:
    // Skip this account and try next
    let next_token = token_manager.get_next_available_token()?;
    return retry_with_account(request, next_token).await;
    
  3. UI notification: Account details page shows validation link for user action.

404 Model Not Found

Specific to Google Cloud Code API phased rollouts:
if status == 404 && request_path.contains("generateCode") {
    tracing::warn!(
        "[404] Model not available on account {}, trying next account...",
        account_id
    );
    
    // Short cooldown (5 seconds vs 8 for other errors)
    token_manager.set_account_cooldown(account_id, Duration::from_secs(5));
    
    // Quick retry with 300ms delay
    tokio::time::sleep(Duration::from_millis(300)).await;
    
    let next_token = token_manager.get_next_available_token()?;
    return retry_with_account(request, next_token).await;
}

500/503 Server Errors

Exponential backoff for temporary server issues:
if status >= 500 && status < 600 {
    let mut retry_count = 0;
    let max_retries = 3;
    
    while retry_count < max_retries {
        let delay = Duration::from_millis(100 * 2_u64.pow(retry_count));
        tokio::time::sleep(delay).await;
        
        match retry_request(request, account_id).await {
            Ok(response) => return Ok(response),
            Err(e) if retry_count < max_retries - 1 => {
                retry_count += 1;
                tracing::warn!("[5xx] Retry {} failed: {}", retry_count, e);
                continue;
            }
            Err(e) => return Err(e),
        }
    }
}
Backoff schedule:
  • Retry 1: 100ms
  • Retry 2: 200ms
  • Retry 3: 400ms

Account Rotation Strategy

Location: src-tauri/src/proxy/token_manager.rs

Smart Account Selection

pub fn get_next_available_token(&self) -> Result<ProxyToken, String> {
    let accounts = self.accounts.read().unwrap();
    
    // Filter available accounts
    let available: Vec<_> = accounts.iter()
        .filter(|acc| {
            !acc.disabled &&
            !acc.proxy_disabled &&
            !acc.validation_blocked &&
            !self.is_in_cooldown(&acc.id)
        })
        .collect();
    
    if available.is_empty() {
        return Err("No available accounts".to_string());
    }
    
    // Tiered routing: prioritize high-reset accounts
    let best = available.iter()
        .max_by_key(|acc| {
            let quota_score = calculate_quota_score(acc);
            let reset_score = calculate_reset_score(acc);
            quota_score * 100 + reset_score
        })
        .unwrap();
    
    Ok(best.to_proxy_token())
}

Quota Score Calculation

fn calculate_quota_score(account: &Account) -> u32 {
    let quota = match &account.quota {
        Some(q) => q,
        None => return 0,
    };
    
    // Average remaining percentage across all models
    let total: i32 = quota.models.iter()
        .map(|m| m.percentage)
        .sum();
    
    (total / quota.models.len() as i32) as u32
}

fn calculate_reset_score(account: &Account) -> u32 {
    // Prefer accounts that reset more frequently
    // Ultra: reset every hour (score: 100)
    // Pro: reset every 8 hours (score: 50)
    // Free: reset every 24 hours (score: 10)
    
    match account.subscription_tier.as_deref() {
        Some("ultra") => 100,
        Some("pro") => 50,
        _ => 10,
    }
}

Cooldown Management

struct AccountCooldown {
    account_id: String,
    until: Instant,
}

impl TokenManager {
    pub fn set_account_cooldown(&self, account_id: &str, duration: Duration) {
        let mut cooldowns = self.cooldowns.write().unwrap();
        cooldowns.insert(
            account_id.to_string(),
            Instant::now() + duration
        );
    }
    
    pub fn is_in_cooldown(&self, account_id: &str) -> bool {
        let cooldowns = self.cooldowns.read().unwrap();
        
        if let Some(until) = cooldowns.get(account_id) {
            Instant::now() < *until
        } else {
            false
        }
    }
}

Self-Healing Features

Location: Changelog references in README.md

Automatic Quota Refresh

Background task refreshes quota every N minutes:
pub async fn auto_refresh_quotas() {
    let mut interval = tokio::time::interval(Duration::from_secs(300)); // 5 min
    
    loop {
        interval.tick().await;
        
        let accounts = account::list_accounts().unwrap_or_default();
        for acc in accounts {
            if acc.disabled || acc.proxy_disabled {
                continue;  // Skip disabled accounts
            }
            
            match account::fetch_account_quota(&acc.id).await {
                Ok(quota) => {
                    // Update quota in database
                    let _ = account::update_account_quota(&acc.id, quota);
                }
                Err(e) if e.contains("403") => {
                    // Mark as forbidden if quota check fails
                    let _ = account::mark_account_forbidden(
                        &acc.id,
                        None,
                        Utc::now().timestamp()
                    );
                }
                Err(_) => {
                    // Ignore transient errors
                }
            }
        }
    }
}

Project ID Recovery

When project ID is missing or invalid:
if project_id.is_empty() || project_id == "projects/" {
    tracing::warn!("[Project] Invalid project ID detected, fetching...");
    
    match oauth::fetch_project_id(&account.refresh_token).await {
        Ok(pid) => {
            account::update_project_id(&account.id, &pid)?;
            project_id = pid;
        }
        Err(_) => {
            // Fallback to verified stable project
            project_id = "bamboo-precept-lgxtn".to_string();
            tracing::info!("[Project] Using fallback project ID");
        }
    }
}

Thinking Signature Recovery

When thinking blocks fail due to missing signatures:
pub fn strip_all_thinking_blocks(contents: Vec<Value>) -> Vec<Value> {
    contents.into_iter().map(|mut msg| {
        if let Some(parts) = msg["parts"].as_array_mut() {
            parts.retain(|part| {
                // Remove all parts with thought=true or thoughtSignature
                !part.get("thought").and_then(|t| t.as_bool()).unwrap_or(false) &&
                !part.get("thoughtSignature").is_some()
            });
        }
        msg
    }).collect()
}
This is automatically applied when:
  • Tool history exists (from previous turns)
  • Retry attempt is detected
  • Signature validation fails

Account Index Auto-Repair

If account index becomes corrupted:
pub fn rebuild_account_index() -> Result<(), String> {
    tracing::warn!("[Index] Rebuilding account index...");
    
    let data_dir = get_data_dir();
    let accounts_dir = data_dir.join("accounts");
    
    let mut index = Vec::new();
    
    for entry in fs::read_dir(&accounts_dir).map_err(|e| e.to_string())? {
        let entry = entry.map_err(|e| e.to_string())?;
        let path = entry.path();
        
        if path.extension().and_then(|s| s.to_str()) == Some("json") {
            if let Ok(account) = load_account_from_file(&path) {
                index.push(account.id);
            }
        }
    }
    
    save_account_index(&index)?;
    tracing::info!("[Index] Rebuilt with {} accounts", index.len());
    
    Ok(())
}
Triggered automatically when:
  • Index file is missing
  • Index contains invalid entries
  • Account count mismatch detected

Quota Protection

Prevents requests when quota is exhausted:
pub fn has_available_quota(account: &Account, model: &str) -> bool {
    let quota = match &account.quota {
        Some(q) => q,
        None => return true,  // Allow if unknown
    };
    
    // Normalize model name for comparison
    let normalized = normalize_model_name(model);
    
    // Find matching quota entry
    for model_quota in &quota.models {
        if model_quota.name == normalized {
            return model_quota.percentage > 0;
        }
    }
    
    // Allow if no specific quota found
    true
}
Integrated into account selection:
let available: Vec<_> = accounts.iter()
    .filter(|acc| has_available_quota(acc, &requested_model))
    .collect();

Error Recovery Modes

Permissive Mode

For first-time thinking requests (no history):
if !has_thinking_history && is_thinking_enabled {
    tracing::info!(
        "[Thinking-Mode] First thinking request detected. Using permissive mode."
    );
    // Allow upstream to validate, don't enforce signature checks
}

Strict Mode

For tool calls with thinking:
if needs_signature_check && !has_valid_signature() {
    tracing::warn!(
        "[Thinking-Mode] No valid signature for function calls. Disabling thinking."
    );
    is_thinking_enabled = false;
}

Adaptive Mode

Dynamically adjusts based on context:
if adaptive_thinking_enabled {
    let budget = match request_complexity {
        Complexity::Low => 4096,
        Complexity::Medium => 8192,
        Complexity::High => 24576,
    };
    
    gen_config["thinkingConfig"]["thinkingBudget"] = json!(budget);
}

Monitoring & Logging

All errors are logged with context:
tracing::error!(
    "[Error] Request failed: status={}, account={}, model={}, error={}",
    status,
    account_id,
    model,
    error_message
);
Accessible via:
  • UI logs page (/api/logs)
  • System logs (stored in data directory)
  • Debug console (if enabled)

Best Practices

  1. Add multiple accounts for seamless rotation
  2. Enable auto-refresh to keep quota status current
  3. Monitor logs for recurring errors
  4. Respond to 403s by following validation links
  5. Keep tokens fresh by using accounts regularly

See Also

Build docs developers (and LLMs) love