Skip to main content

gcal_fetcher/
error.rs

1/// Enum for different potential errors.
2#[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            // Redact the API key that ureq embeds in the URL inside its Debug output.
23            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            // Do not expose the raw ureq error – it contains the URL with the API key.
37            FetchError::Http(_)  => None,
38            FetchError::Io(e)    => Some(e),
39            FetchError::Parse(e) => Some(e),
40        }
41    }
42}
43
44/// Replace the value of the `key=` query parameter with `REDACTED` so that
45/// API keys are not accidentally leaked in log output or error messages.
46fn 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]); // include "key="
51        rest = &rest[pos + 4..];
52        // skip until the next '&', whitespace, or end-of-string
53        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            // Construct a FetchError::Http from a hand-built ureq::Error and confirm
105            // that formatting it with {:?} does not expose the raw API key.
106            // We test via the Display path which exercises redact_key directly,
107            // since we cannot easily construct a ureq::Error containing a live URL.
108            // Instead verify that redact_key is applied by round-tripping a string
109            // that matches what ureq would embed.
110            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}