Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 66 additions & 16 deletions crates/forge_infra/src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,25 +203,32 @@ impl<F: forge_app::FileWriterInfra + 'static> ForgeHttpInfra<F> {
reqwest::header::CONNECTION,
HeaderValue::from_static("keep-alive"),
);
debug!(headers = ?Self::sanitize_headers(&headers), "Request Headers");
debug!(headers = ?sanitize_headers(&headers), "Request Headers");
headers
}
}

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()
}
/// Sanitizes headers for logging by redacting sensitive values like
/// authorization tokens and API keys.
pub fn sanitize_headers(headers: &HeaderMap) -> HeaderMap {
let sensitive_headers = [
AUTHORIZATION.as_str(),
"x-api-key",
"x-goog-api-key",
"api-key",
];
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()
}

impl<F: forge_app::FileWriterInfra + 'static> ForgeHttpInfra<F> {
Expand Down Expand Up @@ -489,4 +496,47 @@ mod tests {
assert_eq!(writes[0].0, debug_path);
assert_eq!(writes[0].1, body);
}

#[test]
fn test_sanitize_headers_redacts_sensitive_values() {
use reqwest::header::HeaderValue;

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-goog-api-key", HeaderValue::from_static("google-secret"));
headers.insert("api-key", HeaderValue::from_static("generic-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-api-key"),
Some(&HeaderValue::from_static("[REDACTED]"))
);
assert_eq!(
sanitized.get("x-goog-api-key"),
Some(&HeaderValue::from_static("[REDACTED]"))
);
assert_eq!(
sanitized.get("api-key"),
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"))
);
}
}
1 change: 1 addition & 0 deletions crates/forge_infra/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ pub use console::StdConsoleWriter;
pub use env::ForgeEnvironmentInfra;
pub use executor::ForgeCommandExecutorService;
pub use forge_infra::*;
pub use http::sanitize_headers;
pub use kv_storage::CacacheStorage;
3 changes: 2 additions & 1 deletion crates/forge_repo/src/provider/openai.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ use forge_app::domain::{
use forge_app::dto::openai::{ListModelResponse, ProviderPipeline, Request, Response};
use forge_config::RetryConfig;
use forge_domain::{ChatRepository, Provider};
use forge_infra::sanitize_headers;
use reqwest::header::AUTHORIZATION;
use tokio_stream::StreamExt;
use tracing::{debug, info};
use url::Url;

use crate::provider::event::into_chat_completion_message;
use crate::provider::retry::into_retry;
use crate::provider::utils::{create_headers, format_http_context, join_url, sanitize_headers};
use crate::provider::utils::{create_headers, format_http_context, join_url};

/// Enhances error messages with provider-specific helpful information
fn enhance_error(error: anyhow::Error, provider_id: &ProviderId) -> anyhow::Error {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ use forge_app::domain::{
};
use forge_config::RetryConfig;
use forge_domain::{BoxStream, ChatRepository, Provider};
use forge_infra::sanitize_headers;
use futures::StreamExt;
use reqwest::header::AUTHORIZATION;
use tracing::info;
use url::Url;

use crate::provider::FromDomain;
use crate::provider::retry::into_retry;
use crate::provider::utils::{create_headers, format_http_context, sanitize_headers};
use crate::provider::utils::{create_headers, format_http_context};

#[derive(Clone)]
pub(super) struct OpenAIResponsesProvider<H> {
Expand Down
53 changes: 1 addition & 52 deletions crates/forge_repo/src/provider/utils.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use anyhow::Context as _;
use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue};
use reqwest::header::HeaderMap;
use reqwest::{StatusCode, Url};

/// Helper function to format HTTP request/response context for logging and
Expand Down Expand Up @@ -49,54 +49,3 @@ pub fn create_headers(headers: Vec<(String, String)>) -> HeaderMap {
}
header_map
}

/// 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"))
);
}
}
Loading