Skip to main content

photon_backend/
sanitize.rs

1//! Sanitize operator-visible error strings (DLQ, ops-log, admin surfaces).
2
3/// Maximum length of persisted / logged handler error messages.
4pub const MAX_ERROR_MESSAGE_CHARS: usize = 512;
5
6/// Truncate and strip obvious secret-looking substrings from an error message.
7///
8/// Does not attempt full redaction; callers must still avoid embedding payload/actor JSON
9/// in errors. Used before recording DLQ rows, ops-log fields, and admin-facing error text.
10#[must_use]
11pub fn sanitize_error_message(raw: &str) -> String {
12    let mut out = raw.replace('\0', "");
13    for needle in [
14        "password=",
15        "Password=",
16        "secret=",
17        "Secret=",
18        "token=",
19        "Token=",
20        "Bearer ",
21        "authorization:",
22    ] {
23        if let Some(idx) = out.find(needle) {
24            let end = (idx + needle.len() + 8).min(out.len());
25            out.replace_range(idx..end, &format!("{needle}[redacted]"));
26        }
27    }
28    if out.chars().count() > MAX_ERROR_MESSAGE_CHARS {
29        out = out.chars().take(MAX_ERROR_MESSAGE_CHARS).collect();
30        out.push('…');
31    }
32    out
33}
34
35#[cfg(test)]
36mod tests {
37    use super::*;
38
39    #[test]
40    fn truncates_long_messages() {
41        let long = "x".repeat(800);
42        let s = sanitize_error_message(&long);
43        assert!(s.chars().count() <= MAX_ERROR_MESSAGE_CHARS + 1);
44        assert!(s.ends_with('…'));
45    }
46
47    #[test]
48    fn redacts_password_prefix() {
49        let s = sanitize_error_message("db failed password=hunter2 more");
50        assert!(s.contains("[redacted]"));
51        assert!(!s.contains("hunter2"));
52    }
53
54    #[test]
55    fn leaves_short_benign_messages() {
56        let s = sanitize_error_message("handler returned Err");
57        assert_eq!(s, "handler returned Err");
58    }
59}