Skip to main content

uqa_client/
http_engine_error.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7use std::fmt;
8use std::io;
9
10use reqwest::StatusCode;
11use thiserror::Error;
12
13/// Redacted failure returned by [`crate::HttpEngine`].
14#[derive(Error)]
15pub enum HttpEngineError {
16    #[error("UQA data-plane URL is invalid")]
17    InvalidBaseURL,
18    #[error("plain HTTP UQA URLs must resolve to loopback")]
19    InsecureRemoteURL,
20    #[error("UQA project token must not be empty")]
21    InvalidCredential,
22    #[error("required UQA connection environment variable {0} is missing")]
23    MissingEnvironmentVariable(&'static str),
24    #[error("UQA project name must not be empty")]
25    EmptyProjectName,
26    #[error("UQA organization name must not be empty")]
27    EmptyOrganizationName,
28    #[error("uqa CLI could not be started; install it or provide its executable path")]
29    CLIUnavailable(#[source] io::Error),
30    #[error("uqa CLI connection lookup failed")]
31    CLIExecution(#[source] io::Error),
32    #[error("uqa CLI connection lookup timed out")]
33    CLITimedOut,
34    #[error("uqa CLI connection output exceeded the client safety limit")]
35    CLIOutputTooLarge,
36    #[error("uqa CLI could not resolve the requested project; run the matching connection command for details")]
37    CLIConnectionFailed,
38    #[error("uqa CLI returned an invalid connection response")]
39    InvalidCLIResponse(#[source] serde_json::Error),
40    #[error("SQL text must not be empty")]
41    EmptySQL,
42    #[error("SQL parameter cannot be represented by the HTTP protocol")]
43    InvalidParameter,
44    #[error("UQA HTTP client could not be initialized")]
45    BuildClient(#[source] reqwest::Error),
46    #[error("UQA HTTP transport failed")]
47    Transport(#[source] reqwest::Error),
48    #[error("UQA returned {status} with code {code}")]
49    Server {
50        status: StatusCode,
51        code: String,
52        message: String,
53        request_id: Option<String>,
54    },
55    #[error("UQA response exceeded the client safety limit")]
56    ResponseTooLarge,
57    #[error("UQA response content type is invalid")]
58    UnexpectedContentType,
59    #[error("UQA response is missing its request ID")]
60    MissingRequestId,
61    #[error("UQA response request IDs do not match")]
62    ResponseRequestIdMismatch,
63    #[error("UQA response body is not valid JSON")]
64    InvalidResponse(#[source] serde_json::Error),
65    #[error("UQA NDJSON stream frame exceeded the client safety limit")]
66    StreamFrameTooLarge,
67    #[error("UQA NDJSON stream frame order is invalid")]
68    InvalidStreamSequence,
69    #[error("UQA NDJSON stream ended before a terminal frame")]
70    TruncatedStream,
71    #[error("UQA NDJSON stream request ID does not match its HTTP response")]
72    StreamRequestIdMismatch,
73}
74
75impl HttpEngineError {
76    pub(crate) fn build_client(error: reqwest::Error) -> Self {
77        Self::BuildClient(error.without_url())
78    }
79
80    pub(crate) fn transport(error: reqwest::Error) -> Self {
81        Self::Transport(error.without_url())
82    }
83}
84
85impl fmt::Debug for HttpEngineError {
86    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
87        match self {
88            Self::Server {
89                status,
90                code,
91                request_id,
92                ..
93            } => formatter
94                .debug_struct("HttpEngineError::Server")
95                .field("status", status)
96                .field("code", code)
97                .field("request_id", request_id)
98                .finish(),
99            Self::InvalidBaseURL => formatter.write_str("HttpEngineError::InvalidBaseURL"),
100            Self::InsecureRemoteURL => formatter.write_str("HttpEngineError::InsecureRemoteURL"),
101            Self::InvalidCredential => formatter.write_str("HttpEngineError::InvalidCredential"),
102            Self::MissingEnvironmentVariable(name) => formatter
103                .debug_tuple("HttpEngineError::MissingEnvironmentVariable")
104                .field(name)
105                .finish(),
106            Self::EmptyProjectName => formatter.write_str("HttpEngineError::EmptyProjectName"),
107            Self::EmptyOrganizationName => {
108                formatter.write_str("HttpEngineError::EmptyOrganizationName")
109            }
110            Self::CLIUnavailable(_) => {
111                formatter.write_str("HttpEngineError::CLIUnavailable([REDACTED])")
112            }
113            Self::CLIExecution(_) => {
114                formatter.write_str("HttpEngineError::CLIExecution([REDACTED])")
115            }
116            Self::CLITimedOut => formatter.write_str("HttpEngineError::CLITimedOut"),
117            Self::CLIOutputTooLarge => formatter.write_str("HttpEngineError::CLIOutputTooLarge"),
118            Self::CLIConnectionFailed => {
119                formatter.write_str("HttpEngineError::CLIConnectionFailed")
120            }
121            Self::InvalidCLIResponse(_) => {
122                formatter.write_str("HttpEngineError::InvalidCLIResponse([REDACTED])")
123            }
124            Self::EmptySQL => formatter.write_str("HttpEngineError::EmptySQL"),
125            Self::InvalidParameter => formatter.write_str("HttpEngineError::InvalidParameter"),
126            Self::BuildClient(_) => formatter.write_str("HttpEngineError::BuildClient([REDACTED])"),
127            Self::Transport(_) => formatter.write_str("HttpEngineError::Transport([REDACTED])"),
128            Self::ResponseTooLarge => formatter.write_str("HttpEngineError::ResponseTooLarge"),
129            Self::UnexpectedContentType => {
130                formatter.write_str("HttpEngineError::UnexpectedContentType")
131            }
132            Self::MissingRequestId => formatter.write_str("HttpEngineError::MissingRequestId"),
133            Self::ResponseRequestIdMismatch => {
134                formatter.write_str("HttpEngineError::ResponseRequestIdMismatch")
135            }
136            Self::InvalidResponse(_) => {
137                formatter.write_str("HttpEngineError::InvalidResponse([REDACTED])")
138            }
139            Self::StreamFrameTooLarge => {
140                formatter.write_str("HttpEngineError::StreamFrameTooLarge")
141            }
142            Self::InvalidStreamSequence => {
143                formatter.write_str("HttpEngineError::InvalidStreamSequence")
144            }
145            Self::TruncatedStream => formatter.write_str("HttpEngineError::TruncatedStream"),
146            Self::StreamRequestIdMismatch => {
147                formatter.write_str("HttpEngineError::StreamRequestIdMismatch")
148            }
149        }
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn server_debug_output_omits_customer_message() {
159        let secret = "customer SQL and result";
160        let error = HttpEngineError::Server {
161            status: StatusCode::BAD_REQUEST,
162            code: "SQL_EXECUTION_FAILED".to_owned(),
163            message: secret.to_owned(),
164            request_id: Some("qry_test".to_owned()),
165        };
166
167        let debug = format!("{error:?}");
168        assert!(!debug.contains(secret));
169        assert!(debug.contains("SQL_EXECUTION_FAILED"));
170    }
171}