#![forbid(unsafe_code)]
#![warn(missing_docs)]
mod builder;
mod client;
mod config;
mod error;
mod metrics;
mod protocol;
mod proxy;
mod spans;
mod tls;
pub use apollo_configuration::types::HeaderName;
pub use builder::HttpClientBuilder;
pub use client::HttpClient;
pub use config::{
ClientAuthentication, Http1Config, Http2Config, HttpClientConfig, MetricsConfig, Protocol,
ProxyConfig, ProxyUrl, SpansConfig, TcpConfig, TelemetryConfig, TlsConfig,
};
pub use error::HttpClientError;
pub use protocol::HttpBody;
#[cfg(unix)]
pub use protocol::unix::unix_uri;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn config_parses_from_yaml() {
let config: HttpClientConfig = apollo_configuration::parse_yaml(
r#"
connect_timeout: 5s
http1:
pool_max_idle_per_host: 20
pool_idle_timeout: 90s
http2:
connection_idle_timeout: 5m
keep_alive_interval: 30s
keep_alive_while_idle: true
tcp:
nodelay: false
keepalive: 30s
tls:
danger_accept_invalid_certs: false
"#,
&Default::default(),
)
.expect("should parse");
assert_eq!(*config.connect_timeout, std::time::Duration::from_secs(5));
assert_eq!(config.http1.pool_max_idle_per_host.get(), 20);
assert_eq!(
*config.http1.pool_idle_timeout,
std::time::Duration::from_secs(90)
);
assert_eq!(
*config.http2.connection_idle_timeout,
std::time::Duration::from_secs(300)
);
assert_eq!(
*config.http2.keep_alive_interval,
std::time::Duration::from_secs(30)
);
assert!(config.http2.keep_alive_while_idle);
assert!(!config.tcp.nodelay);
assert_eq!(*config.tcp.keepalive, std::time::Duration::from_secs(30));
assert!(!config.tls.danger_accept_invalid_certs);
}
#[test]
fn config_defaults_are_sensible() {
let config = HttpClientConfig::default();
assert_eq!(*config.connect_timeout, std::time::Duration::from_secs(10));
assert_eq!(config.http1.pool_max_idle_per_host.get(), 10);
assert_eq!(
*config.http1.pool_idle_timeout,
std::time::Duration::from_secs(90)
);
assert_eq!(
*config.http2.connection_idle_timeout,
std::time::Duration::from_secs(300)
);
assert_eq!(
*config.http2.keep_alive_interval,
std::time::Duration::from_secs(30)
);
assert!(config.http2.keep_alive_while_idle);
assert!(config.tcp.nodelay);
assert_eq!(*config.tcp.keepalive, std::time::Duration::from_secs(60));
assert!(!config.tls.danger_accept_invalid_certs);
}
#[test]
fn certificate_authorities_is_redacted_in_debug_output() {
let pem = "-----BEGIN CERTIFICATE-----\nSUPERSECRETPEMCONTENTS\n-----END CERTIFICATE-----";
let yaml = format!(
"tls:\n certificate_authorities: |\n {}\n",
pem.replace('\n', "\n "),
);
let config: HttpClientConfig =
apollo_configuration::parse_yaml(&yaml, &Default::default()).expect("valid config");
let debug = format!("{:?}", config.tls.certificate_authorities);
assert!(
!debug.contains("SUPERSECRETPEMCONTENTS"),
"raw PEM leaked through Debug: {debug}"
);
assert!(debug.contains("REDACTED"), "expected REDACTED in {debug}");
}
#[test]
fn client_authentication_fields_are_redacted_in_debug_output() {
let yaml = "\
tls:
client_authentication:
certificate_chain: |
-----BEGIN CERTIFICATE-----
CLIENTCERTSECRET
-----END CERTIFICATE-----
key: |
-----BEGIN PRIVATE KEY-----
CLIENTKEYSECRET
-----END PRIVATE KEY-----
";
let config: HttpClientConfig =
apollo_configuration::parse_yaml(yaml, &Default::default()).expect("valid config");
let auth = config
.tls
.client_authentication
.as_ref()
.expect("client_authentication should parse");
let debug = format!("{:?}", auth);
assert!(
!debug.contains("CLIENTCERTSECRET"),
"raw cert leaked through Debug: {debug}"
);
assert!(
!debug.contains("CLIENTKEYSECRET"),
"raw key leaked through Debug: {debug}"
);
assert!(debug.contains("REDACTED"), "expected REDACTED in {debug}");
}
#[test]
fn json_schema_snapshot() {
insta::assert_json_snapshot!(
apollo_configuration::export_json_schema::<HttpClientConfig>()
);
}
#[test]
fn proxy_config_rejects_unknown_fields() {
let err = apollo_configuration::parse_yaml::<HttpClientConfig>(
r#"
proxy:
url: http://proxy.example.com:3128
typo_field: value
"#,
&Default::default(),
)
.expect_err("unknown fields under `proxy` should be rejected");
let rendered = format!("{err:?}");
assert!(
rendered.contains("typo_field"),
"error should mention the unknown field, got: {rendered}"
);
}
#[test]
fn proxy_config_rejects_invalid_url() {
apollo_configuration::parse_yaml::<HttpClientConfig>(
r#"
proxy:
url: "not-a-url"
"#,
&Default::default(),
)
.expect_err("malformed URL should fail to parse");
}
#[test]
fn proxy_config_rejects_unsupported_scheme() {
let err = apollo_configuration::parse_yaml::<HttpClientConfig>(
r#"
proxy:
url: "socks5://proxy.corp.example.com:1080"
"#,
&Default::default(),
)
.expect_err("non-http(s) proxy scheme should fail validation");
let rendered = format!("{err:?}");
assert!(
rendered.contains("socks5") || rendered.contains("http or https"),
"error should explain the unsupported scheme, got: {rendered}"
);
}
#[test]
fn proxy_config_accepts_credentials_without_exposing_password() {
let config: HttpClientConfig = apollo_configuration::parse_yaml(
r#"
proxy:
url: "http://alice:s3cr3t@proxy.corp.example.com:3128"
"#,
&Default::default(),
)
.expect("valid config");
let proxy = config.proxy.as_ref().expect("proxy set");
let debug = format!("{:?}", proxy.url);
assert!(
!debug.contains("s3cr3t"),
"password must not appear in Debug output: {debug}"
);
assert!(
debug.contains("alice"),
"username is visible in Debug output: {debug}"
);
}
}