Skip to content

Commit f41ffdc

Browse files
cjfitjohntmyers
authored andcommitted
feat(server): hint re-login when subscription token refresh is rejected
When the anthropic-oauth token endpoint rejects the refresh grant with HTTP 4xx (revoked login, lapsed plan), append recovery guidance to the stored refresh error so provider refresh status tells the operator to re-authenticate on the host and recreate the provider. Transient network errors and 5xx responses are excluded. Signed-off-by: Cedric Fitzgerald <soulcedric2@gmail.com>
1 parent 0a3b3b1 commit f41ffdc

1 file changed

Lines changed: 60 additions & 3 deletions

File tree

crates/openshell-server/src/provider_refresh.rs

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,14 @@ pub async fn refresh_provider_credential(
468468
}
469469
Err(err) => {
470470
let now_ms = current_time_ms();
471+
let err = if anthropic_subscription_login_rejected(&provider, &err) {
472+
Status::new(
473+
err.code(),
474+
format!("{}{ANTHROPIC_RELOGIN_HINT}", err.message()),
475+
)
476+
} else {
477+
err
478+
};
471479
state.status = "error".to_string();
472480
state.last_error = err.message().to_string();
473481
state.next_refresh_at_ms =
@@ -488,6 +496,18 @@ pub async fn refresh_provider_credential(
488496
}
489497
}
490498

499+
const ANTHROPIC_RELOGIN_HINT: &str = "; the subscription login is no longer valid — run `claude login` on the host, then delete and recreate the provider with `openshell provider create --from-claude-login`";
500+
501+
/// True when an `anthropic-oauth` refresh failed because the token endpoint
502+
/// rejected the grant (revoked login, lapsed plan) rather than a transient
503+
/// network or gateway error, so re-authenticating on the host is the fix.
504+
fn anthropic_subscription_login_rejected(provider: &Provider, err: &Status) -> bool {
505+
openshell_core::inference::normalize_inference_provider_type(&provider.r#type)
506+
== Some("anthropic-oauth")
507+
&& err.code() == tonic::Code::FailedPrecondition
508+
&& err.message().contains("token endpoint returned HTTP 4")
509+
}
510+
491511
async fn apply_minted_credential(
492512
store: &Store,
493513
provider: &Provider,
@@ -1060,9 +1080,9 @@ async fn run_refresh_worker_tick(store: &Store) -> Result<(), Status> {
10601080
#[cfg(test)]
10611081
mod tests {
10621082
use super::{
1063-
NewRefreshStateConfig, delete_refresh_state, get_refresh_state, new_refresh_state,
1064-
put_refresh_state, refresh_provider_credential, refresh_state_name, refresh_strategy_name,
1065-
run_refresh_worker_tick, seconds_until_ms,
1083+
NewRefreshStateConfig, anthropic_subscription_login_rejected, delete_refresh_state,
1084+
get_refresh_state, new_refresh_state, put_refresh_state, refresh_provider_credential,
1085+
refresh_state_name, refresh_strategy_name, run_refresh_worker_tick, seconds_until_ms,
10661086
};
10671087
use crate::persistence::test_store;
10681088
use openshell_core::ObjectId;
@@ -1071,6 +1091,7 @@ mod tests {
10711091
Provider, ProviderCredentialRefreshStrategy, Sandbox, SandboxSpec,
10721092
};
10731093
use std::collections::HashMap;
1094+
use tonic::Status;
10741095
use wiremock::matchers::{body_string_contains, method, path};
10751096
use wiremock::{Mock, MockServer, ResponseTemplate};
10761097

@@ -2237,6 +2258,42 @@ mod tests {
22372258
}
22382259
}
22392260

2261+
#[test]
2262+
fn relogin_hint_applies_only_to_anthropic_oauth_grant_rejection() {
2263+
let rejected = Status::failed_precondition("token endpoint returned HTTP 400 Bad Request");
2264+
assert!(anthropic_subscription_login_rejected(
2265+
&provider("plan", "anthropic-oauth"),
2266+
&rejected
2267+
));
2268+
assert!(
2269+
anthropic_subscription_login_rejected(
2270+
&provider("plan", "claude-subscription"),
2271+
&rejected
2272+
),
2273+
"type alias must get the hint too"
2274+
);
2275+
assert!(
2276+
!anthropic_subscription_login_rejected(
2277+
&provider("plan", "anthropic-oauth"),
2278+
&Status::unavailable("token endpoint request failed: timeout")
2279+
),
2280+
"transient network errors must not suggest re-login"
2281+
);
2282+
assert!(
2283+
!anthropic_subscription_login_rejected(
2284+
&provider("plan", "anthropic-oauth"),
2285+
&Status::failed_precondition(
2286+
"token endpoint returned HTTP 500 Internal Server Error"
2287+
)
2288+
),
2289+
"server-side errors must not suggest re-login"
2290+
);
2291+
assert!(!anthropic_subscription_login_rejected(
2292+
&provider("graph", "custom"),
2293+
&rejected
2294+
));
2295+
}
2296+
22402297
const TEST_RSA_PRIVATE_KEY: &str = r"-----BEGIN PRIVATE KEY-----
22412298
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCvCoZ0mVHpCHsF
22422299
zeeqw2caNIe/eb4BQUccFPhZfRnF7sCfyB84zTBmuwG2umRBdjFnVsfIIZRp2HcD

0 commit comments

Comments
 (0)