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())
|
.and_then(|mut response| response.body_mut().read_to_string())
|
||||||
{
|
{
|
||||||
Ok(text) => text,
|
Ok(text) => text,
|
||||||
Err(err) => {
|
// The error string can embed the URL but never the token.
|
||||||
// The error string can embed the URL but never the token.
|
Err(err) => return self.snapshot(UsageState::Failed { detail: why(&err) }, Vec::new()),
|
||||||
return self.snapshot(
|
|
||||||
UsageState::Failed {
|
|
||||||
detail: format!("usage endpoint unreachable: {err}"),
|
|
||||||
},
|
|
||||||
Vec::new(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
let body: Value = match serde_json::from_str(&text) {
|
let body: Value = match serde_json::from_str(&text) {
|
||||||
Ok(body) => body,
|
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.
|
/// Which kind of "no credentials" a failed read was.
|
||||||
///
|
///
|
||||||
/// The distinction is the point of having both states. `cat` failing because
|
/// The distinction is the point of having both states. `cat` failing because
|
||||||
@@ -631,6 +643,15 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::config::DriverKind;
|
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]
|
#[test]
|
||||||
fn parses_the_limits_array_defensively() {
|
fn parses_the_limits_array_defensively() {
|
||||||
// Trimmed from a live 2026-08-24 response.
|
// Trimmed from a live 2026-08-24 response.
|
||||||
|
|||||||
Reference in new issue
Block a user