Skip to content

Commit 9639a11

Browse files
max-zheng64bit
andauthored
Return http status code for ApiError (#552)
* Return http status code for ApiError * doc comments on ApiErrorResponse * cargo fmt --------- Co-authored-by: Himanshu Neema <himanshun.iitkgp@gmail.com>
1 parent 8b788ae commit 9639a11

3 files changed

Lines changed: 46 additions & 12 deletions

File tree

async-openai/src/client.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use crate::error::StreamError;
1212
use crate::executor::TowerExecutor;
1313
use crate::{
1414
config::{Config, OpenAIConfig},
15-
error::{map_deserialization_error, ApiError, OpenAIError, WrappedError},
15+
error::{map_deserialization_error, ApiError, ApiErrorResponse, OpenAIError, WrappedError},
1616
executor::{HttpRequestFactory, ReqwestExecutor, SharedExecutor},
1717
traits::AsyncTryFrom,
1818
RequestOptions,
@@ -732,11 +732,14 @@ async fn read_response(response: Response) -> Result<(Bytes, HeaderMap), OpenAIE
732732
// OpenAI does not guarantee server errors are returned as JSON so we cannot deserialize them.
733733
let message: String = String::from_utf8_lossy(&bytes).into_owned();
734734
tracing::warn!("Server error: {status} - {message}");
735-
return Err(OpenAIError::ApiError(ApiError {
736-
message,
737-
r#type: None,
738-
param: None,
739-
code: None,
735+
return Err(OpenAIError::ApiError(ApiErrorResponse {
736+
status_code: status,
737+
api_error: ApiError {
738+
message,
739+
r#type: None,
740+
param: None,
741+
code: None,
742+
},
740743
}));
741744
}
742745

@@ -745,7 +748,10 @@ async fn read_response(response: Response) -> Result<(Bytes, HeaderMap), OpenAIE
745748
let wrapped_error: WrappedError = serde_json::from_slice(bytes.as_ref())
746749
.map_err(|e| map_deserialization_error(e, bytes.as_ref()))?;
747750

748-
return Err(OpenAIError::ApiError(wrapped_error.error));
751+
return Err(OpenAIError::ApiError(ApiErrorResponse {
752+
status_code: status,
753+
api_error: wrapped_error.error,
754+
}));
749755
}
750756

751757
Ok((bytes, headers))

async-openai/src/error.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ pub enum OpenAIError {
88
/// Underlying error from reqwest library after an API call was made
99
#[error("http error: {0}")]
1010
Reqwest(#[from] reqwest::Error),
11-
/// OpenAI returns error object with details of API call failure
11+
/// OpenAI returns error object with details of API call failure, along
12+
/// with the HTTP status code from the response.
1213
#[error("{0}")]
13-
ApiError(ApiError),
14+
ApiError(ApiErrorResponse),
1415
/// Error when a response cannot be deserialized into a Rust type
1516
#[error("failed to deserialize api response: error:{0} content:{1}")]
1617
JSONDeserialize(serde_json::Error, String),
@@ -109,6 +110,26 @@ impl std::fmt::Display for ApiError {
109110

110111
impl std::error::Error for ApiError {}
111112

113+
/// `ApiError` paired with the HTTP status code from the response.
114+
#[cfg(feature = "_api")]
115+
#[derive(Debug, Clone)]
116+
pub struct ApiErrorResponse {
117+
/// HTTP status code
118+
pub status_code: reqwest::StatusCode,
119+
/// Parsed error from response
120+
pub api_error: ApiError,
121+
}
122+
123+
#[cfg(feature = "_api")]
124+
impl std::fmt::Display for ApiErrorResponse {
125+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126+
write!(f, "{} {}", self.status_code, self.api_error)
127+
}
128+
}
129+
130+
#[cfg(feature = "_api")]
131+
impl std::error::Error for ApiErrorResponse {}
132+
112133
/// Wrapper to deserialize the error object nested in "error" JSON key
113134
#[derive(Debug, Deserialize, Serialize)]
114135
pub struct WrappedError {

async-openai/src/middleware/retry/openai.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::{future::Future, pin::Pin, time::Duration};
33
use reqwest::{header::HeaderMap, Response};
44

55
use crate::{
6-
error::{OpenAIError, WrappedError},
6+
error::{ApiErrorResponse, OpenAIError, WrappedError},
77
executor::HttpRequestFactory,
88
};
99

@@ -165,6 +165,7 @@ where
165165
let (final_result, headers, retry_after) = match result {
166166
Ok(response) if response.status().is_success() => return Ok(response),
167167
Ok(response) if response.status().as_u16() == 429 => {
168+
let status_code = response.status();
168169
let headers = response.headers().clone();
169170
let retry_after = retry_after(&headers);
170171
let bytes = match response.bytes().await {
@@ -177,10 +178,16 @@ where
177178
// 429 and insufficient_quota are treated as permanent error.
178179
// https://developers.openai.com/api/docs/guides/error-codes
179180
if wrapped_error.error.r#type.as_deref() == Some(INSUFFICIENT_QUOTA) {
180-
return Err(OpenAIError::ApiError(wrapped_error.error));
181+
return Err(OpenAIError::ApiError(ApiErrorResponse {
182+
status_code,
183+
api_error: wrapped_error.error,
184+
}));
181185
}
182186

183-
OpenAIError::ApiError(wrapped_error.error)
187+
OpenAIError::ApiError(ApiErrorResponse {
188+
status_code,
189+
api_error: wrapped_error.error,
190+
})
184191
}
185192
Err(error) => {
186193
return Err(OpenAIError::JSONDeserialize(

0 commit comments

Comments
 (0)