From 4ec09d30301cad71bcef9394a30d6cc9aedab6bd Mon Sep 17 00:00:00 2001 From: Rahul Thathoo Date: Sun, 10 May 2026 17:00:52 -0700 Subject: [PATCH] Handle transient API key validation failures --- .../request_processors/account_processor.rs | 15 +++++++ codex-rs/app-server/tests/suite/v2/account.rs | 42 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/codex-rs/app-server/src/request_processors/account_processor.rs b/codex-rs/app-server/src/request_processors/account_processor.rs index d174aca80b..0ebff10c97 100644 --- a/codex-rs/app-server/src/request_processors/account_processor.rs +++ b/codex-rs/app-server/src/request_processors/account_processor.rs @@ -45,6 +45,10 @@ enum RefreshTokenRequestOutcome { FailedPermanently, } +fn api_key_validation_unavailable() -> JSONRPCErrorError { + invalid_request("Could not validate API key right now. Check your connection and try again.") +} + impl Drop for ActiveLogin { fn drop(&mut self) { self.cancel(); @@ -297,6 +301,17 @@ impl AccountRequestProcessor { CodexErr::UnexpectedStatus(err) if matches!(err.status.as_u16(), 401 | 403) => { invalid_request("API key is invalid or unusable.") } + CodexErr::UnexpectedStatus(err) + if err.status.is_server_error() || matches!(err.status.as_u16(), 408 | 429) => + { + api_key_validation_unavailable() + } + CodexErr::Timeout + | CodexErr::Stream(..) + | CodexErr::ResponseStreamFailed(_) + | CodexErr::ConnectionFailed(_) + | CodexErr::InternalServerError + | CodexErr::RetryLimit(_) => api_key_validation_unavailable(), err => internal_error(format!("failed to validate api key: {err}")), }) } diff --git a/codex-rs/app-server/tests/suite/v2/account.rs b/codex-rs/app-server/tests/suite/v2/account.rs index 1e6908eaf2..33a71bd5e5 100644 --- a/codex-rs/app-server/tests/suite/v2/account.rs +++ b/codex-rs/app-server/tests/suite/v2/account.rs @@ -994,6 +994,48 @@ async fn login_account_api_key_rejects_unusable_key_before_persisting() -> Resul Ok(()) } +#[tokio::test] +async fn login_account_api_key_validation_transient_failure_is_retryable() -> Result<()> { + let codex_home = TempDir::new()?; + let mock_server = MockServer::start().await; + create_config_toml( + codex_home.path(), + CreateConfigTomlParams { + requires_openai_auth: Some(true), + base_url: Some(format!("{}/v1", mock_server.uri())), + ..Default::default() + }, + )?; + + Mock::given(method("GET")) + .and(path("/v1/models")) + .respond_with(ResponseTemplate::new(500).set_body_json(json!({ + "error": { "message": "temporary outage" } + }))) + .expect(1) + .mount(&mock_server) + .await; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let request_id = mcp + .send_login_account_api_key_request("sk-test-key") + .await?; + let err: JSONRPCError = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(request_id)), + ) + .await??; + + assert_eq!( + err.error.message, + "Could not validate API key right now. Check your connection and try again." + ); + assert!(!codex_home.path().join("auth.json").exists()); + Ok(()) +} + #[tokio::test] async fn login_account_api_key_rejected_when_forced_chatgpt() -> Result<()> { let codex_home = TempDir::new()?;