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").
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, soget_auth_token()readsNone.Environment
1cba1f0e12ce001b115f93507130059f35a6613elogincommand, connecting over websocket (TLS) to a self-hosted Uptime Kuma behind a reverse proxyObserved behavior
With
RUST_LOG=debug, a failing run shows:Note that
Connected!is printed butLogged in as ...!is never printed. No warning/error is logged fromlogin(). 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 thatis_ready()became true proves the server accepted the login and issued a token — the client simply returned fromconnect()before that token was stored locally.Root cause
In
kuma-client/src/client.rs,Worker::connect()finishes based solely onis_ready():and
is_ready()only checks the five data-list flags — it never checksis_logged_inor the presence of an auth token:Each incoming socket.io event is handled in its own spawned task (
handle.spawn(...)in theon_anyhandler), so the login-ack task (which stores the token inlogin()) and the list-event tasks (which set the readiness flags) run concurrently and unordered. When the list events win,connect()returns beforelogin()stores the token, andlogin.rsreadsNone: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_loginpreserves current behavior for auth-disabled instances (where no login occurs and no token is expected). This has been tested against1cba1f0and resolves the failures on an instance where it was previously 100% reproducible.Secondary observation
login.rscollapses 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 servermsg, or distinguish "login not completed" from "login rejected").