1use std::env::VarError;
2use std::fmt::{Display, Formatter};
3use std::time::Duration;
4
5#[derive(Debug)]
6pub enum ApiError {
7 MissingCredentials {
8 provider: &'static str,
9 env_vars: &'static [&'static str],
10 },
11 ExpiredOAuthToken,
12 Auth(String),
13 InvalidApiKeyEnv(VarError),
14 Http(reqwest::Error),
15 Io(std::io::Error),
16 Json(serde_json::Error),
17 Api {
18 status: reqwest::StatusCode,
19 error_type: Option<String>,
20 message: Option<String>,
21 body: String,
22 url: Option<String>,
23 retryable: bool,
24 },
25 RetriesExhausted {
26 attempts: u32,
27 last_error: Box<ApiError>,
28 },
29 InvalidSseFrame(&'static str),
30 BackoffOverflow {
31 attempt: u32,
32 base_delay: Duration,
33 },
34 ResponsePayloadTooLarge {
35 limit: usize,
36 },
37 StreamApplicationError {
39 error_type: Option<String>,
40 message: String,
41 },
42}
43
44impl ApiError {
45 #[must_use]
46 pub const fn missing_credentials(
47 provider: &'static str,
48 env_vars: &'static [&'static str],
49 ) -> Self {
50 Self::MissingCredentials { provider, env_vars }
51 }
52
53 #[must_use]
54 pub fn is_retryable(&self) -> bool {
55 match self {
56 Self::Http(error) => error.is_connect() || error.is_timeout(),
57 Self::Api { retryable, .. } => *retryable,
58 Self::RetriesExhausted { last_error, .. } => last_error.is_retryable(),
59 Self::MissingCredentials { .. }
60 | Self::ExpiredOAuthToken
61 | Self::Auth(_)
62 | Self::InvalidApiKeyEnv(_)
63 | Self::Io(_)
64 | Self::Json(_)
65 | Self::InvalidSseFrame(_)
66 | Self::BackoffOverflow { .. }
67 | Self::ResponsePayloadTooLarge { .. } => false,
68 Self::StreamApplicationError { error_type, .. } => matches!(
69 error_type.as_deref(),
70 Some("overloaded_error" | "rate_limit_error")
71 ),
72 }
73 }
74}
75
76impl Display for ApiError {
77 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
78 match self {
79 Self::MissingCredentials { provider, env_vars } => write!(
80 f,
81 "missing {provider} credentials; export {} before calling the {provider} API",
82 env_vars.join(" or ")
83 ),
84 Self::ExpiredOAuthToken => {
85 write!(
86 f,
87 "saved OAuth token is expired and no refresh token is available"
88 )
89 }
90 Self::Auth(message) => write!(f, "auth error: {message}"),
91 Self::InvalidApiKeyEnv(error) => {
92 write!(f, "failed to read credential environment variable: {error}")
93 }
94 Self::Http(error) => write!(f, "http error: {error}"),
95 Self::Io(error) => write!(f, "io error: {error}"),
96 Self::Json(error) => write!(f, "json error: {error}"),
97 Self::Api {
98 status,
99 error_type,
100 message,
101 body,
102 url,
103 ..
104 } => {
105 match (error_type, message) {
106 (Some(error_type), Some(message)) => {
107 write!(f, "api returned {status} ({error_type}): {message}")?;
108 }
109 _ if body.is_empty() => {
110 write!(f, "api returned {status} (no response body)")?;
111 }
112 _ => {
113 write!(f, "api returned {status}: {body}")?;
114 }
115 }
116 if let Some(url) = url {
117 write!(f, "\n request url: {url}")?;
118 }
119 Ok(())
120 }
121 Self::RetriesExhausted {
122 attempts,
123 last_error,
124 } => write!(f, "api failed after {attempts} attempts: {last_error}"),
125 Self::InvalidSseFrame(message) => write!(f, "invalid sse frame: {message}"),
126 Self::BackoffOverflow {
127 attempt,
128 base_delay,
129 } => write!(
130 f,
131 "retry backoff overflowed on attempt {attempt} with base delay {base_delay:?}"
132 ),
133 Self::ResponsePayloadTooLarge { limit } => {
134 write!(f, "response payload exceeded {limit} byte limit")
135 }
136 Self::StreamApplicationError {
137 error_type,
138 message,
139 } => match error_type {
140 Some(t) => write!(f, "stream error ({t}): {message}"),
141 None => write!(f, "stream error: {message}"),
142 },
143 }
144 }
145}
146
147impl std::error::Error for ApiError {}
148
149impl From<reqwest::Error> for ApiError {
150 fn from(value: reqwest::Error) -> Self {
151 Self::Http(value)
152 }
153}
154
155impl From<std::io::Error> for ApiError {
156 fn from(value: std::io::Error) -> Self {
157 Self::Io(value)
158 }
159}
160
161impl From<serde_json::Error> for ApiError {
162 fn from(value: serde_json::Error) -> Self {
163 Self::Json(value)
164 }
165}
166
167impl From<VarError> for ApiError {
168 fn from(value: VarError) -> Self {
169 Self::InvalidApiKeyEnv(value)
170 }
171}