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