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