Skip to content

Commit 54b2ccc

Browse files
fix(http): consolidate sanitize_headers into forge_infra and expand sensitive header list (#2879)
1 parent 6888e47 commit 54b2ccc

5 files changed

Lines changed: 72 additions & 70 deletions

File tree

crates/forge_infra/src/http.rs

Lines changed: 66 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -203,25 +203,32 @@ impl<F: forge_app::FileWriterInfra + 'static> ForgeHttpInfra<F> {
203203
reqwest::header::CONNECTION,
204204
HeaderValue::from_static("keep-alive"),
205205
);
206-
debug!(headers = ?Self::sanitize_headers(&headers), "Request Headers");
206+
debug!(headers = ?sanitize_headers(&headers), "Request Headers");
207207
headers
208208
}
209+
}
209210

210-
fn sanitize_headers(headers: &HeaderMap) -> HeaderMap {
211-
let sensitive_headers = [AUTHORIZATION.as_str()];
212-
headers
213-
.iter()
214-
.map(|(name, value)| {
215-
let name_str = name.as_str().to_lowercase();
216-
let value_str = if sensitive_headers.contains(&name_str.as_str()) {
217-
HeaderValue::from_static("[REDACTED]")
218-
} else {
219-
value.clone()
220-
};
221-
(name.clone(), value_str)
222-
})
223-
.collect()
224-
}
211+
/// Sanitizes headers for logging by redacting sensitive values like
212+
/// authorization tokens and API keys.
213+
pub fn sanitize_headers(headers: &HeaderMap) -> HeaderMap {
214+
let sensitive_headers = [
215+
AUTHORIZATION.as_str(),
216+
"x-api-key",
217+
"x-goog-api-key",
218+
"api-key",
219+
];
220+
headers
221+
.iter()
222+
.map(|(name, value)| {
223+
let name_str = name.as_str().to_lowercase();
224+
let value_str = if sensitive_headers.contains(&name_str.as_str()) {
225+
HeaderValue::from_static("[REDACTED]")
226+
} else {
227+
value.clone()
228+
};
229+
(name.clone(), value_str)
230+
})
231+
.collect()
225232
}
226233

227234
impl<F: forge_app::FileWriterInfra + 'static> ForgeHttpInfra<F> {
@@ -489,4 +496,47 @@ mod tests {
489496
assert_eq!(writes[0].0, debug_path);
490497
assert_eq!(writes[0].1, body);
491498
}
499+
500+
#[test]
501+
fn test_sanitize_headers_redacts_sensitive_values() {
502+
use reqwest::header::HeaderValue;
503+
504+
let mut headers = HeaderMap::new();
505+
headers.insert(
506+
AUTHORIZATION,
507+
HeaderValue::from_static("Bearer secret-api-key"),
508+
);
509+
headers.insert("x-api-key", HeaderValue::from_static("another-secret"));
510+
headers.insert("x-goog-api-key", HeaderValue::from_static("google-secret"));
511+
headers.insert("api-key", HeaderValue::from_static("generic-secret"));
512+
headers.insert("x-title", HeaderValue::from_static("forge"));
513+
headers.insert("content-type", HeaderValue::from_static("application/json"));
514+
515+
let sanitized = sanitize_headers(&headers);
516+
517+
assert_eq!(
518+
sanitized.get("authorization"),
519+
Some(&HeaderValue::from_static("[REDACTED]"))
520+
);
521+
assert_eq!(
522+
sanitized.get("x-api-key"),
523+
Some(&HeaderValue::from_static("[REDACTED]"))
524+
);
525+
assert_eq!(
526+
sanitized.get("x-goog-api-key"),
527+
Some(&HeaderValue::from_static("[REDACTED]"))
528+
);
529+
assert_eq!(
530+
sanitized.get("api-key"),
531+
Some(&HeaderValue::from_static("[REDACTED]"))
532+
);
533+
assert_eq!(
534+
sanitized.get("x-title"),
535+
Some(&HeaderValue::from_static("forge"))
536+
);
537+
assert_eq!(
538+
sanitized.get("content-type"),
539+
Some(&HeaderValue::from_static("application/json"))
540+
);
541+
}
492542
}

crates/forge_infra/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,5 @@ pub use console::StdConsoleWriter;
2323
pub use env::ForgeEnvironmentInfra;
2424
pub use executor::ForgeCommandExecutorService;
2525
pub use forge_infra::*;
26+
pub use http::sanitize_headers;
2627
pub use kv_storage::CacacheStorage;

crates/forge_repo/src/provider/openai.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ use forge_app::domain::{
1010
use forge_app::dto::openai::{ListModelResponse, ProviderPipeline, Request, Response};
1111
use forge_config::RetryConfig;
1212
use forge_domain::{ChatRepository, Provider};
13+
use forge_infra::sanitize_headers;
1314
use reqwest::header::AUTHORIZATION;
1415
use tokio_stream::StreamExt;
1516
use tracing::{debug, info};
1617
use url::Url;
1718

1819
use crate::provider::event::into_chat_completion_message;
1920
use crate::provider::retry::into_retry;
20-
use crate::provider::utils::{create_headers, format_http_context, join_url, sanitize_headers};
21+
use crate::provider::utils::{create_headers, format_http_context, join_url};
2122

2223
/// Enhances error messages with provider-specific helpful information
2324
fn enhance_error(error: anyhow::Error, provider_id: &ProviderId) -> anyhow::Error {

crates/forge_repo/src/provider/openai_responses/repository.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ use forge_app::domain::{
1010
};
1111
use forge_config::RetryConfig;
1212
use forge_domain::{BoxStream, ChatRepository, Provider};
13+
use forge_infra::sanitize_headers;
1314
use futures::StreamExt;
1415
use reqwest::header::AUTHORIZATION;
1516
use tracing::info;
1617
use url::Url;
1718

1819
use crate::provider::FromDomain;
1920
use crate::provider::retry::into_retry;
20-
use crate::provider::utils::{create_headers, format_http_context, sanitize_headers};
21+
use crate::provider::utils::{create_headers, format_http_context};
2122

2223
#[derive(Clone)]
2324
pub(super) struct OpenAIResponsesProvider<H> {
Lines changed: 1 addition & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use anyhow::Context as _;
2-
use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue};
2+
use reqwest::header::HeaderMap;
33
use reqwest::{StatusCode, Url};
44

55
/// Helper function to format HTTP request/response context for logging and
@@ -49,54 +49,3 @@ pub fn create_headers(headers: Vec<(String, String)>) -> HeaderMap {
4949
}
5050
header_map
5151
}
52-
53-
/// Sanitizes headers for logging by redacting sensitive values
54-
pub fn sanitize_headers(headers: &HeaderMap) -> HeaderMap {
55-
let sensitive_headers = [AUTHORIZATION.as_str()];
56-
headers
57-
.iter()
58-
.map(|(name, value)| {
59-
let name_str = name.as_str().to_lowercase();
60-
let value_str = if sensitive_headers.contains(&name_str.as_str()) {
61-
HeaderValue::from_static("[REDACTED]")
62-
} else {
63-
value.clone()
64-
};
65-
(name.clone(), value_str)
66-
})
67-
.collect()
68-
}
69-
70-
#[cfg(test)]
71-
mod tests {
72-
use reqwest::header::HeaderValue;
73-
74-
use super::*;
75-
76-
#[test]
77-
fn test_sanitize_headers_for_logging() {
78-
let mut headers = HeaderMap::new();
79-
headers.insert(
80-
AUTHORIZATION,
81-
HeaderValue::from_static("Bearer secret-api-key"),
82-
);
83-
headers.insert("x-api-key", HeaderValue::from_static("another-secret"));
84-
headers.insert("x-title", HeaderValue::from_static("forge"));
85-
headers.insert("content-type", HeaderValue::from_static("application/json"));
86-
87-
let sanitized = sanitize_headers(&headers);
88-
89-
assert_eq!(
90-
sanitized.get("authorization"),
91-
Some(&HeaderValue::from_static("[REDACTED]"))
92-
);
93-
assert_eq!(
94-
sanitized.get("x-title"),
95-
Some(&HeaderValue::from_static("forge"))
96-
);
97-
assert_eq!(
98-
sanitized.get("content-type"),
99-
Some(&HeaderValue::from_static("application/json"))
100-
);
101-
}
102-
}

0 commit comments

Comments
 (0)