use uuid::Uuid;
use crate::error::Result;
use crate::http::{Request, Response};
use crate::router::Next;
pub const CORRELATION_ID_HEADER: &str = "x-correlation-id";
#[derive(Debug, Clone)]
pub struct CorrelationId(pub String);
impl CorrelationId {
pub fn as_str(&self) -> &str {
&self.0
}
}
pub async fn correlation_id(mut req: Request, next: Next) -> Result<Response> {
let id = inbound_id_or_fresh(req.header(CORRELATION_ID_HEADER));
req.ctx_mut().insert(CorrelationId(id.clone()));
let mut resp = next.run(req).await?;
resp = resp.with_header(CORRELATION_ID_HEADER, id);
Ok(resp)
}
fn inbound_id_or_fresh(header: Option<&str>) -> String {
if let Some(raw) = header {
let trimmed = raw.trim();
if looks_like_id(trimmed) {
return trimmed.to_string();
}
}
Uuid::now_v7().hyphenated().to_string()
}
fn looks_like_id(s: &str) -> bool {
if s.is_empty() || s.len() > 64 || s.len() < 16 {
return false;
}
s.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fresh_id_is_uuid_v7_shape() {
let id = inbound_id_or_fresh(None);
assert_eq!(id.len(), 36);
let uuid = Uuid::parse_str(&id).expect("valid uuid");
assert_eq!(uuid.get_version(), Some(uuid::Version::SortRand));
}
#[test]
fn inbound_safe_value_is_kept() {
let inbound = "01910e4f-4b0a-7e62-8d87-1e7a09c3b1aa";
let out = inbound_id_or_fresh(Some(inbound));
assert_eq!(out, inbound);
}
#[test]
fn inbound_with_whitespace_is_trimmed() {
let inbound = " 01910e4f-4b0a-7e62-8d87-1e7a09c3b1aa ";
let out = inbound_id_or_fresh(Some(inbound));
assert_eq!(out, inbound.trim());
}
#[test]
fn inbound_too_short_is_replaced() {
let out = inbound_id_or_fresh(Some("short"));
assert_ne!(out, "short");
assert!(Uuid::parse_str(&out).is_ok());
}
#[test]
fn inbound_too_long_is_replaced() {
let evil = "x".repeat(200);
let out = inbound_id_or_fresh(Some(&evil));
assert_ne!(out, evil);
}
#[test]
fn inbound_with_control_chars_is_replaced() {
for evil in [
"abc def 1234567890",
"abc\ndef-1234567890",
"\x1b[31mevil\x1b[0m-1234",
] {
let out = inbound_id_or_fresh(Some(evil));
assert_ne!(out, evil, "header {evil:?} should have been replaced");
}
}
#[test]
fn fresh_ids_are_unique() {
assert_ne!(inbound_id_or_fresh(None), inbound_id_or_fresh(None));
}
}