Skip to main content

codineer_api/
error.rs

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    /// In-stream error object from the Anthropic Messages SSE protocol (`type: "error"`).
38    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}