diff --git a/crates/forge_provider/src/forge_provider/provider.rs b/crates/forge_provider/src/forge_provider/provider.rs index 04606d070a..11ed0d12eb 100644 --- a/crates/forge_provider/src/forge_provider/provider.rs +++ b/crates/forge_provider/src/forge_provider/provider.rs @@ -14,7 +14,7 @@ use super::request::Request; use super::response::Response; use crate::error::Error; use crate::forge_provider::transformers::{ProviderPipeline, Transformer}; -use crate::utils::format_http_context; +use crate::utils::{format_http_context, sanitize_headers}; #[derive(Clone, Builder)] pub struct ForgeProvider { @@ -71,7 +71,7 @@ impl ForgeProvider { reqwest::header::CONNECTION, HeaderValue::from_static("keep-alive"), ); - debug!(headers = ?headers, "Request Headers"); + debug!(headers = ?sanitize_headers(&headers), "Request Headers"); headers } @@ -90,7 +90,7 @@ impl ForgeProvider { info!( url = %url, model = %model, - headers = ?headers, + headers = ?sanitize_headers(&headers), message_count = %request.message_count(), message_cache_count = %request.message_cache_count(), "Connecting Upstream" @@ -189,7 +189,7 @@ impl ForgeProvider { async fn fetch_models(&self, url: Url) -> Result { let headers = self.headers(); - info!(method = "GET", url = %url, headers = ?headers, "Fetching Models"); + info!(method = "GET", url = %url, headers = ?sanitize_headers(&headers), "Fetching Models"); match self.client.get(url.clone()).headers(headers).send().await { Ok(response) => { let status = response.status(); diff --git a/crates/forge_provider/src/utils.rs b/crates/forge_provider/src/utils.rs index fce55c5d4f..04386365b4 100644 --- a/crates/forge_provider/src/utils.rs +++ b/crates/forge_provider/src/utils.rs @@ -1,3 +1,4 @@ +use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION}; use reqwest::StatusCode; /// Helper function to format HTTP request/response context for logging and @@ -13,3 +14,54 @@ pub(crate) fn format_http_context>( format!("{} {}", method, url.as_ref()) } } + +/// Sanitizes headers for logging by redacting sensitive values +pub fn sanitize_headers(headers: &HeaderMap) -> HeaderMap { + let sensitive_headers = [AUTHORIZATION.as_str()]; + headers + .iter() + .map(|(name, value)| { + let name_str = name.as_str().to_lowercase(); + let value_str = if sensitive_headers.contains(&name_str.as_str()) { + HeaderValue::from_static("[REDACTED]") + } else { + value.clone() + }; + (name.clone(), value_str) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use reqwest::header::HeaderValue; + + use super::*; + + #[test] + fn test_sanitize_headers_for_logging() { + let mut headers = HeaderMap::new(); + headers.insert( + AUTHORIZATION, + HeaderValue::from_static("Bearer secret-api-key"), + ); + headers.insert("x-api-key", HeaderValue::from_static("another-secret")); + headers.insert("x-title", HeaderValue::from_static("forge")); + headers.insert("content-type", HeaderValue::from_static("application/json")); + + let sanitized = sanitize_headers(&headers); + + assert_eq!( + sanitized.get("authorization"), + Some(&HeaderValue::from_static("[REDACTED]")) + ); + assert_eq!( + sanitized.get("x-title"), + Some(&HeaderValue::from_static("forge")) + ); + assert_eq!( + sanitized.get("content-type"), + Some(&HeaderValue::from_static("application/json")) + ); + } +}