1use thiserror::Error;
4
5#[derive(Error, Debug)]
6pub enum GrrError {
7 #[error("Authentication error: {0}")]
8 Auth(Box<dyn std::error::Error + Send + Sync>),
9
10 #[error("HTTP request error: {0}")]
11 Http(#[from] reqwest::Error),
12
13 #[error("JSON serialization error: {0}")]
14 Json(#[from] serde_json::Error),
15
16 #[error("IO error: {0}")]
17 Io(#[from] std::io::Error),
18
19 #[error("URL parse error: {0}")]
20 Url(#[from] url::ParseError),
21
22 #[error("Configuration error: {0}")]
23 Config(String),
24
25 #[error("Rate limited: retry after {retry_after_secs}s")]
26 RateLimited { retry_after_secs: u64 },
27
28 #[error("API error {status}: {message}")]
29 Api { status: u16, message: String },
30
31 #[error("Not found: {0}")]
32 NotFound(String),
33
34 #[error("Permission denied: {0}")]
35 PermissionDenied(String),
36
37 #[error("Internal error: {0}")]
38 Internal(String),
39
40 #[error("Timeout: {0}")]
41 Timeout(String),
42
43 #[error(transparent)]
44 Anyhow(#[from] anyhow::Error),
45}
46
47impl GrrError {
48 pub fn is_retryable(&self) -> bool {
50 match self {
51 GrrError::Http(e) => e.is_timeout() || e.is_connect() || e.is_request(),
52 GrrError::RateLimited { .. } | GrrError::Timeout(_) => true,
53 GrrError::Api { status, .. } => *status >= 500 || *status == 429,
54 _ => false,
55 }
56 }
57
58 pub fn status_code(&self) -> Option<u16> {
60 match self {
61 GrrError::Api { status, .. } => Some(*status),
62 GrrError::Http(e) => e.status().map(|s| s.as_u16()),
63 GrrError::RateLimited { .. } => Some(429),
64 _ => None,
65 }
66 }
67}
68
69pub type Result<T, E = GrrError> = std::result::Result<T, E>;
70
71pub(crate) fn json_error_detail(body: &serde_json::Value) -> String {
72 let reason = body
73 .get("error")
74 .and_then(|value| {
75 value
76 .as_str()
77 .or_else(|| value.get("message").and_then(serde_json::Value::as_str))
78 })
79 .or_else(|| body.get("message").and_then(serde_json::Value::as_str))
80 .unwrap_or("unknown error");
81 let description = body
82 .get("error_description")
83 .and_then(serde_json::Value::as_str)
84 .or_else(|| body.get("description").and_then(serde_json::Value::as_str))
85 .unwrap_or("");
86 if description.is_empty() || description == reason {
87 reason.to_string()
88 } else {
89 format!("{reason}: {description}")
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use serde_json::json;
97
98 #[test]
99 fn extracts_oauth_error_details() {
100 let value = json!({"error":"invalid_grant", "error_description":"expired"});
101 assert_eq!(json_error_detail(&value), "invalid_grant: expired");
102 }
103}