Kuberwastaken/claurst · warning

OAuth callback timeout (5 minutes)

Error message

OAuth callback timeout (5 minutes)

What it means

After binding the callback server, the OAuth flow waits up to 5 minutes (300s) for the browser to complete authorization and hit the local callback URL. `tokio::time::timeout` cancels the accept when the deadline passes and the flow converts that into this error. It means the user never completed the browser authorization in time.

Solutions

  1. Retry the login and complete the browser authorization promptly (within 5 minutes).
  2. Open the printed auth URL manually if the browser did not launch automatically.
  3. Check network/VPN/proxy issues that might block the redirect back to 127.0.0.1.
  4. If you consistently need longer, the flow must be re-run — the challenge/verifier pair is single-use.
Defensive patterns

Strategy: retry

Try / catch

if let Err(e) = run_oauth_flow(&mut app).await {
    if e.to_string().contains("OAuth callback timeout") {
        // single-use verifier: restart the whole flow rather than waiting longer
        prompt_user("Login timed out; press Enter to retry");
        run_oauth_flow(&mut app).await?;
    }
}

Prevention

When it happens

Trigger: `listener.accept()` wrapped in a 300-second `tokio::time::timeout` elapses inside `wait_for_callback` — no callback connection was received before the deadline.

Common situations: User switched away and never finished browser login; browser failed to open or the auth URL wasn't visited; the authorization page hung on a captive portal/VPN; user took longer than 5 minutes to enter credentials/2FA.

Understand the failure class

Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.

Related errors


AI-assisted analysis of Kuberwastaken/claurst@b0637c97ec (2026-09-10). Data as JSON: /api/errors/697fe7ea73e358cf. Report an issue: GitHub.

Appendix: source

Thrown at src-rust/crates/cli/src/codex_oauth_flow.rs:120

    let tokens = exchange_code_for_tokens(&code, &verifier).await?;

    // Persist tokens and register an account profile in the registry.
    claurst_core::oauth_config::save_codex_tokens_and_register(&tokens, label)?;

    eprintln!("Codex login successful!");
    Ok(tokens)
}

/// Wait for OAuth callback on local server, extract code and state.
async fn wait_for_callback(listener: TcpListener) -> anyhow::Result<(String, String)> {
    use tokio::io::AsyncWriteExt;

    let (mut socket, _) = tokio::time::timeout(
        std::time::Duration::from_secs(300), // 5 minute timeout
        listener.accept(),
    )
    .await
    .map_err(|_| anyhow!("OAuth callback timeout (5 minutes)"))?
    .map_err(|e| anyhow!("Failed to accept connection: {}", e))?;

    let mut reader = BufReader::new(&mut socket);
    let mut request_line = String::new();
    reader.read_line(&mut request_line).await?;

    // Parse "GET /auth/callback?code=...&state=... HTTP/1.1"
    let parts: Vec<&str> = request_line.split_whitespace().collect();
    if parts.len() < 2 {
        bail!("Invalid HTTP request");
    }

    let path = parts[1];
    let query_start = path.find('?').ok_or_else(|| anyhow!("No query string in callback"))?;
    let query = &path[query_start + 1..];

    let mut code = String::new();
    let mut state = String::new();

View on GitHub (pinned to b0637c97ec)