mirror of
https://github.com/openai/codex.git
synced 2026-05-28 15:00:16 +00:00
Treat refresh_token_reused 400s as relogin-required (#24830)
## Summary - classify known refresh-token terminal failures from `/oauth/token` as permanent even when the backend returns `400` - preserve the existing relogin-required message for `refresh_token_reused` instead of retrying and collapsing into a generic cloud requirements error - add regression coverage for `400 refresh_token_reused` ## Testing - `just fmt` - `cargo test -p codex-login`
This commit is contained in:
@@ -844,8 +844,8 @@ async fn request_chatgpt_token_refresh(
|
||||
} else {
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
tracing::error!("Failed to refresh token: {status}: {body}");
|
||||
if status == StatusCode::UNAUTHORIZED {
|
||||
let failed = classify_refresh_token_failure(&body);
|
||||
let failed = classify_refresh_token_failure(&body);
|
||||
if status == StatusCode::UNAUTHORIZED || failed.reason != RefreshTokenFailedReason::Other {
|
||||
Err(RefreshTokenError::Permanent(failed))
|
||||
} else {
|
||||
let message = try_parse_error_message(&body);
|
||||
@@ -871,7 +871,7 @@ fn classify_refresh_token_failure(body: &str) -> RefreshTokenFailedError {
|
||||
tracing::warn!(
|
||||
backend_code = normalized_code.as_deref(),
|
||||
backend_body = body,
|
||||
"Encountered unknown 401 response while refreshing token"
|
||||
"Encountered unknown response while refreshing token"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -625,6 +625,73 @@ async fn refresh_token_does_not_retry_after_permanent_failure() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[serial_test::serial(auth_refresh)]
|
||||
#[tokio::test]
|
||||
async fn refresh_token_does_not_retry_after_bad_request_reused_failure() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/oauth/token"))
|
||||
.respond_with(ResponseTemplate::new(400).set_body_json(json!({
|
||||
"error": {
|
||||
"code": "refresh_token_reused"
|
||||
}
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let ctx = RefreshTokenTestContext::new(&server).await?;
|
||||
let initial_last_refresh = Utc::now() - Duration::days(1);
|
||||
let initial_tokens = build_tokens(INITIAL_ACCESS_TOKEN, INITIAL_REFRESH_TOKEN);
|
||||
let initial_auth = AuthDotJson {
|
||||
auth_mode: Some(AuthMode::Chatgpt),
|
||||
openai_api_key: None,
|
||||
tokens: Some(initial_tokens.clone()),
|
||||
last_refresh: Some(initial_last_refresh),
|
||||
agent_identity: None,
|
||||
};
|
||||
ctx.write_auth(&initial_auth).await?;
|
||||
|
||||
let first_err = ctx
|
||||
.auth_manager
|
||||
.refresh_token()
|
||||
.await
|
||||
.err()
|
||||
.context("first refresh should fail")?;
|
||||
assert_eq!(
|
||||
first_err.failed_reason(),
|
||||
Some(RefreshTokenFailedReason::Exhausted)
|
||||
);
|
||||
|
||||
let second_err = ctx
|
||||
.auth_manager
|
||||
.refresh_token()
|
||||
.await
|
||||
.err()
|
||||
.context("second refresh should fail without retrying")?;
|
||||
assert_eq!(
|
||||
second_err.failed_reason(),
|
||||
Some(RefreshTokenFailedReason::Exhausted)
|
||||
);
|
||||
|
||||
let stored = ctx.load_auth()?;
|
||||
assert_eq!(stored, initial_auth);
|
||||
let cached_auth = ctx
|
||||
.auth_manager
|
||||
.auth()
|
||||
.await
|
||||
.context("auth should remain cached")?;
|
||||
let cached = cached_auth
|
||||
.get_token_data()
|
||||
.context("token data should remain cached")?;
|
||||
assert_eq!(cached, initial_tokens);
|
||||
|
||||
server.verify().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[serial_test::serial(auth_refresh)]
|
||||
#[tokio::test]
|
||||
async fn refresh_token_reloads_changed_auth_after_permanent_failure() -> Result<()> {
|
||||
|
||||
Reference in New Issue
Block a user