jlcodes99/cockpit-tools · error

[Windsurf OAuth] 回调服务异常: login_id={}, error={}

Error message

[Windsurf OAuth] 回调服务异常: login_id={}, error={}

What it means

This error is logged when the local HTTP callback server used to receive the Windsurf OAuth redirect fails to start or aborts mid-flow inside a tokio::spawn task. The login record (pending login, state token) has already been staged, so the browser will complete the redirect but nothing is listening on the port, leaving the login hanging. It is a wrapper around the underlying io/BindError from start_callback_server.

Source

Thrown at crates/cockpit-core/src/modules/windsurf_oauth.rs:1072

    let pending = PendingOAuthState {
        login_id: login_id.clone(),
        state: state_token.clone(),
        auth_url: auth_url.clone(),
        callback_url: callback_url.clone(),
        port,
        created_at: now_timestamp(),
        expires_at: now_timestamp() + OAUTH_TIMEOUT_SECONDS as i64,
        access_token: None,
        callback_error: None,
    };

    set_pending_login(Some(pending.clone()));

    let callback_login_id = login_id.clone();
    let callback_state = state_token.clone();
    tokio::spawn(async move {
        if let Err(e) = start_callback_server(port, callback_login_id, callback_state).await {
            logger::log_error(&format!(
                "[Windsurf OAuth] 回调服务异常: login_id={}, error={}",
                login_id, e
            ));
        }
    });

    logger::log_info(&format!(
        "[Windsurf OAuth] 登录会话已创建: login_id={}, callback_url={}",
        pending.login_id, pending.callback_url
    ));
    Ok(to_start_response(&pending))
}

pub async fn complete_login(login_id: &str) -> Result<WindsurfOAuthCompletePayload, String> {
    hydrate_pending_login_if_missing();
    let token = loop {
        let state = {
            let guard = PENDING_OAUTH_STATE

View on GitHub (pinned to 1ed8b77992)

Solutions

  1. Check that the configured callback port is free (lsof -i :<port> / netstat) and kill the stale listener before retrying login
  2. Retry the login — each start_login call picks a port; avoid launching two logins in parallel
  3. Inspect the wrapped error in the log line to distinguish bind failure (port in use) from runtime failure
  4. Update the app so the callback server uses an ephemeral port or retries on bind failure

Example fix

// before
let port = 14657;
tokio::spawn(async move {
    if let Err(e) = start_callback_server(port, callback_login_id, callback_state).await {
        logger::log_error(&format!("[Windsurf OAuth] 回调服务异常: login_id={}, error={}", login_id, e));
    }
});
// after
// bind is now attempted eagerly so the user gets an immediate error instead of a silent hang
let listener = TcpListener::bind(("127.0.0.1", port)).await
    .map_err(|e| format!("回调端口 {} 被占用: {}", port, e))?;
tokio::spawn(async move {
    if let Err(e) = start_callback_server_with_listener(listener, callback_login_id, callback_state).await {
        logger::log_error(&format!("[Windsurf OAuth] 回调服务异常: login_id={}, error={}", login_id, e));
    }
});
Defensive patterns

Strategy: validation

Validate before calling

// before starting login, ensure the callback port is bindable
async fn callback_port_free(port: u16) -> bool {
    tokio::net::TcpListener::bind(("127.0.0.1", port)).await.is_ok()
}
if !callback_port_free(port).await {
    return Err(format!("回调端口 {} 已被占用,请关闭占用进程后重试", port));
}

Try / catch

// the spawn swallows the error, so watch for the absence of a completion instead
match tokio::time::timeout(Duration::from_secs(120), wait_for_login_result(&login_id)).await {
    Ok(result) => result,
    Err(_) => Err("登录超时:回调服务可能启动失败,请检查端口占用后重试".to_string()),
}

Prevention

When it happens

Trigger: start_login() calls start_callback_server(port, login_id, state_token) in a spawned task and that future returns Err — e.g. the port is already bound by another instance or stale process, or the server task panics/errors while awaiting the callback.

Common situations: Running two login attempts concurrently on the same port; a previous crashed session left a listener on the port; firewall/security software blocking local socket binds; port collision with another dev tool listening on localhost.

Related errors


AI-assisted analysis of jlcodes99/cockpit-tools@1ed8b77992 (2026-09-05). Data as JSON: /api/errors/a37fd17d090837e1. Report an issue: GitHub.