praxis_protocol/http/pingora/
mod.rs1use std::sync::Arc;
16
17use praxis_core::{
18 PingoraServerRuntime, ProxyError,
19 config::{Config, ProtocolKind},
20};
21
22use crate::{ListenerPipelines, Protocol};
23
24pub mod context;
26pub(crate) mod convert;
27pub(crate) mod grpc_trailers;
29pub mod handler;
31pub mod health;
33#[cfg(feature = "admin-api")]
34pub(crate) mod json;
35#[cfg(feature = "admin-api")]
37pub mod kv;
38pub mod listener;
40pub mod metrics;
42
43pub struct PingoraHttp;
55
56impl Protocol for PingoraHttp {
57 fn register(
58 self: Box<Self>,
59 server: &mut PingoraServerRuntime,
60 config: &Config,
61 pipelines: &ListenerPipelines,
62 ) -> Result<Vec<tokio::sync::watch::Sender<bool>>, ProxyError> {
63 let http_listeners: Vec<_> = config
64 .listeners
65 .iter()
66 .filter(|l| l.protocol == ProtocolKind::Http)
67 .collect();
68
69 if http_listeners.is_empty() {
70 return Ok(Vec::new());
71 }
72
73 let mut cert_watcher_shutdowns = Vec::new();
74 for listener in &http_listeners {
75 let pipeline = pipelines.get(&listener.name).map(Arc::clone).ok_or_else(|| {
76 ProxyError::Config(format!("no pipeline for listener '{name}'", name = listener.name))
77 })?;
78
79 handler::load_http_handler(server.server_mut(), listener, pipeline, &mut cert_watcher_shutdowns)?;
80 }
81
82 Ok(cert_watcher_shutdowns)
83 }
84}
85
86#[cfg(test)]
91#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
92#[allow(
93 clippy::unwrap_used,
94 clippy::expect_used,
95 clippy::indexing_slicing,
96 clippy::panic,
97 clippy::too_many_lines,
98 clippy::use_self,
99 clippy::missing_panics_doc,
100 reason = "tests"
101)]
102mod tests {
103 use std::collections::HashMap;
104
105 use praxis_core::config::{
106 AdminConfig, BodyLimitsConfig, InsecureOptions, Listener, MetricsConfig, RuntimeConfig, TelemetryConfig,
107 };
108 use praxis_filter::{FilterPipeline, FilterRegistry};
109
110 use super::*;
111
112 fn make_config(listeners: Vec<Listener>) -> Config {
114 Config {
115 admin: AdminConfig::default(),
116 body_limits: BodyLimitsConfig::default(),
117 clusters: vec![],
118 filter_chains: vec![],
119 insecure_options: InsecureOptions::default(),
120 listeners,
121 metrics: MetricsConfig::default(),
122 runtime: RuntimeConfig::default(),
123 shutdown_timeout_secs: 30,
124 telemetry: TelemetryConfig::default(),
125 }
126 }
127
128 fn make_listener(name: &str, protocol: ProtocolKind) -> Listener {
130 Listener {
131 name: name.to_owned(),
132 address: "127.0.0.1:8080".to_owned(),
133 protocol,
134 cluster: None,
135 downstream_read_timeout_ms: None,
136 filter_chains: vec![],
137 max_connections: None,
138 tcp_max_duration_secs: None,
139 tcp_session_timeout_ms: None,
140 tls: None,
141 upstream: None,
142 }
143 }
144
145 fn make_pipelines(names: &[&str]) -> ListenerPipelines {
147 let registry = FilterRegistry::with_builtins();
148 let mut map = HashMap::new();
149 for name in names {
150 let pipeline = Arc::new(FilterPipeline::build(&mut [], ®istry).unwrap());
151 map.insert((*name).to_owned(), pipeline);
152 }
153 ListenerPipelines::new(map)
154 }
155
156 #[test]
157 fn register_returns_empty_vec_when_no_http_listeners() {
158 let listeners = vec![
160 make_listener("tcp1", ProtocolKind::Tcp),
161 make_listener("tcp2", ProtocolKind::Tcp),
162 ];
163 let config = make_config(listeners);
164
165 let http_listeners: Vec<_> = config
168 .listeners
169 .iter()
170 .filter(|l| l.protocol == ProtocolKind::Http)
171 .collect();
172
173 assert!(
174 http_listeners.is_empty(),
175 "should have no HTTP listeners when all are TCP"
176 );
177 }
178
179 #[test]
180 fn register_returns_empty_vec_when_no_listeners_at_all() {
181 let config = make_config(vec![]);
182
183 let http_listeners: Vec<_> = config
184 .listeners
185 .iter()
186 .filter(|l| l.protocol == ProtocolKind::Http)
187 .collect();
188
189 assert!(
190 http_listeners.is_empty(),
191 "should have no HTTP listeners in empty config"
192 );
193 }
194
195 #[test]
196 fn register_filters_http_listeners_correctly() {
197 let listeners = vec![
198 make_listener("http1", ProtocolKind::Http),
199 make_listener("tcp1", ProtocolKind::Tcp),
200 make_listener("http2", ProtocolKind::Http),
201 ];
202 let config = make_config(listeners);
203
204 let http_listeners: Vec<_> = config
205 .listeners
206 .iter()
207 .filter(|l| l.protocol == ProtocolKind::Http)
208 .collect();
209
210 assert_eq!(http_listeners.len(), 2, "should have exactly 2 HTTP listeners");
211 assert_eq!(http_listeners[0].name, "http1");
212 assert_eq!(http_listeners[1].name, "http2");
213 }
214
215 #[test]
216 fn register_fails_when_pipeline_missing_for_listener() {
217 let listeners = vec![
218 make_listener("http1", ProtocolKind::Http),
219 make_listener("http2", ProtocolKind::Http),
220 ];
221 let config = make_config(listeners);
222
223 let pipelines = make_pipelines(&["http1"]);
225
226 let listener = &config.listeners[1]; let result = pipelines
229 .get(&listener.name)
230 .ok_or_else(|| ProxyError::Config(format!("no pipeline for listener '{name}'", name = listener.name)));
231
232 assert!(result.is_err(), "should fail when pipeline is missing");
233 if let Err(ProxyError::Config(msg)) = result {
234 assert!(
235 msg.contains("no pipeline for listener"),
236 "error should mention missing pipeline"
237 );
238 assert!(msg.contains("http2"), "error should include listener name");
239 } else {
240 panic!("expected ProxyError::Config");
241 }
242 }
243
244 #[test]
245 fn register_finds_pipeline_for_matching_listener() {
246 let listeners = vec![make_listener("web", ProtocolKind::Http)];
247 let config = make_config(listeners);
248 let pipelines = make_pipelines(&["web"]);
249
250 let listener = &config.listeners[0];
251 let result = pipelines.get(&listener.name);
252
253 assert!(result.is_some(), "should find pipeline for matching listener");
254 }
255
256 #[test]
257 fn register_processes_multiple_http_listeners() {
258 let listeners = vec![
259 make_listener("http1", ProtocolKind::Http),
260 make_listener("http2", ProtocolKind::Http),
261 make_listener("http3", ProtocolKind::Http),
262 ];
263 let config = make_config(listeners);
264 let pipelines = make_pipelines(&["http1", "http2", "http3"]);
265
266 for listener in &config.listeners {
268 assert!(
269 pipelines.get(&listener.name).is_some(),
270 "should have pipeline for {name}",
271 name = listener.name
272 );
273 }
274 }
275
276 #[test]
277 fn register_error_message_format() {
278 let listener_name = "my-http-listener";
279 let error = ProxyError::Config(format!("no pipeline for listener '{listener_name}'"));
280
281 match error {
282 ProxyError::Config(msg) => {
283 assert_eq!(msg, "no pipeline for listener 'my-http-listener'");
284 assert!(msg.starts_with("no pipeline"));
285 },
286 }
287 }
288
289 #[test]
290 fn protocol_kind_http_equality() {
291 assert_eq!(ProtocolKind::Http, ProtocolKind::Http);
292 assert_ne!(ProtocolKind::Http, ProtocolKind::Tcp);
293 }
294
295 #[test]
296 fn pingora_http_can_be_boxed() {
297 let protocol: Box<dyn Protocol> = Box::new(PingoraHttp);
298 drop(protocol);
300 }
301
302 #[test]
303 fn listener_name_used_for_pipeline_lookup() {
304 let listener1 = make_listener("listener-one", ProtocolKind::Http);
305 let listener2 = make_listener("listener-two", ProtocolKind::Http);
306
307 let pipelines = make_pipelines(&["listener-one"]);
308
309 assert!(
310 pipelines.get(&listener1.name).is_some(),
311 "should find pipeline by exact name match"
312 );
313 assert!(
314 pipelines.get(&listener2.name).is_none(),
315 "should not find pipeline for non-existent name"
316 );
317 }
318
319 #[test]
320 fn empty_pipeline_lookup_returns_none() {
321 let pipelines = make_pipelines(&[]);
322 assert!(
323 pipelines.get("any-name").is_none(),
324 "empty pipelines should return None"
325 );
326 }
327
328 #[test]
329 fn protocol_error_config_variant() {
330 let error = ProxyError::Config("test error".to_owned());
331 match error {
332 ProxyError::Config(msg) => assert_eq!(msg, "test error"),
333 }
334 }
335
336 #[test]
337 fn http_listener_filtering_with_mixed_protocols() {
338 let listeners = vec![
339 make_listener("http1", ProtocolKind::Http),
340 make_listener("tcp1", ProtocolKind::Tcp),
341 make_listener("http2", ProtocolKind::Http),
342 make_listener("tcp2", ProtocolKind::Tcp),
343 make_listener("http3", ProtocolKind::Http),
344 ];
345 let config = make_config(listeners);
346
347 let http_listeners: Vec<_> = config
348 .listeners
349 .iter()
350 .filter(|l| l.protocol == ProtocolKind::Http)
351 .collect();
352
353 assert_eq!(http_listeners.len(), 3);
354 assert!(http_listeners.iter().all(|l| l.protocol == ProtocolKind::Http));
355 }
356}