use std::time::Duration;
use serde::Deserialize;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ApiError {
pub error_type: String,
pub message: String,
pub request_id: Option<String>,
}
impl std::fmt::Display for ApiError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} ({})", self.message, self.error_type)?;
if let Some(request_id) = &self.request_id {
write!(f, " [request-id: {request_id}]")?;
}
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("bad request (400): {0}")]
BadRequest(ApiError),
#[error("authentication error (401): {0}")]
Authentication(ApiError),
#[error("permission denied (403): {0}")]
PermissionDenied(ApiError),
#[error("not found (404): {0}")]
NotFound(ApiError),
#[error("request too large (413): {0}")]
RequestTooLarge(ApiError),
#[error("rate limited (429): {err}")]
RateLimit {
err: ApiError,
retry_after: Option<Duration>,
},
#[error("overloaded (529): {0}")]
Overloaded(ApiError),
#[error("api error ({status}): {err}")]
Api {
status: u16,
err: ApiError,
},
#[error("request timed out")]
Timeout,
#[error(transparent)]
Connection(reqwest::Error),
#[error("JSON error: {0}")]
Serde(#[from] serde_json::Error),
#[error("stream error: {0}")]
Stream(String),
#[error("configuration error: {0}")]
Config(String),
}
impl From<reqwest::Error> for Error {
fn from(err: reqwest::Error) -> Self {
if err.is_timeout() {
Error::Timeout
} else {
Error::Connection(err)
}
}
}
impl Error {
pub fn is_retryable(&self) -> bool {
match self {
Error::RateLimit { .. }
| Error::Overloaded(_)
| Error::Timeout
| Error::Connection(_) => true,
Error::Api { status, .. } => *status == 408 || *status == 409 || *status >= 500,
_ => false,
}
}
pub fn retry_after(&self) -> Option<Duration> {
match self {
Error::RateLimit { retry_after, .. } => *retry_after,
_ => None,
}
}
}
pub(crate) fn from_status(
status: u16,
body: &[u8],
request_id: Option<String>,
retry_after: Option<Duration>,
) -> Error {
let api = parse_api_error(body, request_id);
status_to_error(status, api, retry_after)
}
pub(crate) fn from_error_body(error_type: String, message: String) -> Error {
let status = match error_type.as_str() {
"invalid_request_error" => 400,
"authentication_error" => 401,
"permission_error" | "billing_error" => 403,
"not_found_error" => 404,
"request_too_large" => 413,
"rate_limit_error" => 429,
"overloaded_error" => 529,
_ => 500,
};
let api = ApiError {
error_type,
message,
request_id: None,
};
status_to_error(status, api, None)
}
fn status_to_error(status: u16, api: ApiError, retry_after: Option<Duration>) -> Error {
match status {
400 => Error::BadRequest(api),
401 => Error::Authentication(api),
403 => Error::PermissionDenied(api),
404 => Error::NotFound(api),
413 => Error::RequestTooLarge(api),
429 => Error::RateLimit {
err: api,
retry_after,
},
529 => Error::Overloaded(api),
_ => Error::Api { status, err: api },
}
}
fn parse_api_error(body: &[u8], request_id: Option<String>) -> ApiError {
#[derive(Deserialize)]
struct Envelope {
error: EnvelopeBody,
#[serde(default)]
request_id: Option<String>,
}
#[derive(Deserialize)]
struct EnvelopeBody {
#[serde(rename = "type", default)]
error_type: String,
#[serde(default)]
message: String,
}
match serde_json::from_slice::<Envelope>(body) {
Ok(env) => ApiError {
error_type: if env.error.error_type.is_empty() {
"unknown_error".to_string()
} else {
env.error.error_type
},
message: if env.error.message.is_empty() {
String::from_utf8_lossy(body).trim().to_string()
} else {
env.error.message
},
request_id: request_id.or(env.request_id),
},
Err(_) => ApiError {
error_type: "unknown_error".to_string(),
message: String::from_utf8_lossy(body).trim().to_string(),
request_id,
},
}
}