Kuberwastaken/claurst · error · anyhow::Error

Timeout waiting for OAuth callback

Error message

Timeout waiting for OAuth callback

What it means

wait_for_authorization_code wraps listener.accept() in a 180-second tokio timeout; if no browser connects to the callback within that window the timeout fires and this error is raised. The library gives up rather than blocking forever on a user who never completes the browser authorization.

Solutions

  1. Re-run the auth flow and complete the browser authorization promptly (within 3 minutes)
  2. Open the printed authorization URL manually if no browser launched
  3. Verify the configured redirect URI matches the listener port so the provider redirects to the local server
  4. In headless environments, forward the callback port (ssh -L) or use a device-code flow instead
Defensive patterns

Strategy: retry

Try / catch

loop {
    match run_mcp_auth_flow(url).await {
        Ok(s) => break s,
        Err(e) if e.to_string().contains("Timeout waiting") => {
            eprintln!("No callback within 180s; reopening browser...");
            open_browser(url);
        }
        Err(e) => return Err(e),
    }
}

Prevention

When it happens

Trigger: run_mcp_auth_session opened the auth URL but no HTTP request hits the local callback listener within 3 minutes — browser never opened, user closed the tab, or the redirect points at the wrong port/path.

Common situations: User ignores or misses the browser tab; headless/SSH environment with no browser; redirect URI port in config differs from the bound listener so the provider redirects elsewhere; slow 2FA/login flow exceeding 180s.

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/1977cee6c77cdd5a. Report an issue: GitHub.

Appendix: source

Thrown at src-rust/crates/mcp/src/oauth.rs:246

    } else {
        redirect_url.path().to_string()
    };
    let listener = TcpListener::bind((host.as_str(), port))
        .await
        .map_err(|e| anyhow::anyhow!("Failed to bind OAuth callback listener on {}:{}: {}", host, port, e))?;

    Ok((listener, host, callback_path))
}

async fn wait_for_authorization_code(
    listener: TcpListener,
    host: &str,
    callback_path: &str,
    expected_state: Option<&str>,
) -> anyhow::Result<String> {
    let (mut socket, _) = tokio::time::timeout(Duration::from_secs(180), listener.accept())
        .await
        .map_err(|_| anyhow::anyhow!("Timeout waiting for OAuth callback"))?
        .map_err(|e| anyhow::anyhow!("Failed to accept OAuth callback connection: {}", e))?;

    let (reader, mut writer) = socket.split();
    let mut reader = BufReader::new(reader);
    let mut request_line = String::new();
    reader
        .read_line(&mut request_line)
        .await
        .map_err(|e| anyhow::anyhow!("Failed to read OAuth callback request: {}", e))?;
    loop {
        let mut header = String::new();
        reader
            .read_line(&mut header)
            .await
            .map_err(|e| anyhow::anyhow!("Failed to read OAuth callback headers: {}", e))?;
        if header.trim().is_empty() {
            break;
        }

View on GitHub (pinned to b0637c97ec)