Skip to main content

clickhouse/
error.rs

1//! Contains [`Error`] and corresponding [`Result`].
2
3use serde::{de, ser};
4use std::{error::Error as StdError, fmt, io, result, str::Utf8Error};
5
6/// A result with a specified [`Error`] type.
7pub type Result<T, E = Error> = result::Result<T, E>;
8
9type BoxedError = Box<dyn StdError + Send + Sync>;
10
11/// Represents all possible errors.
12#[derive(Debug, thiserror::Error)]
13#[non_exhaustive]
14#[allow(missing_docs)]
15pub enum Error {
16    #[error("invalid params: {0}")]
17    InvalidParams(#[source] BoxedError),
18    #[error("network error: {0}")]
19    Network(#[source] BoxedError),
20    #[error("compression error: {0}")]
21    Compression(#[source] BoxedError),
22    #[error("decompression error: {0}")]
23    Decompression(#[source] BoxedError),
24    #[error("no rows returned by a query that expected to return at least one row")]
25    RowNotFound,
26    #[error("sequences must have a known size ahead of time")]
27    SequenceMustHaveLength,
28    #[error("`deserialize_any` is not supported")]
29    DeserializeAnyNotSupported,
30    #[error("not enough data, probably a row type mismatches a database schema")]
31    NotEnoughData,
32    #[error("string is not valid utf8")]
33    InvalidUtf8Encoding(#[from] Utf8Error),
34    #[error("tag for enum is not valid")]
35    InvalidTagEncoding(usize),
36    #[error("max number of types in the Variant data type is 255, got {0}")]
37    VariantDiscriminatorIsOutOfBound(usize),
38    #[error("a custom error message from serde: {0}")]
39    Custom(String),
40    #[error("bad response: {0}")]
41    BadResponse(String),
42    #[error("timeout expired")]
43    TimedOut,
44    #[error("error while parsing columns header from the response: {0}")]
45    InvalidColumnsHeader(#[source] BoxedError),
46    #[error("schema mismatch: {0}")]
47    SchemaMismatch(String),
48    #[error("unsupported: {0}")]
49    Unsupported(String),
50    #[cfg(feature = "sea-ql")]
51    #[error("type error: {0}")]
52    TypeError(#[from] crate::TypeError),
53    #[error("{0}")]
54    Other(BoxedError),
55}
56
57impl From<clickhouse_types::error::TypesError> for Error {
58    fn from(err: clickhouse_types::error::TypesError) -> Self {
59        Self::InvalidColumnsHeader(Box::new(err))
60    }
61}
62
63impl From<hyper::Error> for Error {
64    fn from(error: hyper::Error) -> Self {
65        Self::Network(Box::new(error))
66    }
67}
68
69impl From<hyper_util::client::legacy::Error> for Error {
70    fn from(error: hyper_util::client::legacy::Error) -> Self {
71        #[cfg(not(any(feature = "rustls-tls", feature = "native-tls")))]
72        if error.is_connect() {
73            static SCHEME_IS_NOT_HTTP: &str = "invalid URL, scheme is not http";
74
75            let src = error.source().unwrap();
76            // Unfortunately, this seems to be the only way, as `INVALID_NOT_HTTP` is not public.
77            // See https://github.com/hyperium/hyper-util/blob/v0.1.14/src/client/legacy/connect/http.rs#L491-L495
78            if src.to_string() == SCHEME_IS_NOT_HTTP {
79                return Self::Unsupported(format!(
80                    "{SCHEME_IS_NOT_HTTP}; if you are trying to connect via HTTPS, \
81                    consider enabling `native-tls` or `rustls-tls` feature"
82                ));
83            }
84        }
85        Self::Network(Box::new(error))
86    }
87}
88
89impl ser::Error for Error {
90    fn custom<T: fmt::Display>(msg: T) -> Self {
91        Self::Custom(msg.to_string())
92    }
93}
94
95impl de::Error for Error {
96    fn custom<T: fmt::Display>(msg: T) -> Self {
97        Self::Custom(msg.to_string())
98    }
99}
100
101impl From<Error> for io::Error {
102    fn from(error: Error) -> Self {
103        io::Error::other(error)
104    }
105}
106
107impl From<io::Error> for Error {
108    fn from(error: io::Error) -> Self {
109        // TODO: after MSRV 1.79 replace with `io::Error::downcast`.
110        if error.get_ref().is_some_and(|r| r.is::<Error>()) {
111            *error.into_inner().unwrap().downcast::<Error>().unwrap()
112        } else {
113            Self::Other(error.into())
114        }
115    }
116}
117
118impl Error {
119    /// https://opentelemetry.io/docs/specs/semconv/registry/attributes/error/#error-type
120    #[cfg(feature = "opentelemetry")]
121    pub(crate) fn error_type(&self) -> &str {
122        match self {
123            Error::InvalidParams(_) => "InvalidParams",
124            Error::Network(_) => "Network",
125            Error::Compression(_) => "Compression",
126            Error::Decompression(_) => "Decompression",
127            Error::RowNotFound => "RowNotFound",
128            Error::SequenceMustHaveLength => "SequenceMustHaveLength",
129            Error::DeserializeAnyNotSupported => "DeserializeAnyNotSupported",
130            Error::NotEnoughData => "NotEnoughData",
131            Error::InvalidUtf8Encoding(_) => "InvalidUtf8Encoding",
132            Error::InvalidTagEncoding(_) => "InvalidTagEncoding",
133            Error::VariantDiscriminatorIsOutOfBound(_) => "VariantDiscriminatorIsOutOfBound",
134            Error::Custom(_) => "Custom",
135            Error::BadResponse(_) => "BadResponse",
136            Error::TimedOut => "TimedOut",
137            Error::InvalidColumnsHeader(_) => "InvalidColumnsHeader",
138            Error::SchemaMismatch(_) => "SchemaMismatch",
139            Error::Unsupported(_) => "Unsupported",
140            Error::Other(_) => "Other",
141        }
142    }
143
144    /// Record this `Error` in the context of the current `tracing::Span`,
145    /// setting the OpenTelemetry conventional fields if the `opentelemetry` feature is enabled.
146    pub(crate) fn record_in_current_span(&self, msg: &str) {
147        // Span fields that remain unpopulated are not reported,
148        // so we can avoid adding noise to logs if the user isn't utilizing this feature.
149        #[cfg(feature = "opentelemetry")]
150        tracing::record_all!(
151            tracing::Span::current(),
152            otel.status_code = "Error",
153            otel.status_description = format!("{msg}: {self}"),
154            error.type = self.error_type(),
155        );
156
157        tracing::debug!(error=%self, "{msg}");
158    }
159}
160
161#[cfg(tests)]
162mod tests {
163    use crate::error::Error;
164    use std::io;
165
166    #[test]
167    fn roundtrip_io_error() {
168        let orig = Error::NotEnoughData;
169
170        // Error -> io::Error
171        let orig_str = orig.to_string();
172        let io = io::Error::from(orig);
173        assert_eq!(io.kind(), io::ErrorKind::Other);
174        assert_eq!(io.to_string(), orig_str);
175
176        // io::Error -> Error
177        let orig = Error::from(io);
178        assert!(matches!(orig, Error::NotEnoughData));
179    }
180
181    #[test]
182    fn error_traits() {
183        fn assert_traits<T: std::error::Error + Send + Sync>() {}
184
185        assert_traits::<Error>();
186    }
187}