From 82468e4e99e6013ab9a44df2e2f12c9352bbc18f Mon Sep 17 00:00:00 2001 From: SwiftWing21 Date: Thu, 7 May 2026 21:08:15 -0700 Subject: [PATCH] fix(bedrock): use floor_char_boundary to avoid UTF-8 slice panic in header preview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit header_value_preview in eventstream_to_sse.rs used a raw byte-slice (&s[..64]) to truncate long header strings for log output. If byte index 64 landed inside a multi-byte codepoint (e.g. 63 ASCII chars followed by é or an emoji), Rust panics at runtime. Replace with floor_char_boundary(64) which returns the largest valid char boundary ≤ 64 without scanning the whole string. Two regression tests added: - truncates_at_char_boundary: 63 ASCII + é → must not panic, must end with … - exact_boundary_not_truncated: 64-byte ASCII string is returned unchanged Fixes #415 --- .../src/bedrock/eventstream_to_sse.rs | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/crates/headroom-proxy/src/bedrock/eventstream_to_sse.rs b/crates/headroom-proxy/src/bedrock/eventstream_to_sse.rs index 5b0c56804..ec7a95d55 100644 --- a/crates/headroom-proxy/src/bedrock/eventstream_to_sse.rs +++ b/crates/headroom-proxy/src/bedrock/eventstream_to_sse.rs @@ -238,7 +238,8 @@ pub fn header_value_preview(v: &HeaderValue) -> String { if s.len() <= 64 { s.clone() } else { - format!("{}…", &s[..64]) + let end = s.floor_char_boundary(64); + format!("{}…", &s[..end]) } } HeaderValue::Bytes(b) => format!("[{} bytes]", b.len()), @@ -394,4 +395,23 @@ mod tests { .starts_with("[3 bytes") ); } + + #[test] + fn header_value_preview_truncates_at_char_boundary() { + // 63 ASCII bytes + a 2-byte UTF-8 char (é = U+00E9) puts a char + // boundary at byte 63 but NOT at byte 64 — the old `&s[..64]` + // would panic here. floor_char_boundary(64) must return 63. + let s = "x".repeat(63) + "éfoo"; + assert!(s.len() > 64); + let preview = header_value_preview(&HeaderValue::String(s)); + assert!(preview.ends_with('…'), "expected ellipsis suffix: {preview:?}"); + assert!(!preview.contains('é'), "must not include the split codepoint"); + } + + #[test] + fn header_value_preview_exact_boundary_not_truncated() { + // A string whose UTF-8 length is exactly 64 must not be truncated. + let s = "x".repeat(64); + assert_eq!(header_value_preview(&HeaderValue::String(s.clone())), s); + } }