fix(bedrock): use floor_char_boundary to avoid UTF-8 slice panic in header preview

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
This commit is contained in:
SwiftWing21 2026-05-07 21:08:15 -07:00
parent cd89a8297a
commit 82468e4e99

View file

@ -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);
}
}