Skip to main content

trino_rust_client/
error.rs

1use reqwest::header::HeaderName;
2use reqwest::StatusCode;
3use serde::Deserialize;
4use thiserror::Error;
5
6use crate::models::QueryError;
7
8#[derive(Error, Debug)]
9#[non_exhaustive]
10pub enum Error {
11    #[error("duplicate header")]
12    DuplicateHeader(HeaderName),
13    #[error("invalid empty auth")]
14    EmptyAuth,
15    #[error("forbidden: {message}")]
16    Forbidden { message: String },
17    #[error("basic auth can not be used with http")]
18    BasicAuthWithHttp,
19    #[error("http error, reason: {0}")]
20    HttpError(#[source] Box<reqwest::Error>),
21    #[error("http not ok, code: {0}, reason: {1}")]
22    HttpNotOk(StatusCode, String),
23    /// A query failed on the Trino coordinator. Match on the inner
24    /// [`QueryError`]'s `error_code` / `error_name` / `error_type` to react to
25    /// a specific failure; the full structured error is also reachable through
26    /// [`std::error::Error::source`].
27    #[error("query error [{}]: {}", .0.error_name, .0.message)]
28    Query(#[source] Box<QueryError>),
29    /// Failed to decode or deserialize a response or a spooled segment.
30    #[error("decode error: {0}")]
31    Decode(String),
32    /// Failed to load or read a TLS certificate.
33    #[error("tls error: {0}")]
34    Tls(String),
35    /// The server used a protocol the client cannot handle in this context
36    /// (e.g. mixing the Direct and Spooled protocols across pages, or spooled
37    /// data received without the `spooling` feature enabled).
38    #[error("protocol error: {0}")]
39    Protocol(String),
40    /// A transaction operation was attempted in a state that does not allow
41    /// it — starting a transaction while one is already active, or committing
42    /// or rolling back without one.
43    #[error("transaction error: {0}")]
44    Transaction(String),
45    /// The interactive OAuth2 authentication flow failed (no token server in the
46    /// challenge, the token endpoint returned an error, or it timed out).
47    #[error("oauth2 error: {0}")]
48    OAuth2(String),
49    #[error("inconsistent data")]
50    InconsistentData,
51    #[error("reach max attempt: {0}")]
52    ReachMaxAttempt(usize),
53    #[error("invalid host: {0}")]
54    InvalidHost(String),
55    /// An unexpected, internal failure that callers are not expected to handle.
56    #[error("internal error: {0}")]
57    InternalError(String),
58}
59
60impl From<reqwest::Error> for Error {
61    fn from(err: reqwest::Error) -> Self {
62        Error::HttpError(Box::new(err))
63    }
64}
65
66impl From<QueryError> for Error {
67    fn from(err: QueryError) -> Self {
68        // error_code 4 is Trino's PERMISSION_DENIED.
69        if err.error_code == 4 {
70            Error::Forbidden {
71                message: err.message,
72            }
73        } else {
74            Error::Query(Box::new(err))
75        }
76    }
77}
78
79pub type Result<T> = std::result::Result<T, Error>;
80
81#[derive(Debug, Deserialize)]
82pub struct TrinoRetryResult {
83    pub id: String,
84    #[serde(rename = "infoUri")]
85    pub info_uri: String,
86    pub stats: TrinoStats,
87    pub error: Option<QueryError>,
88    #[serde(rename = "updateType")]
89    pub update_type: Option<String>,
90    #[serde(rename = "updateCount")]
91    pub update_count: Option<u64>,
92}
93
94#[derive(Debug, Deserialize)]
95pub struct TrinoStats {
96    pub state: String,
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use crate::models::QueryError;
103
104    fn query_error(error_code: i32, error_name: &str) -> QueryError {
105        QueryError {
106            message: "boom".into(),
107            sql_state: None,
108            error_code,
109            error_name: error_name.into(),
110            error_type: "USER_ERROR".into(),
111            error_location: None,
112            failure_info: None,
113        }
114    }
115
116    // Both the query and execute paths funnel Trino failures through
117    // `From<QueryError>`, so these two tests pin the single, shared mapping.
118
119    #[test]
120    fn permission_denied_maps_to_forbidden() {
121        // error_code 4 is Trino's PERMISSION_DENIED.
122        match Error::from(query_error(4, "PERMISSION_DENIED")) {
123            Error::Forbidden { message } => assert_eq!(message, "boom"),
124            other => panic!("expected Forbidden, got {other:?}"),
125        }
126    }
127
128    #[test]
129    fn other_failures_map_to_structured_query() {
130        match Error::from(query_error(1, "SYNTAX_ERROR")) {
131            Error::Query(q) => {
132                assert_eq!(q.error_name, "SYNTAX_ERROR");
133                assert_eq!(q.error_code, 1);
134                assert_eq!(q.error_type, "USER_ERROR");
135            }
136            other => panic!("expected Query, got {other:?}"),
137        }
138    }
139
140    #[test]
141    fn query_error_preserves_source_chain() {
142        use std::error::Error as _;
143
144        let err = Error::from(query_error(1, "SYNTAX_ERROR"));
145        // Top-level Display stays concise (no failure_info dump)...
146        assert_eq!(err.to_string(), "query error [SYNTAX_ERROR]: boom");
147        // ...while the underlying error is reachable via the source chain, so
148        // generic tooling (anyhow / eyre / tracing) can surface the cause.
149        let source = err.source().expect("Query should expose a source");
150        assert!(source.to_string().contains("boom"));
151    }
152}