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