use collections::HashMap; use http_client::CustomHeaders; use http_client::http::{HeaderName, HeaderValue}; pub mod anthropic; pub mod anthropic_compatible; pub mod api_compatible; pub mod bedrock; pub mod cloud; pub mod copilot_chat; pub mod deepseek; pub mod google; pub mod llama_cpp; pub mod lmstudio; pub mod mistral; pub mod ollama; pub mod open_ai; pub mod open_ai_compatible; pub mod open_router; pub mod openai_subscribed; pub mod opencode; pub mod vercel_ai_gateway; pub mod x_ai; const COMMON_RESERVED_HEADER_NAMES: &[&str] = &["Authorization", "Content-Type", "Accept"]; /// Validate the user-supplied custom-headers map once at settings load time, /// dropping reserved or malformed entries (each with a `log::warn!`) and /// returning a typed `CustomHeaders` ready to be appended to outgoing requests. pub(crate) fn resolve_custom_headers( provider_name: &str, settings: &HashMap, reserved_header_names: &[&str], ) -> CustomHeaders { let headers = settings .iter() .filter_map(|(name, value)| { if COMMON_RESERVED_HEADER_NAMES .iter() .chain(reserved_header_names) .any(|reserved| reserved.eq_ignore_ascii_case(name)) { log::warn!( "ignoring custom {provider_name} header `{name}`: managed by Zed and cannot be overridden" ); return None; } let header_name = match name.parse::() { Ok(header_name) => header_name, Err(err) => { log::warn!("ignoring custom {provider_name} header `{name}`: invalid header name ({err})"); return None; } }; let header_value = match HeaderValue::from_str(value) { Ok(header_value) => header_value, Err(err) => { log::warn!( "ignoring custom {provider_name} header `{name}`: invalid header value ({err})" ); return None; } }; Some((header_name, header_value)) }) .collect(); CustomHeaders::new(headers) } #[cfg(test)] mod tests { use super::*; fn map(pairs: &[(&str, &str)]) -> HashMap { pairs .iter() .map(|(key, value)| ((*key).to_string(), (*value).to_string())) .collect() } fn names(headers: &CustomHeaders) -> Vec { let mut names: Vec = headers .iter() .map(|(name, _)| name.as_str().to_owned()) .collect(); names.sort(); names } #[test] fn drops_common_and_provider_reserved_headers() { let settings = map(&[ ("Authorization", "Bearer leak"), ("Content-Type", "text/plain"), ("Accept", "text/plain"), ("X-Api-Key", "leak"), ("X-Allowed", "yes"), ]); let merged = resolve_custom_headers("Test", &settings, &["X-Api-Key"]); assert_eq!(names(&merged), vec!["x-allowed".to_string()]); } #[test] fn reserved_header_match_is_case_insensitive() { let settings = map(&[ ("authorization", "Bearer leak"), ("CONTENT-TYPE", "text/plain"), ("x-api-key", "leak"), ("X-Allowed", "yes"), ]); let merged = resolve_custom_headers("Test", &settings, &["X-Api-Key"]); assert_eq!(names(&merged), vec!["x-allowed".to_string()]); } #[test] fn headers_with_reserved_prefix_are_kept() { let settings = map(&[("Authorization-Forwarded", "ok"), ("X-Api-Key-Trace", "ok")]); let merged = resolve_custom_headers("Test", &settings, &["X-Api-Key"]); assert_eq!( names(&merged), vec![ "authorization-forwarded".to_string(), "x-api-key-trace".to_string(), ] ); } #[test] fn drops_invalid_header_name_and_value() { let settings = map(&[ ("Bad Name", "ok"), ("X-Bad-Value", "line1\nline2"), ("X-Allowed", "yes"), ]); let merged = resolve_custom_headers("Test", &settings, &[]); assert_eq!(names(&merged), vec!["x-allowed".to_string()]); } }