diff --git a/codex-rs/core/src/auth.rs b/codex-rs/core/src/auth.rs index 85fb1721..b18cae5f 100644 --- a/codex-rs/core/src/auth.rs +++ b/codex-rs/core/src/auth.rs @@ -25,6 +25,7 @@ use crate::default_client::CodexHttpClient; use crate::token_data::PlanType; use crate::token_data::TokenData; use crate::token_data::parse_id_token; +use crate::util::try_parse_error_message; #[derive(Debug, Clone)] pub struct CodexAuth { @@ -42,6 +43,9 @@ impl PartialEq for CodexAuth { } } +// TODO(pakrym): use token exp field to check for expiration instead +const TOKEN_REFRESH_INTERVAL: i64 = 8; + impl CodexAuth { pub async fn refresh_token(&self) -> Result { tracing::info!("Refreshing token"); @@ -94,7 +98,7 @@ impl CodexAuth { last_refresh: Some(last_refresh), .. }) => { - if last_refresh < Utc::now() - chrono::Duration::days(28) { + if last_refresh < Utc::now() - chrono::Duration::days(TOKEN_REFRESH_INTERVAL) { let refresh_response = tokio::time::timeout( Duration::from_secs(60), try_refresh_token(tokens.refresh_token.clone(), &self.client), @@ -446,8 +450,9 @@ async fn try_refresh_token( Ok(refresh_response) } else { Err(std::io::Error::other(format!( - "Failed to refresh token: {}", - response.status() + "Failed to refresh token: {}: {}", + response.status(), + try_parse_error_message(&response.text().await.unwrap_or_default()), ))) } } diff --git a/codex-rs/core/src/util.rs b/codex-rs/core/src/util.rs index 8ef258d9..59e3154b 100644 --- a/codex-rs/core/src/util.rs +++ b/codex-rs/core/src/util.rs @@ -1,6 +1,7 @@ use std::time::Duration; use rand::Rng; +use tracing::debug; const INITIAL_DELAY_MS: u64 = 200; const BACKOFF_FACTOR: f64 = 2.0; @@ -11,3 +12,47 @@ pub(crate) fn backoff(attempt: u64) -> Duration { let jitter = rand::rng().random_range(0.9..1.1); Duration::from_millis((base as f64 * jitter) as u64) } + +pub(crate) fn try_parse_error_message(text: &str) -> String { + debug!("Parsing server error response: {}", text); + let json = serde_json::from_str::(text).unwrap_or_default(); + if let Some(error) = json.get("error") + && let Some(message) = error.get("message") + && let Some(message_str) = message.as_str() + { + return message_str.to_string(); + } + if text.is_empty() { + return "Unknown error".to_string(); + } + text.to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_try_parse_error_message() { + let text = r#"{ + "error": { + "message": "Your refresh token has already been used to generate a new access token. Please try signing in again.", + "type": "invalid_request_error", + "param": null, + "code": "refresh_token_reused" + } +}"#; + let message = try_parse_error_message(text); + assert_eq!( + message, + "Your refresh token has already been used to generate a new access token. Please try signing in again." + ); + } + + #[test] + fn test_try_parse_error_message_no_error() { + let text = r#"{"message": "test"}"#; + let message = try_parse_error_message(text); + assert_eq!(message, r#"{"message": "test"}"#); + } +}