Skip to content

login intermittently returns {"error":"no auth token received"} despite valid credentials (connect/login race) #190

Description

@satan-stone

Summary

kuma login (and token retrieval generally) intermittently fails with:

{"error":"no auth token received"}

even though the credentials are valid and login succeeds on the server. On a fast/low-latency Uptime Kuma instance the failure becomes deterministic (fails every attempt). The root cause is a race in Client::connect(): it returns as soon as the initial data lists have been received, without waiting for the login acknowledgement to be stored, so get_auth_token() reads None.

Environment

  • AutoKuma commit: 1cba1f0e12ce001b115f93507130059f35a6613e
  • kuma-cli login command, connecting over websocket (TLS) to a self-hosted Uptime Kuma behind a reverse proxy
  • Auth: username/password, no 2FA, credentials verified working via the Uptime Kuma web UI

Observed behavior

With RUST_LOG=debug, a failing run shows:

DEBUG [tungstenite::handshake::client] Client handshake done.
DEBUG [kuma_client::client] Waiting for connection
DEBUG [kuma_client::client] Connection opened!
DEBUG [kuma_client::client] Waiting for Kuma to get ready...
DEBUG [kuma_client::client] Waiting for Kuma to get ready...
DEBUG [kuma_client::client] Waiting for Kuma to get ready...
DEBUG [kuma_client::client] Waiting for Kuma to get ready...
DEBUG [kuma_client::client] Connected!

Note that Connected! is printed but Logged in as ...! is never printed. No warning/error is logged from login(). The command then prints {"error":"no auth token received"} and exits.

Because Uptime Kuma only pushes monitorList / notificationList / etc. to an authenticated socket, the fact that is_ready() became true proves the server accepted the login and issued a token — the client simply returned from connect() before that token was stored locally.

Root cause

In kuma-client/src/client.rs, Worker::connect() finishes based solely on is_ready():

for i in 0..10 {
    if self.is_ready().await {
        debug!("Connected!");
        return Ok(());
    }
    debug!("Waiting for Kuma to get ready...");
    tokio::time::sleep(Duration::from_millis(200 * i)).await;
}

and is_ready() only checks the five data-list flags — it never checks is_logged_in or the presence of an auth token:

pub fn is_ready(&self) -> bool {
    self.monitor_list && self.notification_list && self.maintenance_list
        && self.status_page_list && self.docker_host_list
}

Each incoming socket.io event is handled in its own spawned task (handle.spawn(...) in the on_any handler), so the login-ack task (which stores the token in login()) and the list-event tasks (which set the readiness flags) run concurrently and unordered. When the list events win, connect() returns before login() stores the token, and login.rs reads None:

let auth_token = client.get_auth_token().await;
if let Some(token) = auth_token {
    // ok
} else {
    print_value(&json!({"error" : "no auth token received"}), cli); // <-- here
}

On a fast server the lists consistently beat the ack, so it fails every time; on a slower connection it's intermittent. A shell-level retry loop does not help, because the ordering is deterministic per environment.

Suggested fix

Make connect() also wait for login to complete when credentials/a token are configured, and store the token before flipping the logged-in flag so the flag never leads the token. Minimal patch:

@@ login() success arm
                 debug!("Logged in as {}!", username.as_ref());
-                *self.is_logged_in.lock().await = true;
                 *self.auth_token.lock().await = Some(auth_token);
+                *self.is_logged_in.lock().await = true;
                 Ok(())
@@ Worker::connect() readiness loop
-        for i in 0..10 {
-            if self.is_ready().await {
+        let needs_login =
+            self.config.username.is_some() || self.config.auth_token.is_some();
+        for i in 0..15 {
+            let ready = self.is_ready().await;
+            let logged_in = !needs_login || *self.is_logged_in.lock().await;
+            if ready && logged_in {
                 debug!("Connected!");
                 return Ok(());
             }
             debug!("Waiting for Kuma to get ready...");
             tokio::time::sleep(Duration::from_millis(200 * i)).await;
         }

Guarding on needs_login preserves current behavior for auth-disabled instances (where no login occurs and no token is expected). This has been tested against 1cba1f0 and resolves the failures on an instance where it was previously 100% reproducible.

Secondary observation

login.rs collapses every failure mode (wrong password, rate-limit, this race) into the single generic message {"error":"no auth token received"}, which makes diagnosis hard. It would help to surface the underlying reason (e.g. the server msg, or distinguish "login not completed" from "login rejected").

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions