use std::sync::OnceLock;
use regex::Regex;
pub const DEFAULT_ERROR_BODY_MAX_LEN: usize = 500;
pub fn truncate_error_body(body: &str, max_len: usize) -> String {
if body.len() <= max_len {
return body.to_string();
}
let mut cut = max_len;
while cut > 0 && !body.is_char_boundary(cut) {
cut -= 1;
}
format!("{}... (truncated)", &body[..cut])
}
fn url_regex() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| Regex::new(r"(https?://[^\s?#]+)\?[^\s]*").expect("valid URL regex"))
}
pub fn sanitize_url_in_error(msg: &str) -> String {
url_regex().replace_all(msg, "$1").into_owned()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn truncate_shorter_than_limit_is_unchanged() {
assert_eq!(truncate_error_body("abc", 500), "abc");
}
#[test]
fn truncate_longer_is_cut_and_marked() {
let body = "a".repeat(600);
let out = truncate_error_body(&body, 500);
assert!(out.ends_with("... (truncated)"));
assert_eq!(out.len(), 500 + "... (truncated)".len());
}
#[test]
fn truncate_respects_utf8_boundaries() {
let body = "漢".repeat(300);
let out = truncate_error_body(&body, 500);
assert!(out.ends_with("... (truncated)"));
assert!(out.contains('漢'));
}
#[test]
fn sanitize_strips_query_string() {
let input = "request failed for https://api.example.com/v1/chat?api-key=sk-test-key-12345";
let out = sanitize_url_in_error(input);
assert_eq!(out, "request failed for https://api.example.com/v1/chat");
assert!(!out.contains("sk-test-key"));
}
#[test]
fn sanitize_leaves_urls_without_query_alone() {
let input = "timeout on https://api.example.com/v1/chat";
assert_eq!(sanitize_url_in_error(input), input);
}
#[test]
fn sanitize_preserves_trailing_text_after_whitespace() {
let input = "error at https://x.test/a?token=secret while retrying";
let out = sanitize_url_in_error(input);
assert_eq!(out, "error at https://x.test/a while retrying");
}
}