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 { id: String, expected: &'static str },
23
24    /// 400.
25    #[error("bad request: {message}")]
26    BadRequest { message: String },
27
28    /// 401: missing or invalid API key.
29    #[error("authentication failed: {message}")]
30    Authentication { message: String },
31
32    /// 403.
33    #[error("permission denied: {message}")]
34    PermissionDenied { message: String },
35
36    /// 404.
37    #[error("not found: {message}")]
38    NotFound { message: String },
39
40    /// 422: the request body failed validation.
41    #[error("unprocessable entity: {message}")]
42    UnprocessableEntity { message: String },
43
44    /// 429: rate limit exceeded after all retries.
45    #[error("rate limited: {message}")]
46    RateLimit {
47        retry_after: Option<Duration>,
48        message: String,
49    },
50
51    /// 529: TypeSafe is temporarily overloaded, after all retries.
52    #[error("overloaded: {message}")]
53    Overloaded {
54        retry_after: Option<Duration>,
55        message: String,
56    },
57
58    /// Any other 5xx, after all retries.
59    #[error("server error {status}: {message}")]
60    Server { status: u16, message: String },
61
62    /// Any other unexpected status.
63    #[error("unexpected status {status}: {message}")]
64    UnexpectedStatus { status: u16, message: String },
65
66    /// The request never produced a response (DNS, connect, TLS, reset).
67    #[error("connection error: {0}")]
68    Connection(#[source] reqwest::Error),
69
70    /// The request exceeded its timeout.
71    #[error("request timed out")]
72    Timeout(#[source] reqwest::Error),
73
74    /// The response body did not match the documented schema.
75    #[error("response validation failed: {0}")]
76    ResponseValidation(#[source] serde_json::Error),
77
78    /// Failed to serialize the request body.
79    #[error("request serialization failed: {0}")]
80    RequestSerialization(#[source] serde_json::Error),
81}
82
83impl Error {
84    /// The HTTP status behind this error, when there is one.
85    pub fn status(&self) -> Option<u16> {
86        match self {
87            Self::BadRequest { .. } => Some(400),
88            Self::Authentication { .. } => Some(401),
89            Self::PermissionDenied { .. } => Some(403),
90            Self::NotFound { .. } => Some(404),
91            Self::UnprocessableEntity { .. } => Some(422),
92            Self::RateLimit { .. } => Some(429),
93            Self::Overloaded { .. } => Some(529),
94            Self::Server { status, .. } | Self::UnexpectedStatus { status, .. } => Some(*status),
95            _ => None,
96        }
97    }
98
99    pub(crate) fn from_status(status: u16, retry_after: Option<Duration>, message: String) -> Self {
100        match status {
101            400 => Self::BadRequest { message },
102            401 => Self::Authentication { message },
103            403 => Self::PermissionDenied { message },
104            404 => Self::NotFound { message },
105            422 => Self::UnprocessableEntity { message },
106            429 => Self::RateLimit { retry_after, message },
107            529 => Self::Overloaded { retry_after, message },
108            500..=599 => Self::Server { status, message },
109            _ => Self::UnexpectedStatus { status, message },
110        }
111    }
112
113    pub(crate) fn from_transport(err: reqwest::Error) -> Self {
114        if err.is_timeout() {
115            Self::Timeout(err)
116        } else {
117            Self::Connection(err)
118        }
119    }
120}