From f4c470881e1eb0082f5e5e5f20beae8ad7c8560e Mon Sep 17 00:00:00 2001 From: atlas Date: Sun, 9 Aug 2026 21:31:04 +0200 Subject: [PATCH] fix(#2851): warn when the state read breaks but the write still works MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit argus's review note: current_room_state collapsed absent-state, transport failure and an unparseable body into one None, so a systematically failing GET was unobservable. Only one of the three actually hides. A transport error takes the PUT down with it one line later, and a 404 is the expected first-setup case — both stay at debug. A non-404 HTTP failure is the silent one: the read is broken while the write still succeeds, so the guard switches off and the sweep resumes emitting with nothing to show for it. That case, and only that case, warns. Keeping the warn narrow is the point: one that also fired on every expected 404 would train the reader to skip the line. --- hive-c0re/src/matrix.rs | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/hive-c0re/src/matrix.rs b/hive-c0re/src/matrix.rs index 1a4aca74..ed019656 100644 --- a/hive-c0re/src/matrix.rs +++ b/hive-c0re/src/matrix.rs @@ -1234,16 +1234,44 @@ fn state_needs_write(current: Option<&serde_json::Value>, desired: &serde_json:: /// Read the room's current content for one state event. `None` on any /// failure — absent state, transport error, or an unparseable body are /// all "we don't know", and [`state_needs_write`] turns that into a write. +/// +/// One failure is louder than the rest, and it is the only one that hides: +/// **a non-404 HTTP failure means the read broke while the write still +/// works**, so the guard degrades to writing every time and the sweep +/// resumes waking the hive with nothing else to show for it. A transport +/// error needs no warning of its own — the PUT immediately after it fails +/// too, loudly — and a 404 is the expected first-setup case. async fn current_room_state( client: &reqwest::Client, admin_token: &str, url: &str, ) -> Option { - let resp = client.get(url).bearer_auth(admin_token).send().await.ok()?; - if !resp.status().is_success() { + let resp = match client.get(url).bearer_auth(admin_token).send().await { + Ok(resp) => resp, + Err(e) => { + tracing::debug!(error = ?e, url, "matrix: state read unreachable; writing"); + return None; + } + }; + let status = resp.status(); + if !status.is_success() { + if status != reqwest::StatusCode::NOT_FOUND { + tracing::warn!( + %status, + url, + "matrix: state read failed while writes still work — the \ + skip-if-unchanged guard is off and every sweep will re-emit" + ); + } return None; } - resp.json::().await.ok() + match resp.json::().await { + Ok(body) => Some(body), + Err(e) => { + tracing::debug!(error = ?e, url, "matrix: state read body unparseable; writing"); + None + } + } } /// PUT a state event into `room_id` using the admin token, **skipping the