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}
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}