Skip to main content

autumn_plugin_clickhouse/
error.rs

1//! The public error type.
2//!
3//! # Contract
4//!
5//! - A ClickHouse error keeps its kind and its full message.
6//! - The error text shows the kind only. [`ClickHouseError::detail`] gives the full message.
7//!   A server message can hold user data.
8//! - [`ErrorKind`] is the plugin's own copy of the ClickHouse error kinds.
9//!   A `clickhouse` update does not change it.
10//! - A timeout gives HTTP 504. A network error gives 503. A bad server answer gives 502.
11//!   A missing row gives 404. Bad parameters give 400. All other errors give 500.
12//! - The `Debug` output is the error text. It does not show the full message.
13//! - A timeout and a network error are retryable.
14
15use std::time::Duration;
16
17use autumn_web::AutumnError;
18use http::StatusCode;
19
20use crate::config::ConfigError;
21
22/// An error from the plugin.
23///
24/// The `Debug` output is the error text. It does not show `detail`.
25#[derive(Clone, PartialEq, Eq, thiserror::Error)]
26#[non_exhaustive]
27pub enum ClickHouseError {
28    /// The configuration is not valid.
29    #[error(transparent)]
30    Config(#[from] ConfigError),
31    /// ClickHouse refused the call.
32    ///
33    /// The text does not show `detail`, because it can hold user data.
34    #[error("ClickHouse refused the call: {kind}")]
35    #[non_exhaustive]
36    Database {
37        /// The ClickHouse error kind.
38        kind: ErrorKind,
39        /// The full ClickHouse message.
40        detail: String,
41    },
42    /// The call did not complete in time.
43    #[error("the call did not complete in {timeout:?}")]
44    #[non_exhaustive]
45    Timeout {
46        /// The timeout.
47        timeout: Duration,
48    },
49    /// A migration failed.
50    ///
51    /// The text does not show `detail`, because it can hold a server message.
52    #[error("the migration `{name}` failed")]
53    #[non_exhaustive]
54    Migration {
55        /// The migration name.
56        name: String,
57        /// The full server message.
58        detail: String,
59    },
60    /// The app does not have the plugin.
61    #[error("the ClickHouse plugin is not installed: add `ClickHousePlugin` to the app")]
62    NotInstalled,
63}
64
65/// The kind of a ClickHouse error.
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
67#[non_exhaustive]
68pub enum ErrorKind {
69    /// A bind value or a query parameter is not valid.
70    InvalidParams,
71    /// The HTTP layer failed. The server is down or unreachable.
72    Network,
73    /// The client time limit ended the operation.
74    TimedOut,
75    /// The server gave a bad answer: an HTTP error or a ClickHouse exception.
76    BadResponse,
77    /// A query found no row.
78    NotFound,
79    /// The row type does not match the table schema.
80    SchemaMismatch,
81    /// The client could not encode or decode the data.
82    Decode,
83    /// The build does not support the operation.
84    Unsupported,
85    /// The client refused the call for its own reason.
86    Custom,
87    /// ClickHouse gave a kind that the plugin does not know.
88    Unknown,
89}
90
91impl std::fmt::Display for ErrorKind {
92    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93        f.write_str(match self {
94            Self::InvalidParams => "invalid parameters",
95            Self::Network => "network error",
96            Self::TimedOut => "timed out",
97            Self::BadResponse => "bad response",
98            Self::NotFound => "row not found",
99            Self::SchemaMismatch => "schema mismatch",
100            Self::Decode => "decode error",
101            Self::Unsupported => "unsupported",
102            Self::Custom => "client error",
103            Self::Unknown => "unknown error",
104        })
105    }
106}
107
108impl std::fmt::Debug for ClickHouseError {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        // The derived output shows `detail`. It can hold user data.
111        f.debug_tuple("ClickHouseError")
112            .field(&self.to_string())
113            .finish()
114    }
115}
116
117impl From<clickhouse::error::Error> for ClickHouseError {
118    fn from(err: clickhouse::error::Error) -> Self {
119        use clickhouse::error::Error as Ch;
120        let kind = match &err {
121            Ch::InvalidParams(_) => ErrorKind::InvalidParams,
122            Ch::Network(_) => ErrorKind::Network,
123            Ch::TimedOut => ErrorKind::TimedOut,
124            Ch::BadResponse(_) => ErrorKind::BadResponse,
125            Ch::RowNotFound => ErrorKind::NotFound,
126            Ch::SchemaMismatch(_) => ErrorKind::SchemaMismatch,
127            Ch::Compression(_)
128            | Ch::Decompression(_)
129            | Ch::InvalidUtf8Encoding(_)
130            | Ch::InvalidTagEncoding(_)
131            | Ch::VariantDiscriminatorIsOutOfBound(_)
132            | Ch::NotEnoughData
133            | Ch::SequenceMustHaveLength
134            | Ch::DeserializeAnyNotSupported
135            | Ch::InvalidColumnsHeader(_) => ErrorKind::Decode,
136            Ch::Unsupported(_) => ErrorKind::Unsupported,
137            Ch::Custom(_) => ErrorKind::Custom,
138            _ => ErrorKind::Unknown,
139        };
140        Self::Database {
141            kind,
142            detail: err.to_string(),
143        }
144    }
145}
146
147impl ClickHouseError {
148    /// The full message of a [`ClickHouseError::Database`] or [`ClickHouseError::Migration`] error.
149    ///
150    /// The message can hold user data. Do not show it to users.
151    #[must_use]
152    pub fn detail(&self) -> Option<&str> {
153        match self {
154            Self::Database { detail, .. } | Self::Migration { detail, .. } => Some(detail),
155            _ => None,
156        }
157    }
158
159    /// The ClickHouse error kind of a [`ClickHouseError::Database`] error.
160    #[must_use]
161    pub const fn kind(&self) -> Option<ErrorKind> {
162        match self {
163            Self::Database { kind, .. } => Some(*kind),
164            _ => None,
165        }
166    }
167
168    /// Returns `true` if a retry of the same call can succeed.
169    #[must_use]
170    pub const fn is_retryable(&self) -> bool {
171        matches!(
172            self,
173            Self::Timeout { .. }
174                | Self::Database {
175                    kind: ErrorKind::Network | ErrorKind::TimedOut,
176                    ..
177                }
178        )
179    }
180
181    /// The HTTP status for this error.
182    #[must_use]
183    pub const fn status(&self) -> StatusCode {
184        match self {
185            Self::Timeout { .. }
186            | Self::Database {
187                kind: ErrorKind::TimedOut,
188                ..
189            } => StatusCode::GATEWAY_TIMEOUT,
190            Self::Database {
191                kind: ErrorKind::Network,
192                ..
193            } => StatusCode::SERVICE_UNAVAILABLE,
194            Self::Database {
195                kind: ErrorKind::BadResponse,
196                ..
197            } => StatusCode::BAD_GATEWAY,
198            Self::Database {
199                kind: ErrorKind::NotFound,
200                ..
201            } => StatusCode::NOT_FOUND,
202            Self::Database {
203                kind: ErrorKind::InvalidParams,
204                ..
205            } => StatusCode::BAD_REQUEST,
206            _ => StatusCode::INTERNAL_SERVER_ERROR,
207        }
208    }
209
210    /// Converts to an [`AutumnError`] with [`status`](Self::status).
211    ///
212    /// `AutumnError` has no bad-gateway constructor, so a 502 status becomes a 500.
213    /// The `?` operator also converts, but always gives status 500.
214    /// Autumn shows server error details only in development.
215    #[must_use]
216    pub fn into_autumn(self) -> AutumnError {
217        match self.status() {
218            StatusCode::NOT_FOUND => AutumnError::not_found(self),
219            StatusCode::BAD_REQUEST => AutumnError::bad_request(self),
220            StatusCode::SERVICE_UNAVAILABLE => AutumnError::service_unavailable(self),
221            StatusCode::GATEWAY_TIMEOUT => AutumnError::query_timeout(self.to_string()),
222            _ => AutumnError::internal_server_error(self),
223        }
224    }
225}
226
227/// Adds [`or_http`](ClickHouseResultExt::or_http) to `Result<T, ClickHouseError>`.
228pub trait ClickHouseResultExt<T> {
229    /// Converts the error with [`ClickHouseError::into_autumn`].
230    ///
231    /// # Errors
232    ///
233    /// Returns the converted error.
234    fn or_http(self) -> Result<T, AutumnError>;
235}
236
237impl<T> ClickHouseResultExt<T> for Result<T, ClickHouseError> {
238    fn or_http(self) -> Result<T, AutumnError> {
239        self.map_err(ClickHouseError::into_autumn)
240    }
241}
242
243#[cfg(test)]
244mod tests;