use async_trait::async_trait;
use pmcp::client::http_middleware::{
HttpMiddleware, HttpMiddlewareChain, HttpMiddlewareContext, HttpRequest, HttpResponse,
};
use pmcp::client::oauth_middleware::{BearerToken, OAuthClientMiddleware};
use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering};
use std::sync::Arc;
struct OrderTrackingMiddleware {
name: &'static str,
priority: i32,
request_order: Arc<AtomicUsize>,
response_order: Arc<AtomicUsize>,
}
impl OrderTrackingMiddleware {
fn new(name: &'static str, priority: i32) -> Self {
Self {
name,
priority,
request_order: Arc::new(AtomicUsize::new(0)),
response_order: Arc::new(AtomicUsize::new(0)),
}
}
fn get_request_order(&self) -> usize {
self.request_order.load(AtomicOrdering::SeqCst)
}
fn get_response_order(&self) -> usize {
self.response_order.load(AtomicOrdering::SeqCst)
}
}
#[async_trait]
impl HttpMiddleware for OrderTrackingMiddleware {
async fn on_request(
&self,
_request: &mut HttpRequest,
context: &HttpMiddlewareContext,
) -> pmcp::Result<()> {
let current_order = context
.get_metadata("request_order_counter")
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(0);
self.request_order
.store(current_order, AtomicOrdering::SeqCst);
context.set_metadata(
"request_order_counter".to_string(),
(current_order + 1).to_string(),
);
tracing::debug!("[{}] on_request: order={}", self.name, current_order);
Ok(())
}
async fn on_response(
&self,
_response: &mut HttpResponse,
context: &HttpMiddlewareContext,
) -> pmcp::Result<()> {
let current_order = context
.get_metadata("response_order_counter")
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(0);
self.response_order
.store(current_order, AtomicOrdering::SeqCst);
context.set_metadata(
"response_order_counter".to_string(),
(current_order + 1).to_string(),
);
tracing::debug!("[{}] on_response: order={}", self.name, current_order);
Ok(())
}
fn priority(&self) -> i32 {
self.priority
}
}
#[tokio::test]
async fn test_middleware_ordering() {
let mw1 = Arc::new(OrderTrackingMiddleware::new("low", 50));
let mw2 = Arc::new(OrderTrackingMiddleware::new("high", 10));
let mw3 = Arc::new(OrderTrackingMiddleware::new("normal", 30));
let mut chain = HttpMiddlewareChain::new();
chain.add(mw1.clone());
chain.add(mw2.clone());
chain.add(mw3.clone());
let mut request = HttpRequest::new("POST".to_string(), "http://test.com".to_string(), vec![]);
let context = HttpMiddlewareContext::new("http://test.com".to_string(), "POST".to_string());
chain.process_request(&mut request, &context).await.unwrap();
assert_eq!(mw2.get_request_order(), 0, "High priority should run first");
assert_eq!(
mw3.get_request_order(),
1,
"Normal priority should run second"
);
assert_eq!(mw1.get_request_order(), 2, "Low priority should run third");
let mut response = HttpResponse::new(200, vec![]);
chain
.process_response(&mut response, &context)
.await
.unwrap();
assert_eq!(
mw1.get_response_order(),
0,
"Low priority should run first in response"
);
assert_eq!(
mw3.get_response_order(),
1,
"Normal priority should run second in response"
);
assert_eq!(
mw2.get_response_order(),
2,
"High priority should run third in response"
);
}
#[tokio::test]
async fn test_oauth_no_provider() {
let token = BearerToken::new("test-token".to_string());
let oauth_mw = OAuthClientMiddleware::new(token);
let mut request = HttpRequest::new("POST".to_string(), "http://test.com".to_string(), vec![]);
let context = HttpMiddlewareContext::new("http://test.com".to_string(), "POST".to_string());
oauth_mw.on_request(&mut request, &context).await.unwrap();
assert_eq!(
request.get_header("Authorization"),
Some("Bearer test-token")
);
}
#[tokio::test]
async fn test_oauth_expired_token() {
use std::time::Duration;
let token = BearerToken::with_expiry("expired-token".to_string(), Duration::from_secs(0));
tokio::time::sleep(Duration::from_millis(10)).await;
let oauth_mw = OAuthClientMiddleware::new(token);
let mut request = HttpRequest::new("POST".to_string(), "http://test.com".to_string(), vec![]);
let context = HttpMiddlewareContext::new("http://test.com".to_string(), "POST".to_string());
let result = oauth_mw.on_request(&mut request, &context).await;
assert!(result.is_err(), "Expired token should return error");
assert!(matches!(
result.unwrap_err(),
pmcp::Error::Authentication(_)
));
}
#[tokio::test]
async fn test_oauth_duplicate_header_detection() {
let token = BearerToken::new("test-token".to_string());
let oauth_mw = OAuthClientMiddleware::new(token);
let mut request = HttpRequest::new("POST".to_string(), "http://test.com".to_string(), vec![]);
request.add_header("Authorization", "Bearer existing-token");
let context = HttpMiddlewareContext::new("http://test.com".to_string(), "POST".to_string());
oauth_mw.on_request(&mut request, &context).await.unwrap();
assert_eq!(
request.get_header("Authorization"),
Some("Bearer existing-token"),
"Existing auth header should not be overwritten"
);
}
#[tokio::test]
async fn test_oauth_precedence_policy() {
let token = BearerToken::new("oauth-token".to_string());
let oauth_mw = OAuthClientMiddleware::new(token);
let mut request = HttpRequest::new("POST".to_string(), "http://test.com".to_string(), vec![]);
let context = HttpMiddlewareContext::new("http://test.com".to_string(), "POST".to_string());
context.set_metadata("auth_already_set".to_string(), "true".to_string());
oauth_mw.on_request(&mut request, &context).await.unwrap();
assert!(
request.get_header("Authorization").is_none(),
"OAuth should skip when transport auth is set"
);
}
#[tokio::test]
async fn test_oauth_401_detection() {
let token = BearerToken::new("test-token".to_string());
let oauth_mw = OAuthClientMiddleware::new(token);
let mut response = HttpResponse::new(401, vec![]);
let context = HttpMiddlewareContext::new("http://test.com".to_string(), "POST".to_string());
let result = oauth_mw.on_response(&mut response, &context).await;
assert!(result.is_err(), "401 response should return error");
assert_eq!(
context.get_metadata("auth_failure"),
Some("true".to_string())
);
assert_eq!(context.get_metadata("status_code"), Some("401".to_string()));
}
#[tokio::test]
async fn test_middleware_short_circuit_on_error() {
struct FailingMiddleware;
#[async_trait]
impl HttpMiddleware for FailingMiddleware {
async fn on_request(
&self,
_request: &mut HttpRequest,
_context: &HttpMiddlewareContext,
) -> pmcp::Result<()> {
Err(pmcp::Error::authentication("Test failure"))
}
fn priority(&self) -> i32 {
10
}
}
struct TrackingMiddleware {
called: Arc<AtomicUsize>,
}
#[async_trait]
impl HttpMiddleware for TrackingMiddleware {
async fn on_request(
&self,
_request: &mut HttpRequest,
_context: &HttpMiddlewareContext,
) -> pmcp::Result<()> {
self.called.fetch_add(1, AtomicOrdering::SeqCst);
Ok(())
}
fn priority(&self) -> i32 {
20 }
}
let mut chain = HttpMiddlewareChain::new();
chain.add(Arc::new(FailingMiddleware));
let tracking = Arc::new(TrackingMiddleware {
called: Arc::new(AtomicUsize::new(0)),
});
chain.add(tracking.clone());
let mut request = HttpRequest::new("POST".to_string(), "http://test.com".to_string(), vec![]);
let context = HttpMiddlewareContext::new("http://test.com".to_string(), "POST".to_string());
let result = chain.process_request(&mut request, &context).await;
assert!(result.is_err(), "Chain should short-circuit on error");
assert_eq!(
tracking.called.load(AtomicOrdering::SeqCst),
0,
"Later middleware should not run after error"
);
}
#[tokio::test]
async fn test_concurrency_no_shared_state_contention() {
use tokio::task::JoinSet;
let token = BearerToken::new("test-token".to_string());
let oauth_mw = Arc::new(OAuthClientMiddleware::new(token));
let mut chain = HttpMiddlewareChain::new();
chain.add(oauth_mw);
let chain = Arc::new(chain);
let mut set = JoinSet::new();
for i in 0..100 {
let chain_clone = chain.clone();
set.spawn(async move {
let mut request = HttpRequest::new(
"POST".to_string(),
format!("http://test.com/req{}", i),
vec![],
);
let context =
HttpMiddlewareContext::new(format!("http://test.com/req{}", i), "POST".to_string());
chain_clone.process_request(&mut request, &context).await
});
}
let mut success_count = 0;
while let Some(result) = set.join_next().await {
assert!(result.is_ok(), "Task should not panic");
if result.unwrap().is_ok() {
success_count += 1;
}
}
assert_eq!(success_count, 100, "All parallel requests should succeed");
}
#[tokio::test]
async fn test_header_case_insensitivity() {
use http::HeaderMap;
use pmcp::client::http_middleware::{HttpRequest, HttpResponse};
let mut request = HttpRequest::new("POST".to_string(), "http://test.com".to_string(), vec![]);
request.add_header("Content-Type", "application/json");
request.add_header("Authorization", "Bearer token");
request.add_header("x-custom-header", "value");
assert_eq!(
request.get_header("content-type"),
Some("application/json"),
"Lowercase lookup should work"
);
assert_eq!(
request.get_header("Content-Type"),
Some("application/json"),
"Mixed case lookup should work"
);
assert_eq!(
request.get_header("CONTENT-TYPE"),
Some("application/json"),
"Uppercase lookup should work"
);
assert_eq!(
request.get_header("authorization"),
Some("Bearer token"),
"Authorization header should be accessible"
);
assert_eq!(
request.get_header("AUTHORIZATION"),
Some("Bearer token"),
"Case variation should work"
);
assert!(request.has_header("authorization"), "Lowercase check");
assert!(request.has_header("Authorization"), "Mixed case check");
assert!(request.has_header("AUTHORIZATION"), "Uppercase check");
assert!(request.has_header("x-custom-header"), "Custom header check");
assert!(
request.has_header("X-Custom-Header"),
"Custom header mixed case"
);
let removed = request.remove_header("CONTENT-TYPE");
assert_eq!(removed, Some("application/json".to_string()));
assert!(
!request.has_header("content-type"),
"Header should be removed"
);
assert!(
!request.has_header("Content-Type"),
"Header should be removed (any case)"
);
let mut response = HttpResponse::new(200, vec![]);
response.add_header("Content-Length", "123");
response.add_header("cache-control", "no-cache");
assert_eq!(response.get_header("content-length"), Some("123"));
assert_eq!(response.get_header("CONTENT-LENGTH"), Some("123"));
assert_eq!(response.get_header("Cache-Control"), Some("no-cache"));
let mut headers = HeaderMap::new();
headers.insert("Content-Type", "text/plain".parse().unwrap());
headers.insert("X-Custom", "value".parse().unwrap());
let response2 = HttpResponse::with_headers(200, headers, vec![]);
assert_eq!(
response2.get_header("content-type"),
Some("text/plain"),
"with_headers should work with HeaderMap"
);
assert_eq!(
response2.get_header("CONTENT-TYPE"),
Some("text/plain"),
"Case-insensitive lookup should work"
);
assert_eq!(
response2.get_header("x-custom"),
Some("value"),
"Custom headers should be accessible"
);
}
#[tokio::test]
async fn test_oauth_duplicate_detection_case_insensitive() {
use pmcp::client::oauth_middleware::{BearerToken, OAuthClientMiddleware};
let token = BearerToken::new("oauth-token".to_string());
let oauth_mw = OAuthClientMiddleware::new(token);
let mut request = HttpRequest::new("POST".to_string(), "http://test.com".to_string(), vec![]);
let context = HttpMiddlewareContext::new("http://test.com".to_string(), "POST".to_string());
request.add_header(
"AUTHORIZATION", "Bearer existing-token",
);
oauth_mw.on_request(&mut request, &context).await.unwrap();
assert_eq!(
request.get_header("authorization"), Some("Bearer existing-token"),
"Original header should be preserved"
);
assert_eq!(
request.get_header("Authorization"), Some("Bearer existing-token"),
"Case-insensitive lookup should work"
);
let auth_header_count = request
.headers
.iter()
.filter(|(k, _)| k.as_str() == "authorization")
.count();
assert_eq!(
auth_header_count, 1,
"Should only have one authorization header"
);
}
#[tokio::test]
async fn test_middleware_chain_with_mixed_case_headers() {
struct AuthCheckMiddleware;
#[async_trait]
impl HttpMiddleware for AuthCheckMiddleware {
async fn on_request(
&self,
request: &mut HttpRequest,
_context: &HttpMiddlewareContext,
) -> pmcp::Result<()> {
assert!(
request.has_header("authorization"),
"Should find auth header"
);
assert!(
request.has_header("Authorization"),
"Should be case-insensitive"
);
assert!(
request.has_header("AUTHORIZATION"),
"Should work with uppercase"
);
let auth = request.get_header("AuThOrIzAtIoN");
assert!(auth.is_some(), "Mixed case lookup should work");
Ok(())
}
fn priority(&self) -> i32 {
20
}
}
let mut chain = HttpMiddlewareChain::new();
let token = BearerToken::new("test-token".to_string());
chain.add(Arc::new(OAuthClientMiddleware::new(token)));
chain.add(Arc::new(AuthCheckMiddleware));
let mut request = HttpRequest::new("POST".to_string(), "http://test.com".to_string(), vec![]);
let context = HttpMiddlewareContext::new("http://test.com".to_string(), "POST".to_string());
chain.process_request(&mut request, &context).await.unwrap();
assert!(request.has_header("authorization"));
assert!(request.has_header("Authorization"));
assert!(request.has_header("AUTHORIZATION"));
assert_eq!(
request.get_header("authorization"),
Some("Bearer test-token")
);
}
#[tokio::test]
async fn test_oauth_retry_coordination_with_metadata() {
use pmcp::client::oauth_middleware::{BearerToken, OAuthClientMiddleware};
struct RetryCoordinationMiddleware {
retry_attempted: Arc<AtomicUsize>,
}
#[async_trait]
impl HttpMiddleware for RetryCoordinationMiddleware {
async fn on_error(
&self,
error: &pmcp::Error,
context: &HttpMiddlewareContext,
) -> pmcp::Result<()> {
if matches!(error, pmcp::Error::Authentication(_)) {
if context.get_metadata("auth_failure") == Some("true".to_string()) {
if context.get_metadata("oauth.retry_used").is_some() {
tracing::warn!("OAuth retry already attempted, not retrying again");
return Ok(());
}
context.set_metadata("oauth.retry_used".to_string(), "true".to_string());
self.retry_attempted.fetch_add(1, AtomicOrdering::SeqCst);
tracing::info!("Retry middleware: auth failure detected, would retry here");
}
}
Ok(())
}
fn priority(&self) -> i32 {
5 }
}
let token = BearerToken::new("test-token".to_string());
let oauth_mw = Arc::new(OAuthClientMiddleware::new(token));
let retry_mw = Arc::new(RetryCoordinationMiddleware {
retry_attempted: Arc::new(AtomicUsize::new(0)),
});
let mut chain = HttpMiddlewareChain::new();
chain.add(oauth_mw);
chain.add(retry_mw.clone());
let mut response = HttpResponse::new(401, vec![]);
let context = HttpMiddlewareContext::new("http://test.com".to_string(), "POST".to_string());
let result = chain.process_response(&mut response, &context).await;
assert!(result.is_err(), "OAuth should return error for 401");
assert_eq!(
context.get_metadata("auth_failure"),
Some("true".to_string()),
"OAuth should set auth_failure metadata"
);
assert_eq!(
context.get_metadata("status_code"),
Some("401".to_string()),
"OAuth should set status_code metadata"
);
assert_eq!(
context.get_metadata("oauth.retry_used"),
Some("true".to_string()),
"Retry middleware should set retry_used metadata"
);
assert_eq!(
retry_mw.retry_attempted.load(AtomicOrdering::SeqCst),
1,
"Retry should have been attempted once"
);
}
#[tokio::test]
async fn test_double_retry_protection() {
use pmcp::client::oauth_middleware::{BearerToken, OAuthClientMiddleware};
struct RetryTrackingMiddleware {
retry_count: Arc<AtomicUsize>,
}
#[async_trait]
impl HttpMiddleware for RetryTrackingMiddleware {
async fn on_error(
&self,
error: &pmcp::Error,
context: &HttpMiddlewareContext,
) -> pmcp::Result<()> {
if matches!(error, pmcp::Error::Authentication(_))
&& context.get_metadata("auth_failure") == Some("true".to_string())
{
if context.get_metadata("oauth.retry_used").is_none() {
context.set_metadata("oauth.retry_used".to_string(), "true".to_string());
self.retry_count.fetch_add(1, AtomicOrdering::SeqCst);
tracing::info!("First retry attempt");
} else {
tracing::warn!("Retry already used, preventing double retry");
}
}
Ok(())
}
fn priority(&self) -> i32 {
5 }
}
let token = BearerToken::new("test-token".to_string());
let oauth_mw = Arc::new(OAuthClientMiddleware::new(token));
let retry_tracker = Arc::new(RetryTrackingMiddleware {
retry_count: Arc::new(AtomicUsize::new(0)),
});
let mut chain = HttpMiddlewareChain::new();
chain.add(oauth_mw);
chain.add(retry_tracker.clone());
let mut response1 = HttpResponse::new(401, vec![]);
let context = HttpMiddlewareContext::new("http://test.com".to_string(), "POST".to_string());
let _ = chain.process_response(&mut response1, &context).await;
assert_eq!(
retry_tracker.retry_count.load(AtomicOrdering::SeqCst),
1,
"First retry should have been attempted"
);
let mut response2 = HttpResponse::new(401, vec![]);
let _ = chain.process_response(&mut response2, &context).await;
assert_eq!(
retry_tracker.retry_count.load(AtomicOrdering::SeqCst),
1,
"Second retry should have been prevented by oauth.retry_used metadata"
);
assert_eq!(
context.get_metadata("oauth.retry_used"),
Some("true".to_string()),
"Retry metadata should persist"
);
}
#[tokio::test]
async fn test_oauth_error_hook_logging() {
use pmcp::client::oauth_middleware::{BearerToken, OAuthClientMiddleware};
let token = BearerToken::new("test-token".to_string());
let oauth_mw = OAuthClientMiddleware::new(token);
let context = HttpMiddlewareContext::new("http://test.com".to_string(), "POST".to_string());
let error = pmcp::Error::authentication("Test authentication error");
let result = oauth_mw.on_error(&error, &context).await;
assert!(
result.is_ok(),
"on_error should handle auth errors gracefully"
);
let other_error = pmcp::Error::internal("Other error");
let result = oauth_mw.on_error(&other_error, &context).await;
assert!(
result.is_ok(),
"on_error should handle non-auth errors gracefully"
);
}
#[tokio::test]
async fn test_http_logging_redacts_sensitive_headers() {
use pmcp::client::http_logging_middleware::HttpLoggingMiddleware;
let middleware = HttpLoggingMiddleware::new();
let mut request = HttpRequest::new("GET".to_string(), "http://test.com".to_string(), vec![]);
request.add_header("Authorization", "Bearer my-secret-token-12345");
request.add_header("AUTHORIZATION", "Bearer another-secret");
request.add_header("cookie", "session=abc123");
request.add_header("x-api-key", "api-key-secret");
request.add_header("content-type", "application/json");
let context = HttpMiddlewareContext::new("http://test.com".to_string(), "GET".to_string());
middleware.on_request(&mut request, &context).await.unwrap();
let formatted = middleware.redact_header_value(
&http::HeaderName::from_static("authorization"),
"Bearer secret",
);
assert_eq!(formatted, "Bearer [REDACTED]");
let formatted_cookie =
middleware.redact_header_value(&http::HeaderName::from_static("cookie"), "session=abc123");
assert_eq!(formatted_cookie, "[REDACTED]");
let formatted_ct = middleware.redact_header_value(
&http::HeaderName::from_static("content-type"),
"application/json",
);
assert_eq!(formatted_ct, "application/json");
}
#[tokio::test]
async fn test_http_logging_respects_overrides() {
use http::HeaderName;
use pmcp::client::http_logging_middleware::HttpLoggingMiddleware;
let middleware =
HttpLoggingMiddleware::new().allow_header(&HeaderName::from_static("x-api-key"));
let formatted =
middleware.redact_header_value(&HeaderName::from_static("x-api-key"), "my-api-key-12345");
assert_eq!(formatted, "my-api-key-12345");
let formatted_auth = middleware.redact_header_value(
&HeaderName::from_static("authorization"),
"Bearer secret-token",
);
assert_eq!(formatted_auth, "Bearer [REDACTED]");
}
#[tokio::test]
async fn test_http_logging_multivalue_headers() {
use http::HeaderMap;
use pmcp::client::http_logging_middleware::HttpLoggingMiddleware;
let middleware = HttpLoggingMiddleware::new();
let mut headers = HeaderMap::new();
headers.append("set-cookie", "session1=abc123".parse().unwrap());
headers.append("set-cookie", "session2=def456".parse().unwrap());
headers.append("set-cookie", "user=john".parse().unwrap());
headers.insert("content-type", "application/json".parse().unwrap());
let formatted = middleware.format_headers(&headers);
assert!(formatted.contains("[REDACTED]"));
assert!(formatted.contains("application/json"));
let redacted_count = formatted.matches("[REDACTED]").count();
assert!(
redacted_count >= 3,
"Expected at least 3 redacted values, got {}",
redacted_count
);
}
#[tokio::test]
async fn test_http_logging_body_disabled_by_default() {
use pmcp::client::http_logging_middleware::HttpLoggingMiddleware;
let middleware_default = HttpLoggingMiddleware::new();
assert!(
middleware_default.max_body_bytes().is_none(),
"Default middleware should not log bodies"
);
let middleware_with_body = HttpLoggingMiddleware::new().with_max_body_bytes(1024);
assert_eq!(
middleware_with_body.max_body_bytes(),
Some(1024),
"Middleware with body logging should have max_body_bytes set"
);
let body = b"sensitive data in body";
let mut request = HttpRequest::new(
"POST".to_string(),
"http://test.com".to_string(),
body.to_vec(),
);
let context = HttpMiddlewareContext::new("http://test.com".to_string(), "POST".to_string());
middleware_default
.on_request(&mut request, &context)
.await
.unwrap();
middleware_with_body
.on_request(&mut request, &context)
.await
.unwrap();
}
#[tokio::test]
async fn test_http_logging_authorization_without_scheme() {
use http::HeaderName;
use pmcp::client::http_logging_middleware::HttpLoggingMiddleware;
let middleware = HttpLoggingMiddleware::new().with_show_auth_scheme(false);
let formatted = middleware.redact_header_value(
&HeaderName::from_static("authorization"),
"Bearer my-secret-token",
);
assert_eq!(formatted, "[REDACTED]");
let formatted_basic = middleware.redact_header_value(
&HeaderName::from_static("authorization"),
"Basic dXNlcjpwYXNz",
);
assert_eq!(formatted_basic, "[REDACTED]");
}
#[tokio::test]
async fn test_http_logging_header_value_truncation() {
use http::HeaderName;
use pmcp::client::http_logging_middleware::HttpLoggingMiddleware;
let middleware = HttpLoggingMiddleware::new().with_max_header_value_len(20);
let long_value = "this-is-a-very-long-content-type-value-that-exceeds-the-limit";
let formatted =
middleware.redact_header_value(&HeaderName::from_static("content-type"), long_value);
assert!(formatted.len() <= 23, "Truncated value too long"); assert!(formatted.ends_with("..."), "Should end with ellipsis");
}
#[tokio::test]
async fn test_http_logging_case_insensitive_redaction() {
use http::HeaderName;
use pmcp::client::http_logging_middleware::HttpLoggingMiddleware;
let middleware = HttpLoggingMiddleware::new();
let test_cases = vec![
("authorization", "Bearer secret"),
("Authorization", "Bearer secret"),
("AUTHORIZATION", "Bearer secret"),
("AuThOrIzAtIoN", "Bearer secret"),
];
for (header_name, value) in test_cases {
let name = HeaderName::from_bytes(header_name.as_bytes()).unwrap();
let formatted = middleware.redact_header_value(&name, value);
assert_eq!(
formatted, "Bearer [REDACTED]",
"Failed for header name: {}",
header_name
);
}
let cookie_cases = vec![
("cookie", "session=abc"),
("Cookie", "session=abc"),
("COOKIE", "session=abc"),
];
for (header_name, value) in cookie_cases {
let name = HeaderName::from_bytes(header_name.as_bytes()).unwrap();
let formatted = middleware.redact_header_value(&name, value);
assert_eq!(
formatted, "[REDACTED]",
"Failed for header name: {}",
header_name
);
}
}