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