mirror of
https://github.com/openai/codex.git
synced 2026-05-02 12:21:26 +03:00
fix: properly handle 401 error in clound requirement fetch. (#14049)
Handle cloud requirements 401s with the same auth recovery flow as normal requests, so permanent refresh failures surface the existing user-facing auth message instead of a generic workspace-config load error.
This commit is contained in:
@@ -10,6 +10,7 @@ use codex_protocol::account::PlanType as AccountPlanType;
|
||||
use codex_protocol::protocol::CreditsSnapshot;
|
||||
use codex_protocol::protocol::RateLimitSnapshot;
|
||||
use codex_protocol::protocol::RateLimitWindow;
|
||||
use reqwest::StatusCode;
|
||||
use reqwest::header::AUTHORIZATION;
|
||||
use reqwest::header::CONTENT_TYPE;
|
||||
use reqwest::header::HeaderMap;
|
||||
@@ -17,6 +18,65 @@ use reqwest::header::HeaderName;
|
||||
use reqwest::header::HeaderValue;
|
||||
use reqwest::header::USER_AGENT;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RequestError {
|
||||
UnexpectedStatus {
|
||||
method: String,
|
||||
url: String,
|
||||
status: StatusCode,
|
||||
content_type: String,
|
||||
body: String,
|
||||
},
|
||||
Other(anyhow::Error),
|
||||
}
|
||||
|
||||
impl RequestError {
|
||||
pub fn status(&self) -> Option<StatusCode> {
|
||||
match self {
|
||||
Self::UnexpectedStatus { status, .. } => Some(*status),
|
||||
Self::Other(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_unauthorized(&self) -> bool {
|
||||
self.status() == Some(StatusCode::UNAUTHORIZED)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for RequestError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::UnexpectedStatus {
|
||||
method,
|
||||
url,
|
||||
status,
|
||||
content_type,
|
||||
body,
|
||||
} => write!(
|
||||
f,
|
||||
"{method} {url} failed: {status}; content-type={content_type}; body={body}"
|
||||
),
|
||||
Self::Other(err) => write!(f, "{err}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for RequestError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::UnexpectedStatus { .. } => None,
|
||||
Self::Other(err) => Some(err.as_ref()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for RequestError {
|
||||
fn from(err: anyhow::Error) -> Self {
|
||||
Self::Other(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum PathStyle {
|
||||
@@ -148,6 +208,33 @@ impl Client {
|
||||
Ok((body, ct))
|
||||
}
|
||||
|
||||
async fn exec_request_detailed(
|
||||
&self,
|
||||
req: reqwest::RequestBuilder,
|
||||
method: &str,
|
||||
url: &str,
|
||||
) -> std::result::Result<(String, String), RequestError> {
|
||||
let res = req.send().await.map_err(anyhow::Error::from)?;
|
||||
let status = res.status();
|
||||
let content_type = res
|
||||
.headers()
|
||||
.get(CONTENT_TYPE)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let body = res.text().await.unwrap_or_default();
|
||||
if !status.is_success() {
|
||||
return Err(RequestError::UnexpectedStatus {
|
||||
method: method.to_string(),
|
||||
url: url.to_string(),
|
||||
status,
|
||||
content_type,
|
||||
body,
|
||||
});
|
||||
}
|
||||
Ok((body, content_type))
|
||||
}
|
||||
|
||||
fn decode_json<T: DeserializeOwned>(&self, url: &str, ct: &str, body: &str) -> Result<T> {
|
||||
match serde_json::from_str::<T>(body) {
|
||||
Ok(v) => Ok(v),
|
||||
@@ -256,14 +343,17 @@ impl Client {
|
||||
///
|
||||
/// `GET /api/codex/config/requirements` (Codex API style) or
|
||||
/// `GET /wham/config/requirements` (ChatGPT backend-api style).
|
||||
pub async fn get_config_requirements_file(&self) -> Result<ConfigFileResponse> {
|
||||
pub async fn get_config_requirements_file(
|
||||
&self,
|
||||
) -> std::result::Result<ConfigFileResponse, RequestError> {
|
||||
let url = match self.path_style {
|
||||
PathStyle::CodexApi => format!("{}/api/codex/config/requirements", self.base_url),
|
||||
PathStyle::ChatGptApi => format!("{}/wham/config/requirements", self.base_url),
|
||||
};
|
||||
let req = self.http.get(&url).headers(self.headers());
|
||||
let (body, ct) = self.exec_request(req, "GET", &url).await?;
|
||||
let (body, ct) = self.exec_request_detailed(req, "GET", &url).await?;
|
||||
self.decode_json::<ConfigFileResponse>(&url, &ct, &body)
|
||||
.map_err(RequestError::from)
|
||||
}
|
||||
|
||||
/// Create a new task (user turn) by POSTing to the appropriate backend path
|
||||
|
||||
@@ -2,6 +2,7 @@ mod client;
|
||||
pub mod types;
|
||||
|
||||
pub use client::Client;
|
||||
pub use client::RequestError;
|
||||
pub use types::CodeTaskDetailsResponse;
|
||||
pub use types::CodeTaskDetailsResponseExt;
|
||||
pub use types::ConfigFileResponse;
|
||||
|
||||
Reference in New Issue
Block a user