use thiserror::Error;
#[derive(Debug, Error)]
pub enum ClientError {
#[error("authentication failed: {message}")]
Auth { message: String },
#[error("{}", format_request_error(.0))]
Request(#[from] reqwest::Error),
#[error("API error ({status}): {message}")]
Api { status: u16, message: String },
#[error("access denied: {message}\n\nHint: {hint}")]
Forbidden { message: String, hint: String },
#[error("not found: {message}")]
NotFound { message: String },
#[error("Azure CLI error: {message}\n\nHint: {hint}")]
AzCli { message: String, hint: String },
#[error("Azure OpenAI error: {message}")]
OpenAI { message: String },
#[error("local AI agent error: {message}")]
LocalAgent { message: String },
#[error("{0}")]
Other(String),
}
impl ClientError {
pub fn auth(msg: impl Into<String>) -> Self {
Self::Auth {
message: msg.into(),
}
}
pub fn az_cli(msg: impl Into<String>, hint: impl Into<String>) -> Self {
Self::AzCli {
message: msg.into(),
hint: hint.into(),
}
}
pub fn forbidden(msg: impl Into<String>, hint: impl Into<String>) -> Self {
Self::Forbidden {
message: extract_message(msg.into()),
hint: hint.into(),
}
}
pub fn not_found(msg: impl Into<String>) -> Self {
Self::NotFound {
message: msg.into(),
}
}
pub fn openai(msg: impl Into<String>) -> Self {
Self::OpenAI {
message: msg.into(),
}
}
pub fn local_agent(msg: impl Into<String>) -> Self {
Self::LocalAgent {
message: msg.into(),
}
}
pub fn api(status: u16, body: impl Into<String>) -> Self {
Self::Api {
status,
message: extract_message(body.into()),
}
}
}
fn format_request_error(err: &reqwest::Error) -> String {
if has_certificate_error(err) {
return "TLS certificate verification failed\n\n\
The remote server's certificate was not trusted. This typically happens on\n\
corporate networks that use TLS inspection with a custom CA certificate.\n\n\
Fix: Install the corporate root CA certificate into your operating system's\n\
certificate store:\n\
\x20 macOS: Add to Keychain Access > System > Certificates\n\
\x20 Linux: Copy to /usr/local/share/ca-certificates/ and run update-ca-certificates\n\
\x20 Windows: Import via certmgr.msc > Trusted Root Certification Authorities"
.to_string();
}
format!("HTTP request failed: {err}")
}
fn has_certificate_error(err: &reqwest::Error) -> bool {
use std::error::Error;
let mut source = err.source();
while let Some(cause) = source {
let msg = cause.to_string();
if msg.contains("certificate") || msg.contains("UnknownIssuer") {
return true;
}
source = cause.source();
}
false
}
fn extract_message(body: String) -> String {
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&body) {
if let Some(msg) = json["message"].as_str().or(json["Message"].as_str()) {
let clean = msg.split("\r\nActivityId:").next().unwrap_or(msg).trim();
return clean.to_string();
}
}
body
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_message_cosmos_json() {
let body = r#"{"code":"Forbidden","message":"Request blocked by Auth mklabcosdb : Request is blocked because principal [abc-123] does not have required RBAC permissions to perform action [Microsoft.DocumentDB/databaseAccounts/readMetadata] on any scope. Learn more: https://aka.ms/cosmos-native-rbac.\r\nActivityId: c93b2c4e-faf8-4a23-848e-1f03c0e0d8a7, Microsoft.Azure.Documents.Common/2.14.0"}"#;
let msg = extract_message(body.to_string());
assert!(msg.starts_with("Request blocked by Auth"));
assert!(msg.contains("readMetadata"));
assert!(!msg.contains("ActivityId:"));
}
#[test]
fn test_extract_message_plain_text() {
let body = "something went wrong";
let msg = extract_message(body.to_string());
assert_eq!(msg, "something went wrong");
}
#[test]
fn test_extract_message_json_without_message_field() {
let body = r#"{"error": "oops"}"#;
let msg = extract_message(body.to_string());
assert_eq!(msg, body);
}
#[test]
fn test_has_certificate_error_detection() {
let check =
|msg: &str| -> bool { msg.contains("certificate") || msg.contains("UnknownIssuer") };
assert!(check("invalid peer certificate: UnknownIssuer"));
assert!(check("certificate verify failed"));
assert!(check("self signed certificate in certificate chain"));
assert!(!check("connection refused"));
assert!(!check("timeout"));
}
#[test]
fn test_format_request_error_cert_message() {
let msg = format!(
"TLS certificate verification failed\n\n\
The remote server's certificate was not trusted. This typically happens on\n\
corporate networks that use TLS inspection with a custom CA certificate."
);
assert!(msg.contains("TLS certificate verification failed"));
assert!(msg.contains("corporate networks"));
}
#[test]
fn test_extract_message_capital_message() {
let body = r#"{"Message": "Something failed"}"#;
let msg = extract_message(body.to_string());
assert_eq!(msg, "Something failed");
}
}