1#[non_exhaustive]
3pub enum FetchError {
4 Http(ureq::Error),
5 Io(std::io::Error),
6 Parse(serde_json::Error),
7}
8
9impl std::fmt::Display for FetchError {
10 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
11 match self {
12 FetchError::Http(e) => write!(f, "HTTP error: {}", redact_key(&e.to_string())),
13 FetchError::Io(e) => write!(f, "IO error: {}", e),
14 FetchError::Parse(e) => write!(f, "Parse error: {}", e),
15 }
16 }
17}
18
19impl std::fmt::Debug for FetchError {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 match self {
22 FetchError::Http(e) => f
24 .debug_tuple("Http")
25 .field(&redact_key(&format!("{e:?}")))
26 .finish(),
27 FetchError::Io(e) => f.debug_tuple("Io").field(e).finish(),
28 FetchError::Parse(e) => f.debug_tuple("Parse").field(e).finish(),
29 }
30 }
31}
32
33impl std::error::Error for FetchError {
34 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
35 match self {
36 FetchError::Http(_) => None,
38 FetchError::Io(e) => Some(e),
39 FetchError::Parse(e) => Some(e),
40 }
41 }
42}
43
44fn redact_key(s: &str) -> String {
47 let mut out = String::with_capacity(s.len());
48 let mut rest = s;
49 while let Some(pos) = rest.find("key=") {
50 out.push_str(&rest[..pos + 4]); rest = &rest[pos + 4..];
52 let end = rest
54 .find(|c: char| c == '&' || c.is_ascii_whitespace())
55 .unwrap_or(rest.len());
56 out.push_str("REDACTED");
57 rest = &rest[end..];
58 }
59 out.push_str(rest);
60 out
61}
62
63impl From<ureq::Error> for FetchError {
64 fn from(e: ureq::Error) -> Self { FetchError::Http(e) }
65}
66
67impl From<std::io::Error> for FetchError {
68 fn from(e: std::io::Error) -> Self { FetchError::Io(e) }
69}
70
71impl From<serde_json::Error> for FetchError {
72 fn from(e: serde_json::Error) -> Self { FetchError::Parse(e) }
73}
74
75#[cfg(test)]
76mod tests {
77 use super::redact_key;
78 mod redact_key {
79 use super::redact_key;
80
81 #[test]
82 fn redacts_key_in_url() {
83 let raw = "https://googleapis.com/calendar/v3/calendars/foo/events?key=SUPER_SECRET&timeMin=now";
84 assert_eq!(
85 redact_key(raw),
86 "https://googleapis.com/calendar/v3/calendars/foo/events?key=REDACTED&timeMin=now"
87 );
88 }
89
90 #[test]
91 fn redacts_key_at_end_of_url() {
92 let raw = "error calling https://example.com?key=SUPER_SECRET";
93 assert_eq!(redact_key(raw), "error calling https://example.com?key=REDACTED");
94 }
95
96 #[test]
97 fn leaves_strings_without_key_alone() {
98 let raw = "some error without any api key parameter";
99 assert_eq!(redact_key(raw), raw);
100 }
101
102 #[test]
103 fn debug_redacts_key_in_http_error() {
104 let raw = "Transport { url: \"https://googleapis.com?key=SUPER_SECRET\", .. }";
111 let redacted = redact_key(raw);
112 assert!(!redacted.contains("SUPER_SECRET"));
113 assert!(redacted.contains("REDACTED"));
114 }
115 }
116}