#[non_exhaustive]
pub enum FetchError {
Http(ureq::Error),
Io(std::io::Error),
Parse(serde_json::Error),
}
impl std::fmt::Display for FetchError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FetchError::Http(e) => write!(f, "HTTP error: {}", redact_key(&e.to_string())),
FetchError::Io(e) => write!(f, "IO error: {}", e),
FetchError::Parse(e) => write!(f, "Parse error: {}", e),
}
}
}
impl std::fmt::Debug for FetchError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FetchError::Http(e) => f
.debug_tuple("Http")
.field(&redact_key(&format!("{e:?}")))
.finish(),
FetchError::Io(e) => f.debug_tuple("Io").field(e).finish(),
FetchError::Parse(e) => f.debug_tuple("Parse").field(e).finish(),
}
}
}
impl std::error::Error for FetchError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
FetchError::Http(_) => None,
FetchError::Io(e) => Some(e),
FetchError::Parse(e) => Some(e),
}
}
}
fn redact_key(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut rest = s;
while let Some(pos) = rest.find("key=") {
out.push_str(&rest[..pos + 4]); rest = &rest[pos + 4..];
let end = rest
.find(|c: char| c == '&' || c.is_ascii_whitespace())
.unwrap_or(rest.len());
out.push_str("REDACTED");
rest = &rest[end..];
}
out.push_str(rest);
out
}
impl From<ureq::Error> for FetchError {
fn from(e: ureq::Error) -> Self { FetchError::Http(e) }
}
impl From<std::io::Error> for FetchError {
fn from(e: std::io::Error) -> Self { FetchError::Io(e) }
}
impl From<serde_json::Error> for FetchError {
fn from(e: serde_json::Error) -> Self { FetchError::Parse(e) }
}
#[cfg(test)]
mod tests {
use super::redact_key;
mod redact_key {
use super::redact_key;
#[test]
fn redacts_key_in_url() {
let raw = "https://googleapis.com/calendar/v3/calendars/foo/events?key=SUPER_SECRET&timeMin=now";
assert_eq!(
redact_key(raw),
"https://googleapis.com/calendar/v3/calendars/foo/events?key=REDACTED&timeMin=now"
);
}
#[test]
fn redacts_key_at_end_of_url() {
let raw = "error calling https://example.com?key=SUPER_SECRET";
assert_eq!(redact_key(raw), "error calling https://example.com?key=REDACTED");
}
#[test]
fn leaves_strings_without_key_alone() {
let raw = "some error without any api key parameter";
assert_eq!(redact_key(raw), raw);
}
#[test]
fn debug_redacts_key_in_http_error() {
let raw = "Transport { url: \"https://googleapis.com?key=SUPER_SECRET\", .. }";
let redacted = redact_key(raw);
assert!(!redacted.contains("SUPER_SECRET"));
assert!(redacted.contains("REDACTED"));
}
}
}