Skip to main content

typesafe_systemone/
error.rs

1use std::time::Duration;
2
3/// Result alias for this crate.
4pub type Result<T> = std::result::Result<T, Error>;
5
6/// Errors returned by [`Client`](crate::Client).
7///
8/// HTTP-status variants carry the response body (truncated) as `message`.
9#[derive(Debug, thiserror::Error)]
10#[non_exhaustive]
11pub enum Error {
12    /// Missing API key or otherwise unusable client configuration.
13    #[error("configuration error: {0}")]
14    Config(String),
15
16    /// The request was not sendable as built (no state, no questions, bad option set).
17    #[error("invalid request: {0}")]
18    InvalidRequest(String),
19
20    /// The response has no answer of the expected type under this id.
21    #[error("no {expected} answer for question `{id}`")]
22    MissingAnswer {
23        /// The question id that was looked up.
24        id: String,
25        /// The answer type that was expected: `noul`, `choice` or `score`.
26        expected: &'static str,
27    },
28
29    /// 400.
30    #[error("bad request: {message}")]
31    BadRequest {
32        /// Response body, truncated.
33        message: String,
34    },
35
36    /// 401: missing or invalid API key.
37    #[error("authentication failed: {message}")]
38    Authentication {
39        /// Response body, truncated.
40        message: String,
41    },
42
43    /// 403.
44    #[error("permission denied: {message}")]
45    PermissionDenied {
46        /// Response body, truncated.
47        message: String,
48    },
49
50    /// 404.
51    #[error("not found: {message}")]
52    NotFound {
53        /// Response body, truncated.
54        message: String,
55    },
56
57    /// 422: the request body failed validation.
58    #[error("unprocessable entity: {message}")]
59    UnprocessableEntity {
60        /// Response body, truncated; names the offending field.
61        message: String,
62    },
63
64    /// 429: rate limit exceeded after all retries.
65    #[error("rate limited: {message}")]
66    RateLimit {
67        /// The server's `retry-after`, when it sent one.
68        retry_after: Option<Duration>,
69        /// Response body, truncated.
70        message: String,
71    },
72
73    /// 529: TypeSafe is temporarily overloaded, after all retries.
74    #[error("overloaded: {message}")]
75    Overloaded {
76        /// The server's `retry-after`, when it sent one.
77        retry_after: Option<Duration>,
78        /// Response body, truncated.
79        message: String,
80    },
81
82    /// Any other 5xx, after all retries.
83    #[error("server error {status}: {message}")]
84    Server {
85        /// HTTP status code.
86        status: u16,
87        /// Response body, truncated.
88        message: String,
89    },
90
91    /// Any other unexpected status.
92    #[error("unexpected status {status}: {message}")]
93    UnexpectedStatus {
94        /// HTTP status code.
95        status: u16,
96        /// Response body, truncated.
97        message: String,
98    },
99
100    /// The request never produced a response (DNS, connect, TLS, reset).
101    #[error("connection error: {0}")]
102    Connection(#[source] reqwest::Error),
103
104    /// The request exceeded its timeout.
105    #[error("request timed out")]
106    Timeout(#[source] reqwest::Error),
107
108    /// The response body did not match the documented schema.
109    #[error("response validation failed: {0}")]
110    ResponseValidation(#[source] serde_json::Error),
111
112    /// Failed to serialize the request body.
113    #[error("request serialization failed: {0}")]
114    RequestSerialization(#[source] serde_json::Error),
115}
116
117impl Error {
118    /// The HTTP status behind this error, when there is one.
119    #[must_use]
120    pub const fn status(&self) -> Option<u16> {
121        match self {
122            Self::BadRequest { .. } => Some(400),
123            Self::Authentication { .. } => Some(401),
124            Self::PermissionDenied { .. } => Some(403),
125            Self::NotFound { .. } => Some(404),
126            Self::UnprocessableEntity { .. } => Some(422),
127            Self::RateLimit { .. } => Some(429),
128            Self::Overloaded { .. } => Some(529),
129            Self::Server { status, .. } | Self::UnexpectedStatus { status, .. } => Some(*status),
130            _ => None,
131        }
132    }
133
134    pub(crate) fn from_status(status: u16, retry_after: Option<Duration>, message: String) -> Self {
135        match status {
136            400 => Self::BadRequest { message },
137            401 => Self::Authentication { message },
138            403 => Self::PermissionDenied { message },
139            404 => Self::NotFound { message },
140            422 => Self::UnprocessableEntity { message },
141            429 => Self::RateLimit { retry_after, message },
142            529 => Self::Overloaded { retry_after, message },
143            500..=599 => Self::Server { status, message },
144            _ => Self::UnexpectedStatus { status, message },
145        }
146    }
147
148    pub(crate) fn from_transport(err: reqwest::Error) -> Self {
149        if err.is_timeout() {
150            Self::Timeout(err)
151        } else {
152            Self::Connection(err)
153        }
154    }
155}