Say when a usage 401 is an expired login, not an unreachable endpoint
A 401 is the endpoint answering and refusing the stored OAuth token, which Claude Code refreshes as it runs -- so a machine whose CLI has been idle hands us a stale one. Reporting it as "usage endpoint unreachable" pointed at the network instead of at the one thing that fixes it.
This commit is contained in:
1 parent
6bdec6e785
commit
7b63330aaa
1 file changed
+30
-9
+30
-9
@@ -206,15 +206,8 @@ impl UsageProvider for ClaudeUsage {
|
||||
.and_then(|mut response| response.body_mut().read_to_string())
|
||||
{
|
||||
Ok(text) => text,
|
||||
Err(err) => {
|
||||
// The error string can embed the URL but never the token.
|
||||
return self.snapshot(
|
||||
UsageState::Failed {
|
||||
detail: format!("usage endpoint unreachable: {err}"),
|
||||
},
|
||||
Vec::new(),
|
||||
);
|
||||
}
|
||||
// The error string can embed the URL but never the token.
|
||||
Err(err) => return self.snapshot(UsageState::Failed { detail: why(&err) }, Vec::new()),
|
||||
};
|
||||
let body: Value = match serde_json::from_str(&text) {
|
||||
Ok(body) => body,
|
||||
@@ -231,6 +224,25 @@ impl UsageProvider for ClaudeUsage {
|
||||
}
|
||||
}
|
||||
|
||||
/// What a failed call to the usage endpoint should say.
|
||||
///
|
||||
/// A status is not a network fault and must not be reported as one: the
|
||||
/// endpoint answered, and 401 in particular says the stored token has expired
|
||||
/// -- Claude Code refreshes it as it runs, so a machine whose CLI has been
|
||||
/// idle long enough hands us a stale one. That is fixable, and the message is
|
||||
/// the only place anybody finds out how.
|
||||
fn why(err: &ureq::Error) -> String {
|
||||
match err {
|
||||
ureq::Error::StatusCode(401) => {
|
||||
format!(
|
||||
"the Claude login on this machine has expired (401); run `claude` there, or re-run `/login`, to refresh {CREDENTIALS}"
|
||||
)
|
||||
}
|
||||
ureq::Error::StatusCode(code) => format!("usage endpoint refused the request: HTTP {code}"),
|
||||
other => format!("usage endpoint unreachable: {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Which kind of "no credentials" a failed read was.
|
||||
///
|
||||
/// The distinction is the point of having both states. `cat` failing because
|
||||
@@ -631,6 +643,15 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::config::DriverKind;
|
||||
|
||||
#[test]
|
||||
fn an_expired_login_is_not_reported_as_an_unreachable_endpoint() {
|
||||
let stale = why(&ureq::Error::StatusCode(401));
|
||||
assert!(stale.contains("expired"), "{stale}");
|
||||
assert!(!stale.contains("unreachable"), "{stale}");
|
||||
assert!(why(&ureq::Error::StatusCode(500)).contains("HTTP 500"));
|
||||
assert!(why(&ureq::Error::HostNotFound).contains("unreachable"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_the_limits_array_defensively() {
|
||||
// Trimmed from a live 2026-08-24 response.
|
||||
|
||||
Reference in new issue
Block a user