1pub mod bundle;
2pub(crate) mod client_cache;
3pub mod config;
4mod header_policy;
5pub mod health;
6#[cfg(test)]
7mod health_wiring_tests;
8pub mod registry;
9pub(crate) mod rest_match;
10pub(crate) mod ssrf;
11pub mod static_config;
12pub mod static_dispatch;
13pub mod static_endpoint;
14pub(crate) mod tls;
15#[cfg(test)]
16mod tls_harness;
17pub(crate) mod tls_reload;
18use crate::config::parse_ok_status_code_range;
19pub use bundle::HttpBundle;
20pub use bundle::HttpStaticBundle;
21pub(crate) use client_cache::{
22 HttpComponentKind, PINNED_CLIENT_MAX_ENTRIES, PINNED_CLIENT_TTL, PinnedClientCache,
23};
24pub use config::HttpConfig;
25pub use health::HttpHealthCheck;
26use health::ListenerGate;
27pub use registry::HttpRouteRegistry;
28pub use static_config::HttpStaticConfig;
29pub use static_endpoint::{HttpStaticComponent, HttpStaticConsumer, HttpStaticEndpoint};
30#[cfg(test)]
31pub(crate) use tls::{
32 FORCE_FALLBACK_REBUILD_FAIL, FORCE_WEBPKI_FALLBACK, build_client_call_count,
33 build_client_fallback_count,
34};
35#[cfg(test)]
36#[allow(unused_imports)]
40pub(crate) use tls::{FallbackTrigger, webpki_fallback_client};
41pub(crate) use tls::{build_client, client_or_emergency, strict_tls_error};
42
43use std::collections::HashMap;
44use std::future::Future;
45use std::pin::Pin;
46
47use std::sync::{Arc, Mutex, OnceLock};
48use std::task::{Context, Poll};
49use std::time::Duration;
50
51use tokio::sync::OnceCell;
52use tower::Layer;
53use tower::Service;
54use tracing::debug;
55
56use axum::body::BodyDataStream;
57use camel_api::component_metadata::ComponentMetadata;
58use camel_auth::bearer_token_layer::BearerTokenLayer;
59use camel_auth::oauth2::TokenProvider;
60use camel_component_api::tls_source::ServerTlsSource;
61use camel_component_api::{Body, BoxProcessor, CamelError, Exchange, StreamBody, StreamMetadata};
62use camel_component_api::{Component, Consumer, Endpoint, ProducerContext, RuntimeObservability};
63use camel_component_api::{UriComponents, UriConfig, parse_uri, raw_query_pairs};
64use futures::StreamExt;
65use futures::TryStreamExt;
66use futures::stream::BoxStream;
67
68#[derive(Clone)]
129pub struct HttpEndpointConfig {
130 pub base_url: String,
131 pub http_method: Option<String>,
132 pub throw_exception_on_failure: bool,
133 pub ok_status_code_range: (u16, u16),
134 pub response_timeout: Option<Duration>,
135 pub query_params: Vec<(String, String)>,
139 pub raw_query: Option<String>,
144 pub allow_internal: bool,
145 pub allow_cleartext: bool,
151 pub blocked_hosts: Vec<String>,
152 pub max_body_size: usize,
153 pub read_timeout_ms: u64,
154 pub max_response_bytes: usize,
155 pub auth: HttpAuth,
156 pub token_provider: Option<Arc<dyn TokenProvider>>,
157 pub user_agent: Option<String>,
158 pub bridge_endpoint: bool,
159 pub connection_close: bool,
160 pub skip_request_headers: Vec<String>,
161 pub skip_response_headers: Vec<String>,
162 pub follow_redirects: bool,
163 pub max_redirects: usize,
164 pub allowed_uri_hosts: Option<Vec<AllowedUriHost>>,
169}
170
171impl std::fmt::Debug for HttpEndpointConfig {
184 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185 f.debug_struct("HttpEndpointConfig")
186 .field("base_url", &camel_api::redact::redact_url(&self.base_url))
187 .field("http_method", &self.http_method)
188 .field(
189 "throw_exception_on_failure",
190 &self.throw_exception_on_failure,
191 )
192 .field("ok_status_code_range", &self.ok_status_code_range)
193 .field("response_timeout", &self.response_timeout)
194 .field(
195 "query_params",
196 &self
197 .query_params
198 .iter()
199 .map(|(key, _)| (key, "***"))
200 .collect::<Vec<_>>(),
201 )
202 .field("raw_query", &self.raw_query.as_ref().map(|_| "?[redacted]"))
203 .field("allow_internal", &self.allow_internal)
204 .field("allow_cleartext", &self.allow_cleartext)
205 .field("blocked_hosts", &self.blocked_hosts)
206 .field("max_body_size", &self.max_body_size)
207 .field("read_timeout_ms", &self.read_timeout_ms)
208 .field("max_response_bytes", &self.max_response_bytes)
209 .field("auth", &self.auth)
210 .field("token_provider", &self.token_provider)
211 .field("user_agent", &self.user_agent)
212 .field("bridge_endpoint", &self.bridge_endpoint)
213 .field("connection_close", &self.connection_close)
214 .field("skip_request_headers", &self.skip_request_headers)
215 .field("skip_response_headers", &self.skip_response_headers)
216 .field("follow_redirects", &self.follow_redirects)
217 .field("max_redirects", &self.max_redirects)
218 .field("allowed_uri_hosts", &self.allowed_uri_hosts)
219 .finish()
220 }
221}
222
223#[derive(Clone, PartialEq)]
224pub enum HttpAuth {
225 None,
226 Basic { username: String, password: String },
227 Bearer { token: String },
228}
229
230impl std::fmt::Debug for HttpAuth {
231 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
232 match self {
233 HttpAuth::None => f.write_str("None"),
234 HttpAuth::Basic { username, .. } => f
235 .debug_struct("Basic")
236 .field("username", username)
237 .field("password", &"***")
238 .finish(),
239 HttpAuth::Bearer { .. } => f.debug_struct("Bearer").field("token", &"***").finish(),
240 }
241 }
242}
243
244fn is_consumed_option(key: &str) -> bool {
253 HttpEndpointConfig::uri_options()
254 .iter()
255 .any(|option| option.name == key || option.aliases.iter().any(|alias| alias == key))
256}
257
258impl UriConfig for HttpEndpointConfig {
259 fn scheme() -> &'static str {
261 "http"
262 }
263
264 fn from_uri(uri: &str) -> Result<Self, CamelError> {
265 let parts = parse_uri(uri)?;
266 Self::from_components(parts)
267 }
268
269 fn from_components(parts: UriComponents) -> Result<Self, CamelError> {
270 if parts.scheme != "http" && parts.scheme != "https" {
272 return Err(CamelError::InvalidUri(format!(
273 "expected scheme 'http' or 'https', got '{}'",
274 parts.scheme
275 )));
276 }
277
278 let base_url = format!("{}:{}", parts.scheme, parts.path);
281
282 let http_method = parts.params.get("httpMethod").cloned();
283
284 let throw_exception_on_failure = match parts.params.get("throwExceptionOnFailure") {
285 Some(v) => parse_bool_param_http(v).map_err(|e| {
286 CamelError::InvalidUri(format!("invalid value for throwExceptionOnFailure: {e}"))
287 })?,
288 None => true,
289 };
290
291 let ok_status_code_range = match parts.params.get("okStatusCodeRange") {
293 Some(v) => parse_ok_status_code_range(v)?,
294 None => (200, 299),
295 };
296
297 let response_timeout = match parts.params.get("responseTimeout") {
298 Some(v) => Some(v.parse::<u64>().map(Duration::from_millis).map_err(|e| {
299 CamelError::InvalidUri(format!("invalid value for responseTimeout: {e}"))
300 })?),
301 None => None,
302 };
303
304 let allow_internal = match parts.params.get("allowInternal") {
306 Some(v) => parse_bool_param_http(v).map_err(|e| {
307 CamelError::InvalidUri(format!("invalid value for allowInternal: {e}"))
308 })?,
309 None => false, };
311
312 let allow_cleartext = match parts.params.get("allowCleartext") {
315 Some(v) => parse_bool_param_http(v).map_err(|e| {
316 CamelError::InvalidUri(format!("invalid value for allowCleartext: {e}"))
317 })?,
318 None => false, };
320
321 let blocked_hosts = parts
323 .params
324 .get("blockedHosts")
325 .map(|v| v.split(',').map(|s| s.trim().to_string()).collect())
326 .unwrap_or_default();
327
328 let max_body_size = match parts.params.get("maxBodySize") {
329 Some(v) => v.parse::<usize>().map_err(|e| {
330 CamelError::InvalidUri(format!("invalid value for maxBodySize: {e}"))
331 })?,
332 None => 10 * 1024 * 1024, };
334
335 let read_timeout_ms = match parts.params.get("readTimeout") {
336 Some(v) => v.parse::<u64>().map_err(|e| {
337 CamelError::InvalidUri(format!("invalid value for readTimeout: {e}"))
338 })?,
339 None => 30_000, };
341
342 let max_response_bytes = match parts.params.get("maxResponseBytes") {
343 Some(v) => v.parse::<usize>().map_err(|e| {
344 CamelError::InvalidUri(format!("invalid value for maxResponseBytes: {e}"))
345 })?,
346 None => 10 * 1024 * 1024, };
348
349 let auth = parse_auth_from_params(&parts.params)?;
350
351 let user_agent = parts.params.get("userAgent").cloned();
352
353 if parts.params.contains_key("cookieHandling") {
354 return Err(CamelError::InvalidUri(
355 "cookieHandling is not supported".into(),
356 ));
357 }
358
359 let bridge_endpoint = match parts.params.get("bridgeEndpoint") {
360 Some(v) => parse_bool_param_http(v).map_err(|e| {
361 CamelError::InvalidUri(format!("invalid value for bridgeEndpoint: {e}"))
362 })?,
363 None => false,
364 };
365
366 let connection_close = match parts.params.get("connectionClose") {
367 Some(v) => parse_bool_param_http(v).map_err(|e| {
368 CamelError::InvalidUri(format!("invalid value for connectionClose: {e}"))
369 })?,
370 None => false,
371 };
372
373 let skip_request_headers = parts
374 .params
375 .get("skipRequestHeaders")
376 .map(|v| {
377 v.split(',')
378 .map(str::trim)
379 .filter(|s| !s.is_empty())
380 .map(|s| s.to_ascii_lowercase())
381 .collect::<Vec<_>>()
382 })
383 .unwrap_or_default();
384
385 let skip_response_headers = parts
386 .params
387 .get("skipResponseHeaders")
388 .map(|v| {
389 v.split(',')
390 .map(str::trim)
391 .filter(|s| !s.is_empty())
392 .map(|s| s.to_ascii_lowercase())
393 .collect::<Vec<_>>()
394 })
395 .unwrap_or_default();
396
397 let follow_redirects = match parts.params.get("followRedirects") {
398 Some(v) => parse_bool_param_http(v).map_err(|e| {
399 CamelError::InvalidUri(format!("invalid value for followRedirects: {e}"))
400 })?,
401 None => false,
402 };
403
404 let max_redirects = match parts.params.get("maxRedirects") {
405 Some(v) => v.parse::<usize>().map_err(|e| {
406 CamelError::InvalidUri(format!("invalid value for maxRedirects: {e}"))
407 })?,
408 None => 10,
409 };
410
411 let allowed_uri_hosts = match parts.params.get("allowedUriHosts") {
414 Some(v) => Some(parse_allowed_uri_hosts(v)?),
415 None => None,
416 };
417
418 let raw_query = parts.raw_query.clone();
423
424 Ok(Self {
425 base_url,
426 http_method,
427 throw_exception_on_failure,
428 ok_status_code_range,
429 response_timeout,
430 query_params: Vec::new(),
431 raw_query,
432 allow_internal,
433 allow_cleartext,
434 blocked_hosts,
435 max_body_size,
436 read_timeout_ms,
437 max_response_bytes,
438 auth,
439 token_provider: None,
440 user_agent,
441 bridge_endpoint,
442 connection_close,
443 skip_request_headers,
444 skip_response_headers,
445 follow_redirects,
446 max_redirects,
447 allowed_uri_hosts,
448 })
449 }
450}
451
452#[derive(Debug, Clone, UriConfig)]
459#[allow(dead_code)]
460#[uri_scheme = "http"]
461#[uri_config(
462 skip_impl,
463 metadata(
464 scheme = "http",
465 description = "HTTP client and server component",
466 producer,
467 consumer,
468 streaming
469 ),
470 crate = "camel_component_api"
471)]
472struct HttpEndpointUriConfig {
473 #[allow(dead_code)]
474 _base_url: String,
475
476 #[uri_param(
477 name = "httpMethod",
478 desc = "HTTP method. Defaults to CamelHttpMethod header or POST/GET"
479 )]
480 http_method: Option<String>,
481
482 #[uri_param(
483 name = "throwExceptionOnFailure",
484 default = "true",
485 desc = "Throw on non-2xx status"
486 )]
487 throw_exception_on_failure: bool,
488
489 #[uri_param(
490 name = "okStatusCodeRange",
491 default = "200-299",
492 desc = "Success status code range"
493 )]
494 ok_status_code_range: String,
495
496 #[uri_param(name = "responseTimeout", desc = "Response timeout in milliseconds")]
497 response_timeout: Option<u64>,
498
499 #[uri_param(
500 name = "connectTimeout",
501 desc = "Connection timeout in milliseconds (consumed option; effective timeout comes from the global http config)"
502 )]
503 connect_timeout: Option<u64>,
504
505 #[uri_param(
506 name = "allowInternal",
507 default = "false",
508 desc = "Allow private/internal network destinations (SSRF)"
509 )]
510 allow_internal: bool,
511
512 #[uri_param(
513 name = "allowCleartext",
514 default = "false",
515 desc = "Allow cleartext http:// to public targets (transport consent; ADR-0081)"
516 )]
517 allow_cleartext: bool,
518
519 #[uri_param(name = "blockedHosts", desc = "Comma-separated blocked host list")]
520 blocked_hosts: Option<String>,
521
522 #[uri_param(
523 name = "maxBodySize",
524 default = "10485760",
525 desc = "Max request/response body bytes"
526 )]
527 max_body_size: u64,
528
529 #[uri_param(name = "readTimeout", desc = "Socket read timeout in milliseconds")]
530 read_timeout: Option<u64>,
531
532 #[uri_param(name = "maxResponseBytes", desc = "Max response body bytes")]
533 max_response_bytes: Option<u64>,
534
535 #[uri_param(
536 name = "authMethod",
537 kind = "enum:None,Basic,Bearer",
538 desc = "Authentication method"
539 )]
540 auth_method: Option<String>,
541
542 #[uri_param(name = "authUsername", secret, desc = "Basic auth username")]
543 auth_username: Option<String>,
544
545 #[uri_param(name = "authPassword", secret, desc = "Basic auth password")]
546 auth_password: Option<String>,
547
548 #[uri_param(name = "authBearerToken", secret, desc = "Bearer auth token")]
549 auth_bearer_token: Option<String>,
550
551 #[uri_param(name = "userAgent", desc = "User-Agent header")]
552 user_agent: Option<String>,
553
554 #[uri_param(
555 name = "bridgeEndpoint",
556 default = "false",
557 desc = "Bridge endpoint mode"
558 )]
559 bridge_endpoint: bool,
560
561 #[uri_param(
562 name = "connectionClose",
563 default = "false",
564 desc = "Send Connection: close"
565 )]
566 connection_close: bool,
567
568 #[uri_param(
569 name = "skipRequestHeaders",
570 desc = "Comma-separated request headers to skip"
571 )]
572 skip_request_headers: Option<String>,
573
574 #[uri_param(
575 name = "skipResponseHeaders",
576 desc = "Comma-separated response headers to skip"
577 )]
578 skip_response_headers: Option<String>,
579
580 #[uri_param(
581 name = "followRedirects",
582 default = "false",
583 desc = "Follow HTTP redirects"
584 )]
585 follow_redirects: bool,
586
587 #[uri_param(name = "maxRedirects", default = "10", desc = "Max redirect hops")]
588 max_redirects: u64,
589
590 #[uri_param(
591 name = "allowedUriHosts",
592 desc = "Comma-separated allowlist of CamelHttpUri override hosts (host or host:port)"
593 )]
594 allowed_uri_hosts: Option<String>,
595}
596
597impl HttpEndpointConfig {
598 pub fn metadata() -> ComponentMetadata {
601 HttpEndpointUriConfig::metadata()
602 }
603
604 pub fn uri_options() -> Vec<camel_api::component_metadata::UriOption> {
606 HttpEndpointUriConfig::uri_options()
607 }
608}
609
610fn parse_auth_from_params(params: &HashMap<String, String>) -> Result<HttpAuth, CamelError> {
611 let Some(method) = params.get("authMethod") else {
612 return Ok(HttpAuth::None);
613 };
614
615 if method.eq_ignore_ascii_case("none") {
616 return Ok(HttpAuth::None);
617 }
618
619 if method.eq_ignore_ascii_case("basic") {
620 let username = params.get("authUsername").cloned().ok_or_else(|| {
621 CamelError::InvalidUri("authUsername is required for authMethod=Basic".to_string())
622 })?;
623 let password = params.get("authPassword").cloned().ok_or_else(|| {
624 CamelError::InvalidUri("authPassword is required for authMethod=Basic".to_string())
625 })?;
626 return Ok(HttpAuth::Basic { username, password });
627 }
628
629 if method.eq_ignore_ascii_case("bearer") {
630 let token = params.get("authBearerToken").cloned().ok_or_else(|| {
631 CamelError::InvalidUri("authBearerToken is required for authMethod=Bearer".to_string())
632 })?;
633 return Ok(HttpAuth::Bearer { token });
634 }
635
636 Err(CamelError::InvalidUri(format!(
637 "invalid value for authMethod: {method} (expected None, Basic, or Bearer)"
638 )))
639}
640
641fn parse_bool_param_http(value: &str) -> Result<bool, CamelError> {
642 match value.to_ascii_lowercase().as_str() {
643 "true" | "1" | "yes" => Ok(true),
644 "false" | "0" | "no" => Ok(false),
645 _ => Err(CamelError::InvalidUri(format!(
646 "invalid boolean value: '{value}'"
647 ))),
648 }
649}
650
651impl HttpEndpointConfig {
652 pub fn from_uri_with_defaults(uri: &str, config: &HttpConfig) -> Result<Self, CamelError> {
653 let parts = parse_uri(uri)?;
654 let mut endpoint = Self::from_components(parts.clone())?;
655 if endpoint.response_timeout.is_none() {
656 endpoint.response_timeout = Some(Duration::from_millis(config.response_timeout_ms));
657 }
658 if !parts.params.contains_key("allowInternal") {
659 endpoint.allow_internal = config.allow_internal;
660 }
661 if !parts.params.contains_key("blockedHosts") {
662 endpoint.blocked_hosts = config.blocked_hosts.clone();
663 }
664 if !parts.params.contains_key("maxBodySize") {
665 endpoint.max_body_size = config.max_body_size;
666 }
667 if !parts.params.contains_key("readTimeout") {
668 endpoint.read_timeout_ms = config.read_timeout_ms;
669 }
670 if !parts.params.contains_key("maxResponseBytes") {
671 endpoint.max_response_bytes = config.max_response_bytes;
672 }
673 if !parts.params.contains_key("okStatusCodeRange")
674 && let Some(range) = &config.ok_status_code_range
675 {
676 endpoint.ok_status_code_range = parse_ok_status_code_range(range)?;
677 }
678 if !parts.params.contains_key("followRedirects") {
679 endpoint.follow_redirects = config.follow_redirects;
680 }
681 if !parts.params.contains_key("maxRedirects") {
682 endpoint.max_redirects = config.max_redirects.unwrap_or(10);
683 }
684
685 Ok(endpoint)
686 }
687}
688
689#[derive(Debug, Clone)]
695pub struct HttpServerConfig {
696 pub scheme: String,
698 pub host: String,
700 pub port: u16,
702 pub path: String,
704 pub max_request_body: usize,
706 pub max_response_body: usize,
708 pub max_inflight_requests: usize,
714 pub method: Option<String>,
721 pub tls_config: Option<crate::config::ServerTlsConfig>,
724}
725
726impl UriConfig for HttpServerConfig {
727 fn scheme() -> &'static str {
729 "http"
730 }
731
732 fn from_uri(uri: &str) -> Result<Self, CamelError> {
733 let parts = parse_uri(uri)?;
734 Self::from_components(parts)
735 }
736
737 fn from_components(parts: UriComponents) -> Result<Self, CamelError> {
738 if parts.scheme != "http" && parts.scheme != "https" {
740 return Err(CamelError::InvalidUri(format!(
741 "expected scheme 'http' or 'https', got '{}'",
742 parts.scheme
743 )));
744 }
745
746 let authority_and_path = parts.path.trim_start_matches('/');
749
750 let (authority, path_suffix) = if let Some(idx) = authority_and_path.find('/') {
752 (&authority_and_path[..idx], &authority_and_path[idx..])
753 } else {
754 (authority_and_path, "/")
755 };
756
757 let path = if path_suffix.is_empty() {
758 "/"
759 } else {
760 path_suffix
761 }
762 .to_string();
763
764 let (host, port) = if let Some(colon) = authority.rfind(':') {
766 let port_str = &authority[colon + 1..];
767 match port_str.parse::<u16>() {
768 Ok(p) => (authority[..colon].to_string(), p),
769 Err(_) => {
770 return Err(CamelError::InvalidUri(format!(
771 "invalid port '{}' in authority",
772 port_str
773 )));
774 }
775 }
776 } else {
777 let default_port = if parts.scheme == "https" { 443 } else { 80 };
779 (authority.to_string(), default_port)
780 };
781
782 let max_request_body = parts
783 .params
784 .get("maxRequestBody")
785 .and_then(|v| v.parse::<usize>().ok())
786 .unwrap_or(2 * 1024 * 1024); let max_response_body = parts
789 .params
790 .get("maxResponseBody")
791 .and_then(|v| v.parse::<usize>().ok())
792 .unwrap_or(10 * 1024 * 1024); let max_inflight_requests = parts
795 .params
796 .get("maxInflightRequests")
797 .and_then(|v| v.parse::<usize>().ok())
798 .unwrap_or(1024);
799 let max_inflight_requests = max_inflight_requests_limit(max_inflight_requests)?;
803
804 let method = parts.params.get("httpMethod").map(|m| m.to_uppercase());
810
811 Ok(Self {
812 scheme: parts.scheme,
813 host,
814 port,
815 path,
816 max_request_body,
817 max_response_body,
818 max_inflight_requests,
819 method,
820 tls_config: {
821 let cert = parts.params.get("tlsCert").cloned();
822 let key = parts.params.get("tlsKey").cloned();
823 match (cert, key) {
824 (Some(c), Some(k)) => Some(crate::config::ServerTlsConfig {
825 cert_path: c,
826 key_path: k,
827 }),
828 (None, None) => None,
829 _ => None, }
831 },
832 })
833 }
834}
835
836impl HttpServerConfig {
837 pub fn from_uri_with_defaults(uri: &str, config: &HttpConfig) -> Result<Self, CamelError> {
838 let parts = parse_uri(uri)?;
839 let mut server = Self::from_components(parts.clone())?;
840 if !parts.params.contains_key("maxRequestBody") {
841 server.max_request_body = config.max_request_body;
842 }
843 if !parts.params.contains_key("maxResponseBody") {
844 server.max_response_body = config.max_body_size;
846 }
847 Ok(server)
848 }
849}
850
851pub enum HttpReplyBody {
859 Bytes(bytes::Bytes),
860 Stream(BoxStream<'static, Result<bytes::Bytes, CamelError>>),
861}
862
863pub struct RequestEnvelope {
868 pub method: String,
869 pub path: String,
870 pub query: String,
871 pub headers: http::HeaderMap,
872 pub body: StreamBody,
873 pub path_params: std::collections::HashMap<String, String>,
879 pub reply_tx: tokio::sync::oneshot::Sender<HttpReply>,
880}
881
882pub struct HttpReply {
886 pub status: u16,
887 pub headers: Vec<(String, String)>,
888 pub body: HttpReplyBody,
889}
890
891type ServerKey = (String, u16);
896
897struct ServerHandle {
899 registry: HttpRouteRegistry,
900 bound_addr: std::net::SocketAddr,
903 max_request_body: usize,
904 max_response_body: usize,
905 max_inflight_requests: usize,
906 is_tls: bool,
907 tls_cert_path: Option<String>,
908 tls_key_path: Option<String>,
909 monitor_task: tokio::task::JoinHandle<()>,
912 #[allow(dead_code)]
918 server_abort: tokio::task::AbortHandle,
919 tls_config: Option<axum_server::tls_rustls::RustlsConfig>,
922 tls_source: Option<ServerTlsSource>,
923}
924
925#[derive(Default)]
928struct RegistryState {
929 entries: HashMap<ServerKey, Arc<OnceCell<ServerHandle>>>,
930 staged: HashMap<ServerKey, tokio::net::TcpListener>,
931}
932
933pub struct ServerRegistry {
935 inner: Mutex<RegistryState>,
936}
937
938impl ServerRegistry {
939 pub fn global() -> &'static Self {
941 static INSTANCE: OnceLock<ServerRegistry> = OnceLock::new();
942 INSTANCE.get_or_init(|| ServerRegistry {
943 inner: Mutex::new(RegistryState::default()),
944 })
945 }
946
947 #[allow(clippy::too_many_arguments)]
950 pub async fn get_or_spawn(
951 &'static self,
952 host: &str,
953 port: u16,
954 max_request_body: usize,
955 max_response_body: usize,
956 max_inflight_requests: usize,
957 runtime: Arc<dyn RuntimeObservability>,
958 route_id: String,
959 tls_config: Option<crate::config::ServerTlsConfig>,
960 ) -> Result<HttpRouteRegistry, CamelError> {
961 self.get_or_spawn_internal(
962 host,
963 port,
964 max_request_body,
965 max_response_body,
966 max_inflight_requests,
967 runtime,
968 route_id,
969 tls_config,
970 None,
971 )
972 .await
973 }
974
975 #[allow(clippy::too_many_arguments)]
982 pub async fn get_or_spawn_with_listener(
983 &'static self,
984 listener: tokio::net::TcpListener,
985 max_request_body: usize,
986 max_response_body: usize,
987 max_inflight_requests: usize,
988 runtime: Arc<dyn RuntimeObservability>,
989 route_id: String,
990 tls_config: Option<crate::config::ServerTlsConfig>,
991 ) -> Result<HttpRouteRegistry, CamelError> {
992 let addr = listener
993 .local_addr()
994 .map_err(|e| CamelError::EndpointCreationFailed(format!("listener local_addr: {e}")))?;
995 self.get_or_spawn_internal(
996 &addr.ip().to_string(),
997 addr.port(),
998 max_request_body,
999 max_response_body,
1000 max_inflight_requests,
1001 runtime,
1002 route_id,
1003 tls_config,
1004 Some(listener),
1005 )
1006 .await
1007 }
1008
1009 pub async fn stage_listener(
1016 &'static self,
1017 listener: tokio::net::TcpListener,
1018 ) -> Result<(), CamelError> {
1019 let addr = listener
1020 .local_addr()
1021 .map_err(|e| CamelError::EndpointCreationFailed(format!("listener local_addr: {e}")))?;
1022 let host = addr.ip().to_string();
1023 use std::collections::hash_map::Entry;
1024 let mut guard = self.inner.lock().map_err(|_| {
1025 CamelError::EndpointCreationFailed("ServerRegistry lock poisoned".into())
1026 })?;
1027 match guard.staged.entry((host.clone(), addr.port())) {
1028 Entry::Occupied(_) => Err(CamelError::EndpointCreationFailed(format!(
1029 "listener already staged for {host}:{}",
1030 addr.port()
1031 ))),
1032 Entry::Vacant(slot) => {
1033 slot.insert(listener);
1034 Ok(())
1035 }
1036 }
1037 }
1038
1039 pub fn bound_addr(&'static self, host: &str, port: u16) -> Option<std::net::SocketAddr> {
1042 let guard = self.inner.lock().ok()?;
1043 guard
1044 .entries
1045 .get(&(host.to_string(), port))
1046 .and_then(|cell| cell.get())
1047 .map(|handle| handle.bound_addr)
1048 }
1049
1050 #[allow(clippy::too_many_arguments)]
1051 async fn get_or_spawn_internal(
1052 &'static self,
1053 host: &str,
1054 port: u16,
1055 max_request_body: usize,
1056 max_response_body: usize,
1057 max_inflight_requests: usize,
1058 runtime: Arc<dyn RuntimeObservability>,
1059 route_id: String,
1060 tls_config: Option<crate::config::ServerTlsConfig>,
1061 provided: Option<tokio::net::TcpListener>,
1062 ) -> Result<HttpRouteRegistry, CamelError> {
1063 let host_owned = host.to_string();
1064 let key = (host.to_string(), port);
1065
1066 let cell = {
1067 let mut guard = self.inner.lock().map_err(|_| {
1068 CamelError::EndpointCreationFailed("ServerRegistry lock poisoned".into())
1069 })?;
1070 if let Some(existing) = guard.entries.get(&key)
1074 && let Some(handle) = existing.get()
1075 && handle.monitor_task.is_finished()
1076 {
1077 if handle.is_tls {
1080 let scheme = if handle.is_tls { "https" } else { "http" };
1081 camel_component_api::tls_source::TlsReloadRegistry::global()
1082 .unregister(scheme, host, port);
1083 }
1084 guard.entries.remove(&key);
1085 }
1086 guard
1087 .entries
1088 .entry(key)
1089 .or_insert_with(|| Arc::new(OnceCell::new()))
1090 .clone()
1091 };
1092
1093 if let Some(existing) = cell.get()
1094 && existing.max_request_body != max_request_body
1095 {
1096 return Err(CamelError::EndpointCreationFailed(format!(
1097 "incompatible maxRequestBody for shared server (host={host}, port={port}): {} vs {}",
1098 existing.max_request_body, max_request_body
1099 )));
1100 }
1101
1102 if let Some(existing) = cell.get()
1103 && existing.max_response_body != max_response_body
1104 {
1105 return Err(CamelError::EndpointCreationFailed(format!(
1106 "incompatible maxResponseBody for shared server (host={host}, port={port}): {} vs {}",
1107 existing.max_response_body, max_response_body
1108 )));
1109 }
1110
1111 if let Some(existing) = cell.get()
1112 && existing.max_inflight_requests != max_inflight_requests
1113 {
1114 return Err(CamelError::EndpointCreationFailed(format!(
1115 "incompatible maxInflightRequests for shared server (host={host}, port={port}): {} vs {}",
1116 existing.max_inflight_requests, max_inflight_requests
1117 )));
1118 }
1119
1120 if let Some(existing) = cell.get()
1122 && existing.is_tls != tls_config.is_some()
1123 {
1124 return Err(CamelError::EndpointCreationFailed(format!(
1125 "incompatible TLS mode for shared server (host={host}, port={port}): existing is_tls={}, new has_tls={}",
1126 existing.is_tls,
1127 tls_config.is_some()
1128 )));
1129 }
1130
1131 if let (Some(existing), Some(new_tls)) = (cell.get(), &tls_config)
1133 && (existing.tls_cert_path.as_deref() != Some(&new_tls.cert_path)
1134 || existing.tls_key_path.as_deref() != Some(&new_tls.key_path))
1135 {
1136 return Err(CamelError::EndpointCreationFailed(format!(
1137 "incompatible TLS cert/key for shared server (host={host}, port={port}): routes on the same TLS port must use the same cert and key"
1138 )));
1139 }
1140
1141 let handle = cell
1142 .get_or_try_init(|| {
1143 let rt = Arc::clone(&runtime);
1144 let rid = route_id.clone();
1145 let key = (host_owned.clone(), port);
1146 async move {
1147 let source = match provided {
1156 Some(listener) => ListenerSource::Staged(listener),
1157 None => {
1158 let mut guard = self.inner.lock().map_err(|_| {
1159 CamelError::EndpointCreationFailed(
1160 "ServerRegistry lock poisoned".into(),
1161 )
1162 })?;
1163 match guard.staged.remove(&key) {
1164 Some(listener) => ListenerSource::Staged(listener),
1165 None => {
1169 if let Some((staged_host, _)) = guard
1170 .staged
1171 .keys()
1172 .find(|(_, staged_port)| *staged_port == port)
1173 {
1174 let staged_host = staged_host.clone();
1175 return Err(CamelError::EndpointCreationFailed(
1176 format!(
1177 "staged listener conflict on port {port}: staged under host {staged_host}, requested {host_owned}"
1178 ),
1179 ));
1180 }
1181 ListenerSource::Bind
1182 }
1183 }
1184 }
1185 };
1186 spawn_entry(
1187 key,
1188 source,
1189 max_request_body,
1190 max_response_body,
1191 max_inflight_requests,
1192 rt,
1193 rid,
1194 tls_config,
1195 )
1196 .await
1197 .and_then(|handle| {
1198 Arc::try_unwrap(handle).map_err(|_| {
1202 CamelError::EndpointCreationFailed(
1203 "spawned server handle has dangling clones".into(),
1204 )
1205 })
1206 })
1207 }
1208 })
1209 .await?;
1210
1211 Ok(handle.registry.clone())
1212 }
1213
1214 pub async fn unregister(&self, host: &str, port: u16) {
1218 debug!(
1219 host = camel_api::redact::redact_host(host),
1220 port = port,
1221 "consumer unregistered from HTTP server"
1222 );
1223 }
1224
1225 #[cfg(test)]
1232 pub fn reset() {
1233 let instance = Self::global();
1234 let mut guard = instance
1235 .inner
1236 .lock()
1237 .expect("ServerRegistry lock poisoned during test reset");
1238 guard.entries.clear();
1239 guard.staged.clear();
1240 }
1241}
1242
1243enum ListenerSource {
1246 Bind,
1247 Staged(tokio::net::TcpListener),
1248}
1249
1250#[allow(clippy::too_many_arguments)]
1255async fn spawn_entry(
1256 key: ServerKey,
1257 source: ListenerSource,
1258 max_request_body: usize,
1259 max_response_body: usize,
1260 max_inflight_requests: usize,
1261 runtime: Arc<dyn RuntimeObservability>,
1262 route_id: String,
1263 tls_config: Option<crate::config::ServerTlsConfig>,
1264) -> Result<Arc<ServerHandle>, CamelError> {
1265 max_inflight_requests_limit(max_inflight_requests)?;
1271 let rt = Arc::clone(&runtime);
1272 let rid = route_id.clone();
1273 let (host_owned, port) = key;
1274 let listener = match source {
1275 ListenerSource::Bind => {
1276 let addr = format!("{host_owned}:{port}");
1277 tokio::net::TcpListener::bind(&addr).await.map_err(|e| {
1278 CamelError::EndpointCreationFailed(format!("Failed to bind {addr}: {e}"))
1279 })?
1280 }
1281 ListenerSource::Staged(listener) => listener,
1282 };
1283 let bound_addr = listener
1284 .local_addr()
1285 .map_err(|e| CamelError::EndpointCreationFailed(format!("listener local_addr: {e}")))?;
1286 let server_exited = tokio_util::sync::CancellationToken::new();
1287 let registry = HttpRouteRegistry::new_with_server_exited(server_exited.clone());
1288 let inflight = Arc::new(tokio::sync::Semaphore::new(max_inflight_requests));
1289 let tls_rustls_cfg: Option<axum_server::tls_rustls::RustlsConfig>;
1292 let tls_source: Option<ServerTlsSource>;
1293 let server_task = if let Some(ref tls) = tls_config {
1294 let rustls_config = load_tls_config(&tls.cert_path, &tls.key_path)?;
1295 let source = ServerTlsSource {
1296 cert_path: std::path::PathBuf::from(&tls.cert_path),
1297 key_path: std::path::PathBuf::from(&tls.key_path),
1298 client_ca_path: None,
1299 };
1300 let rustls_cfg =
1304 axum_server::tls_rustls::RustlsConfig::from_config(std::sync::Arc::new(rustls_config));
1305 tls_rustls_cfg = Some(rustls_cfg.clone());
1306 tls_source = Some(source);
1307 let std_listener = listener.into_std().map_err(|e| {
1309 CamelError::EndpointCreationFailed(format!("TLS listener conversion: {e}"))
1310 })?;
1311 tokio::spawn(run_axum_server_tls(
1312 std_listener,
1313 rustls_cfg,
1314 registry.clone(),
1315 max_request_body,
1316 max_response_body,
1317 Arc::clone(&inflight),
1318 Arc::clone(&rt),
1319 rid.clone(),
1320 ))
1321 } else {
1322 tls_rustls_cfg = None;
1323 tls_source = None;
1324 tokio::spawn(run_axum_server(
1325 listener,
1326 registry.clone(),
1327 max_request_body,
1328 max_response_body,
1329 Arc::clone(&inflight),
1330 Arc::clone(&rt),
1331 rid.clone(),
1332 ))
1333 };
1334 let addr_for_monitor = format!("{host_owned}:{port}");
1335 let server_abort = server_task.abort_handle();
1336 let monitor_task = tokio::spawn(monitor_axum_task(
1337 server_task,
1338 addr_for_monitor,
1339 Arc::clone(&rt),
1340 rid,
1341 server_exited,
1342 ));
1343 let handle = ServerHandle {
1344 registry,
1345 bound_addr,
1346 max_request_body,
1347 max_response_body,
1348 max_inflight_requests,
1349 is_tls: tls_config.is_some(),
1350 tls_cert_path: tls_config.as_ref().map(|t| t.cert_path.clone()),
1351 tls_key_path: tls_config.as_ref().map(|t| t.key_path.clone()),
1352 monitor_task,
1353 server_abort,
1354 tls_config: tls_rustls_cfg,
1355 tls_source,
1356 };
1357 if let (Some(tls_cfg), Some(source)) = (handle.tls_config.as_ref(), handle.tls_source.as_ref())
1362 {
1363 let handler = Arc::new(crate::tls_reload::HttpReloadHandler::new(
1364 tls_cfg.clone(),
1365 source.clone(),
1366 host_owned.clone(),
1367 port,
1368 ));
1369 camel_component_api::tls_source::TlsReloadRegistry::global().register(handler);
1370 }
1371 Ok(Arc::new(handle))
1372}
1373
1374use axum::{
1379 Router,
1380 body::Body as AxumBody,
1381 extract::{Request, State},
1382 http::{Response, StatusCode},
1383 response::IntoResponse,
1384};
1385
1386#[derive(Clone)]
1387pub(crate) struct AppState {
1388 registry: HttpRouteRegistry,
1389 max_request_body: usize,
1390 max_response_body: usize,
1391 inflight: Arc<tokio::sync::Semaphore>,
1392}
1393
1394const CONSUMER_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
1401
1402async fn run_axum_server(
1403 listener: tokio::net::TcpListener,
1404 registry: HttpRouteRegistry,
1405 max_request_body: usize,
1406 max_response_body: usize,
1407 inflight: Arc<tokio::sync::Semaphore>,
1408 runtime: Arc<dyn RuntimeObservability>,
1409 route_id: String,
1410) {
1411 let state = AppState {
1412 registry,
1413 max_request_body,
1414 max_response_body,
1415 inflight,
1416 };
1417 let app = Router::new()
1418 .fallback(dispatch_handler)
1419 .with_state(state)
1420 .layer(tower_http::timeout::TimeoutLayer::with_status_code(
1421 StatusCode::REQUEST_TIMEOUT,
1422 CONSUMER_REQUEST_TIMEOUT,
1423 ));
1424
1425 axum::serve(listener, app).await.unwrap_or_else(|e| {
1426 runtime
1427 .metrics()
1428 .increment_errors(&route_id, "e:http:accept");
1429 tracing::error!(error = %e, "Axum server error");
1431 });
1432}
1433
1434#[allow(clippy::too_many_arguments)]
1435async fn run_axum_server_tls(
1436 listener: std::net::TcpListener,
1437 tls_cfg: axum_server::tls_rustls::RustlsConfig,
1438 registry: HttpRouteRegistry,
1439 max_request_body: usize,
1440 max_response_body: usize,
1441 inflight: Arc<tokio::sync::Semaphore>,
1442 runtime: Arc<dyn RuntimeObservability>,
1443 route_id: String,
1444) {
1445 let state = AppState {
1446 registry,
1447 max_request_body,
1448 max_response_body,
1449 inflight,
1450 };
1451 let app = Router::new()
1452 .fallback(dispatch_handler)
1453 .with_state(state)
1454 .layer(tower_http::timeout::TimeoutLayer::with_status_code(
1455 StatusCode::REQUEST_TIMEOUT,
1456 CONSUMER_REQUEST_TIMEOUT,
1457 ));
1458
1459 let server = match axum_server::from_tcp_rustls(listener, tls_cfg) {
1464 Ok(server) => server,
1465 Err(e) => {
1466 runtime
1467 .metrics()
1468 .increment_errors(&route_id, "e:http:accept-tls");
1469 tracing::error!(error = %e, "Axum TLS server setup error");
1471 return;
1472 }
1473 };
1474
1475 server
1476 .serve(app.into_make_service())
1477 .await
1478 .unwrap_or_else(|e| {
1479 runtime
1480 .metrics()
1481 .increment_errors(&route_id, "e:http:accept-tls");
1482 tracing::error!(error = %e, "Axum TLS server error");
1484 });
1485}
1486
1487async fn monitor_axum_task(
1497 handle: tokio::task::JoinHandle<()>,
1498 addr: String,
1499 runtime: Arc<dyn RuntimeObservability>,
1500 route_id: String,
1501 server_exited: tokio_util::sync::CancellationToken,
1502) {
1503 match handle.await {
1504 Ok(()) => {
1505 }
1507 Err(join_err) => {
1508 runtime
1509 .metrics()
1510 .increment_errors(&route_id, "e:http:server-task-exited");
1511 tracing::error!(
1513 addr = %addr,
1514 error = %join_err,
1515 "Axum server task exited unexpectedly — all routes on this port are now dead"
1516 );
1517 server_exited.cancel();
1521 }
1522 }
1523}
1524
1525fn load_tls_config(
1528 cert_path: &str,
1529 key_path: &str,
1530) -> Result<tokio_rustls::rustls::ServerConfig, CamelError> {
1531 use std::fs::File;
1532 use std::io::BufReader;
1533
1534 let cert_file = File::open(cert_path)
1535 .map_err(|e| CamelError::EndpointCreationFailed(format!("TLS cert file error: {e}")))?;
1536 let key_file = File::open(key_path)
1537 .map_err(|e| CamelError::EndpointCreationFailed(format!("TLS key file error: {e}")))?;
1538
1539 let certs: Vec<_> = rustls_pemfile::certs(&mut BufReader::new(cert_file))
1540 .collect::<Result<Vec<_>, _>>()
1541 .map_err(|e| CamelError::EndpointCreationFailed(format!("TLS cert parse error: {e}")))?;
1542
1543 let key = rustls_pemfile::private_key(&mut BufReader::new(key_file))
1544 .map_err(|e| CamelError::EndpointCreationFailed(format!("TLS key parse error: {e}")))?
1545 .ok_or_else(|| CamelError::EndpointCreationFailed("TLS: no private key found".into()))?;
1546
1547 tokio_rustls::rustls::ServerConfig::builder()
1548 .with_no_client_auth()
1549 .with_single_cert(certs, key)
1550 .map_err(|e| CamelError::EndpointCreationFailed(format!("TLS config error: {e}")))
1551}
1552
1553async fn dispatch_handler(State(state): State<AppState>, req: Request) -> impl IntoResponse {
1554 let path = req.uri().path().to_owned();
1555 let method = req.method().to_string();
1556
1557 let api_sender = {
1574 let inner = state.registry.inner.read().await;
1575 inner.api_routes.get(&path).cloned()
1576 }; let (rest_sender, path_params) = if api_sender.is_some() {
1579 (None, Default::default())
1581 } else {
1582 let inner = state.registry.inner.read().await;
1583 match rest_match::match_endpoint(&method, &path, &inner.rest_endpoints) {
1584 rest_match::MatchOutcome::Found(m) => (Some(m.payload), m.path_params),
1585 rest_match::MatchOutcome::Ambiguous => {
1586 tracing::warn!(
1592 method = %method,
1593 path = %path,
1594 "ambiguous REST template match — returning 500"
1595 );
1596 return Response::builder()
1597 .status(StatusCode::INTERNAL_SERVER_ERROR)
1598 .body(AxumBody::from("Internal Server Error"))
1599 .expect("infallible"); }
1601 rest_match::MatchOutcome::NotFound => (None, Default::default()),
1602 }
1603 }; let sender = api_sender.or(rest_sender);
1606
1607 if let Some(sender) = sender {
1608 let query = req.uri().query().unwrap_or("").to_string();
1609 let headers = req.headers().clone();
1610
1611 let content_length: Option<u64> = headers
1613 .get(http::header::CONTENT_LENGTH)
1614 .and_then(|v| v.to_str().ok())
1615 .and_then(|s| s.parse().ok());
1616
1617 if let Some(len) = content_length
1618 && len > state.max_request_body as u64
1619 {
1620 return Response::builder()
1621 .status(StatusCode::PAYLOAD_TOO_LARGE)
1622 .body(AxumBody::from("Request body exceeds configured limit"))
1623 .expect("infallible"); }
1625
1626 let _permit = match Arc::clone(&state.inflight).try_acquire_owned() {
1627 Ok(permit) => permit,
1628 Err(_) => {
1629 return Response::builder()
1630 .status(StatusCode::SERVICE_UNAVAILABLE)
1631 .body(AxumBody::from("Service Unavailable"))
1632 .expect("infallible"); }
1634 };
1635
1636 let content_type = headers
1642 .get(http::header::CONTENT_TYPE)
1643 .and_then(|v| v.to_str().ok())
1644 .map(|s| s.to_string());
1645
1646 let data_stream: BodyDataStream = req.into_body().into_data_stream();
1647 let max_body = state.max_request_body;
1648 let mut seen: u64 = 0;
1649 let capped_stream =
1650 data_stream
1651 .map_err(|e| CamelError::Io(e.to_string()))
1652 .map(move |chunk| match chunk {
1653 Ok(bytes) => {
1654 seen = seen.saturating_add(bytes.len() as u64);
1655 if seen > max_body as u64 {
1656 Err(CamelError::ProcessorError(format!(
1657 "Request body exceeds configured limit of {max_body} bytes"
1658 )))
1659 } else {
1660 Ok(bytes)
1661 }
1662 }
1663 Err(e) => Err(e),
1664 });
1665 let boxed: BoxStream<'static, Result<bytes::Bytes, CamelError>> = Box::pin(capped_stream);
1666
1667 let stream_body = StreamBody {
1668 stream: Arc::new(tokio::sync::Mutex::new(Some(boxed))),
1669 metadata: StreamMetadata {
1670 size_hint: content_length,
1671 content_type,
1672 origin: None,
1673 },
1674 };
1675
1676 let (reply_tx, reply_rx) = tokio::sync::oneshot::channel::<HttpReply>();
1677 let envelope = RequestEnvelope {
1678 method,
1679 path,
1680 query,
1681 headers,
1682 body: stream_body,
1683 path_params,
1684 reply_tx,
1685 };
1686
1687 if sender.send(envelope).await.is_err() {
1688 return Response::builder()
1689 .status(StatusCode::SERVICE_UNAVAILABLE)
1690 .body(AxumBody::from("Consumer unavailable"))
1691 .expect("infallible"); }
1693
1694 match reply_rx.await {
1695 Ok(reply) => {
1696 let reply = match reply.body {
1697 HttpReplyBody::Bytes(b)
1698 if exceeds_max_response_body(b.len(), state.max_response_body) =>
1699 {
1700 HttpReply {
1701 status: 500,
1702 headers: vec![],
1703 body: HttpReplyBody::Bytes(bytes::Bytes::from(
1704 "Response body exceeds configured limit",
1705 )),
1706 }
1707 }
1708 _ => reply,
1709 };
1710
1711 let status =
1712 StatusCode::from_u16(reply.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
1713 let mut builder = Response::builder().status(status);
1714 for (k, v) in &reply.headers {
1715 builder = builder.header(k.as_str(), v.as_str());
1716 }
1717 match reply.body {
1718 HttpReplyBody::Bytes(b) => {
1719 builder.body(AxumBody::from(b)).unwrap_or_else(|_| {
1720 Response::builder()
1721 .status(StatusCode::INTERNAL_SERVER_ERROR)
1722 .body(AxumBody::from("Invalid response headers from consumer"))
1723 .expect("infallible") })
1725 }
1726 HttpReplyBody::Stream(stream) => builder
1727 .body(AxumBody::from_stream(stream))
1728 .unwrap_or_else(|_| {
1729 Response::builder()
1730 .status(StatusCode::INTERNAL_SERVER_ERROR)
1731 .body(AxumBody::from("Invalid response headers from consumer"))
1732 .expect("infallible") }),
1734 }
1735 }
1736 Err(_) => Response::builder()
1737 .status(StatusCode::INTERNAL_SERVER_ERROR)
1738 .body(AxumBody::from("Pipeline error"))
1739 .expect("infallible"), }
1741 } else {
1742 static_dispatch::dispatch_static(&state, req, &path).await
1744 }
1745}
1746
1747fn exceeds_max_response_body(len: usize, max: usize) -> bool {
1748 len > max
1749}
1750
1751fn title_case_header(name: &str) -> String {
1752 name.split('-')
1753 .map(|part| {
1754 let mut chars = part.chars();
1755 match chars.next() {
1756 None => String::new(),
1757 Some(first) => first.to_uppercase().chain(chars.as_str().chars()).collect(),
1758 }
1759 })
1760 .collect::<Vec<_>>()
1761 .join("-")
1762}
1763
1764pub(crate) struct HttpKernelAuth {
1779 pub(crate) plan: camel_api::security_policy::RouteSecurityPlan,
1780 pub(crate) providers: Arc<camel_auth::ProviderRegistry>,
1781}
1782
1783impl HttpKernelAuth {
1784 pub(crate) fn from_security_context(
1789 ctx: &camel_component_api::SecurityContext,
1790 ) -> Option<Self> {
1791 Some(Self {
1792 plan: ctx.plan.clone()?,
1793 providers: ctx.providers.clone()?,
1794 })
1795 }
1796}
1797
1798fn envelope_channel_capacity(max_inflight_requests: usize) -> usize {
1812 max_inflight_requests.max(1)
1813}
1814
1815pub(crate) fn max_inflight_requests_limit(configured: usize) -> Result<usize, CamelError> {
1830 if configured > tokio::sync::Semaphore::MAX_PERMITS {
1831 return Err(CamelError::Config(format!(
1832 "maxInflightRequests {configured} exceeds the supported upper bound {} (tokio::sync::Semaphore::MAX_PERMITS)",
1833 tokio::sync::Semaphore::MAX_PERMITS
1834 )));
1835 }
1836 Ok(configured)
1837}
1838
1839pub struct HttpConsumer {
1840 config: HttpServerConfig,
1841 runtime: Arc<dyn RuntimeObservability>,
1843 kernel: Option<Arc<HttpKernelAuth>>,
1847}
1848
1849impl HttpConsumer {
1850 pub fn new(config: HttpServerConfig, runtime: Arc<dyn RuntimeObservability>) -> Self {
1851 Self {
1852 config,
1853 runtime,
1854 kernel: None,
1855 }
1856 }
1857}
1858
1859#[async_trait::async_trait]
1860impl Consumer for HttpConsumer {
1861 async fn start(&mut self, ctx: camel_component_api::ConsumerContext) -> Result<(), CamelError> {
1862 use camel_component_api::{Body, Exchange, Message};
1863
1864 max_inflight_requests_limit(self.config.max_inflight_requests)?;
1869
1870 let registry = ServerRegistry::global()
1871 .get_or_spawn(
1872 &self.config.host,
1873 self.config.port,
1874 self.config.max_request_body,
1875 self.config.max_response_body,
1876 self.config.max_inflight_requests,
1877 self.runtime.clone(),
1878 ctx.route_id().to_string(),
1879 self.config.tls_config.clone(),
1880 )
1881 .await?;
1882
1883 let (env_tx, mut env_rx) = tokio::sync::mpsc::channel::<RequestEnvelope>(
1887 envelope_channel_capacity(self.config.max_inflight_requests),
1888 );
1889 if let Some(method) = self.config.method.clone() {
1896 let segments = rest_match::parse_path_template(&self.config.path);
1897 registry
1898 .register_rest_endpoint(method, segments, env_tx)
1899 .await?;
1900 } else {
1901 registry
1902 .register_api_route(self.config.path.clone(), env_tx)
1903 .await?;
1904 }
1905
1906 ctx.mark_ready();
1915
1916 let in_flight = ctx.in_flight_counter();
1920
1921 let path = self.config.path.clone();
1922 let registry_for_cleanup = registry.clone();
1923 let server_exited = registry.server_exited.clone();
1924 let cancel_token = ctx.cancel_token();
1925 let kernel = self.kernel.clone();
1926 let mut server_died = false;
1931 loop {
1932 tokio::select! {
1933 _ = ctx.cancelled() => {
1934 break;
1935 }
1936 _ = server_exited.cancelled() => {
1937 server_died = true;
1942 break;
1943 }
1944 envelope = env_rx.recv() => {
1945 let Some(envelope) = envelope else { break; };
1946
1947 let claim =
1955 in_flight.as_ref().map(camel_component_api::InFlightClaim::attach);
1956
1957 let mut msg = Message::default();
1959
1960 msg.set_header("CamelHttpMethod",
1962 serde_json::Value::String(envelope.method.clone()));
1963 msg.set_header("CamelHttpPath",
1964 serde_json::Value::String(envelope.path.clone()));
1965 msg.set_header("CamelHttpQuery",
1966 serde_json::Value::String(envelope.query.clone()));
1967
1968 for (param_name, param_value) in &envelope.path_params {
1975 msg.set_header(
1976 format!("CamelHttpPath_{param_name}"),
1977 serde_json::Value::String(param_value.clone()),
1978 );
1979 }
1980
1981 for (k, v) in &envelope.headers {
1983 if let Ok(val_str) = v.to_str() {
1984 msg.set_header(
1985 title_case_header(k.as_str()),
1986 serde_json::Value::String(val_str.to_string()),
1987 );
1988 }
1989 }
1990
1991 msg.body = Body::Stream(envelope.body);
1994
1995 #[allow(unused_mut)]
1996 let mut exchange = Exchange::new(msg);
1997
1998 #[cfg(feature = "otel")]
2000 {
2001 let headers: HashMap<String, String> = envelope
2002 .headers
2003 .iter()
2004 .filter_map(|(k, v)| {
2005 Some((k.as_str().to_lowercase(), v.to_str().ok()?.to_string()))
2006 })
2007 .collect();
2008 camel_otel::extract_into_exchange(&mut exchange, &headers);
2009 }
2010
2011 let reply_tx = envelope.reply_tx;
2012 let sender = ctx.sender().clone();
2013 let path_clone = path.clone();
2014 let cancel = cancel_token.clone();
2015 let auth_headers = envelope.headers.clone();
2019 let auth_uri: http::Uri = {
2020 let full = if envelope.query.is_empty() {
2021 envelope.path.clone()
2022 } else {
2023 format!("{}?{}", envelope.path, envelope.query)
2024 };
2025 full.parse().unwrap_or_default()
2029 };
2030 let kernel = kernel.clone();
2031
2032 tokio::spawn(async move {
2052 if cancel.is_cancelled() {
2060 let _ = reply_tx.send(HttpReply {
2061 status: 503,
2062 headers: vec![],
2063 body: HttpReplyBody::Bytes(bytes::Bytes::from("Service Unavailable")),
2064 });
2065 return;
2066 }
2067
2068 if let Some(kernel) = kernel.as_ref()
2077 && !matches!(
2078 kernel.plan.access_mode,
2079 camel_api::security_policy::AccessMode::Public
2080 )
2081 {
2082 let principal = match camel_auth::extract_token_multi(
2083 &auth_headers,
2084 &auth_uri,
2085 &kernel.plan.credential_sources,
2086 ) {
2087 Some(extracted) => {
2088 match camel_auth::kernel_authenticate(
2089 &kernel.plan,
2090 &kernel.providers,
2091 &extracted,
2092 )
2093 .await
2094 {
2095 Ok(principal) => principal,
2096 Err(e) => {
2097 tracing::warn!(
2099 path = %path_clone,
2100 error = %e,
2101 "HTTP request authentication failed"
2102 );
2103 let _ = reply_tx.send(pipeline_error_to_reply(
2104 e,
2105 &path_clone,
2106 ));
2107 return;
2108 }
2109 }
2110 }
2111 None => {
2112 tracing::warn!(
2114 path = %path_clone,
2115 "HTTP request rejected: no credential found in any source"
2116 );
2117 let _ = reply_tx.send(pipeline_error_to_reply(
2118 CamelError::Unauthenticated(
2119 "no credential found in any source".to_string(),
2120 ),
2121 &path_clone,
2122 ));
2123 return;
2124 }
2125 };
2126 camel_auth::install_carrier(&mut exchange, &principal);
2127 }
2128
2129 let (tx, rx) = tokio::sync::oneshot::channel();
2131 let envelope = camel_component_api::consumer::ExchangeEnvelope {
2132 exchange,
2133 reply_tx: Some(tx),
2134 in_flight_claim: claim,
2140 };
2141
2142 let result = match sender.send(envelope).await {
2143 Ok(()) => rx.await.map_err(|_| camel_component_api::CamelError::ChannelClosed),
2144 Err(_) => Err(camel_component_api::CamelError::ChannelClosed),
2145 }
2146 .and_then(|r| r);
2147
2148 let reply = match result {
2149 Ok(out) => {
2150 let status = out
2151 .input
2152 .header("CamelHttpResponseCode")
2153 .and_then(|v| {
2154 let raw = v.as_u64()
2155 .or_else(|| v.as_str().and_then(|s| s.parse().ok()))?;
2156 let code = raw as u16;
2157 (100..1000).contains(&code).then_some(code)
2158 })
2159 .unwrap_or(200);
2160
2161 let user_content_type = out
2162 .input
2163 .header("Content-Type")
2164 .and_then(|v| v.as_str().map(|s| s.to_string()));
2165
2166 let (reply_body, inferred_content_type): (HttpReplyBody, Option<String>) = match out.input.body {
2167 Body::Bytes(b) => (HttpReplyBody::Bytes(b), None),
2168 Body::Text(s) => (HttpReplyBody::Bytes(bytes::Bytes::from(s.into_bytes())), Some("text/plain; charset=utf-8".to_string())),
2169 Body::Xml(s) => (HttpReplyBody::Bytes(bytes::Bytes::from(s.into_bytes())), Some("application/xml".to_string())),
2170 Body::Json(v) => (HttpReplyBody::Bytes(bytes::Bytes::from(
2171 v.to_string().into_bytes(),
2172 )), Some("application/json".to_string())),
2173 Body::Stream(s) => {
2174 let ct = s.metadata.content_type.clone();
2175 match s.stream.lock().await.take() {
2176 Some(stream) => (
2177 HttpReplyBody::Stream(stream),
2178 ct,
2179 ),
2180 None => {
2181 tracing::error!(
2183 "Body::Stream already consumed before HTTP reply — returning 500"
2184 );
2185 let error_reply = HttpReply {
2186 status: 500,
2187 headers: vec![],
2188 body: HttpReplyBody::Bytes(bytes::Bytes::new()),
2189 };
2190 if reply_tx.send(error_reply).is_err() {
2191 debug!("reply_tx dropped before error reply could be sent");
2192 }
2193 return;
2194 }
2195 }
2196 }
2197 _ => (HttpReplyBody::Bytes(bytes::Bytes::new()), None),
2199 };
2200
2201 let resp_headers = select_response_headers(
2202 &out.input.headers,
2203 user_content_type,
2204 inferred_content_type,
2205 );
2206
2207 HttpReply {
2208 status,
2209 headers: resp_headers,
2210 body: reply_body,
2211 }
2212 }
2213 Err(e) => {
2214 pipeline_error_to_reply(e, &path_clone)
2215 }
2216 };
2217
2218 let _ = reply_tx.send(reply);
2220 });
2221 }
2222 }
2223 }
2224
2225 if let Some(method) = &self.config.method {
2230 registry_for_cleanup
2231 .unregister_rest_endpoint(method, &path)
2232 .await;
2233 } else {
2234 registry_for_cleanup.unregister_api_route(&path).await;
2235 }
2236
2237 ServerRegistry::global()
2243 .unregister(&self.config.host, self.config.port)
2244 .await;
2245
2246 if server_died {
2247 tracing::error!(
2249 host = %camel_api::redact::redact_host(&self.config.host),
2250 port = self.config.port,
2251 path = %path,
2252 "Shared HTTP server exited — failing consumer to engage route supervision (ADR-0007)"
2253 );
2254 return Err(CamelError::RouteError(format!(
2258 "shared HTTP server for {}:{} exited unexpectedly; route transport is dead",
2259 camel_api::redact::redact_host(&self.config.host),
2260 self.config.port
2261 )));
2262 }
2263
2264 Ok(())
2265 }
2266
2267 async fn stop(&mut self) -> Result<(), CamelError> {
2268 Ok(())
2269 }
2270
2271 fn concurrency_model(&self) -> camel_component_api::ConcurrencyModel {
2272 camel_component_api::ConcurrencyModel::Concurrent { max: None }
2273 }
2274
2275 fn startup_mode(&self) -> camel_component_api::ConsumerStartupMode {
2282 camel_component_api::ConsumerStartupMode::Explicit
2283 }
2284
2285 fn set_security_context(&mut self, ctx: camel_component_api::SecurityContext) {
2288 self.kernel = HttpKernelAuth::from_security_context(&ctx).map(Arc::new);
2289 }
2290}
2291
2292pub struct HttpComponent {
2297 config: HttpConfig,
2298 pinned_cache: std::sync::Arc<PinnedClientCache>,
2299 client: reqwest::Client,
2300 strict_tls_error: Option<CamelError>,
2304}
2305
2306impl HttpComponent {
2307 pub fn new() -> Self {
2308 let config = HttpConfig::default();
2309 let strict_err = strict_tls_error(&config);
2310 let (client, build_err) = client_or_emergency(&config);
2311 Self {
2312 client,
2313 config,
2314 pinned_cache: std::sync::Arc::new(PinnedClientCache::new(
2315 PINNED_CLIENT_TTL,
2316 PINNED_CLIENT_MAX_ENTRIES,
2317 )),
2318 strict_tls_error: strict_err.or(build_err),
2319 }
2320 }
2321
2322 pub fn with_config(config: HttpConfig) -> Self {
2323 let strict_err = strict_tls_error(&config);
2324 let (client, build_err) = client_or_emergency(&config);
2325 Self {
2326 client,
2327 config,
2328 pinned_cache: std::sync::Arc::new(PinnedClientCache::new(
2329 PINNED_CLIENT_TTL,
2330 PINNED_CLIENT_MAX_ENTRIES,
2331 )),
2332 strict_tls_error: strict_err.or(build_err),
2333 }
2334 }
2335
2336 pub fn with_optional_config(config: Option<HttpConfig>) -> Self {
2337 match config {
2338 Some(cfg) => Self::with_config(cfg),
2339 None => Self::new(),
2340 }
2341 }
2342}
2343
2344impl Default for HttpComponent {
2345 fn default() -> Self {
2346 Self::new()
2347 }
2348}
2349
2350impl Component for HttpComponent {
2351 fn scheme(&self) -> &str {
2352 "http"
2353 }
2354
2355 fn metadata(&self) -> ComponentMetadata {
2356 HttpEndpointConfig::metadata()
2357 }
2358
2359 fn create_endpoint(
2360 &self,
2361 uri: &str,
2362 ctx: &dyn camel_component_api::ComponentContext,
2363 ) -> Result<Box<dyn Endpoint>, CamelError> {
2364 if let Some(err) = &self.strict_tls_error {
2365 return Err(err.clone());
2366 }
2367 self.config.validate()?;
2368 let config = HttpEndpointConfig::from_uri_with_defaults(uri, &self.config)?;
2369 let server_config = HttpServerConfig::from_uri_with_defaults(uri, &self.config)?;
2370 let listener_gate = ListenerGate::default();
2372 ctx.register_current_route_health_check(Arc::new(HttpHealthCheck::gated(
2373 server_config.host.clone(),
2374 server_config.port,
2375 listener_gate.clone(),
2376 )));
2377 self.pinned_cache
2378 .wire(HttpComponentKind::Http, ctx.metrics());
2379 Ok(Box::new(HttpEndpoint {
2380 uri: uri.to_string(),
2381 config,
2382 server_config,
2383 client: self.client.clone(),
2384 pinned_cache: std::sync::Arc::clone(&self.pinned_cache),
2385 http_config: self.config.clone(),
2386 listener_gate,
2387 }))
2388 }
2389}
2390
2391pub struct HttpsComponent {
2392 config: HttpConfig,
2393 pinned_cache: std::sync::Arc<PinnedClientCache>,
2394 client: reqwest::Client,
2395 strict_tls_error: Option<CamelError>,
2399}
2400
2401impl HttpsComponent {
2402 pub fn new() -> Self {
2403 let config = HttpConfig::default();
2404 let strict_err = strict_tls_error(&config);
2405 let (client, build_err) = client_or_emergency(&config);
2406 Self {
2407 client,
2408 config,
2409 pinned_cache: std::sync::Arc::new(PinnedClientCache::new(
2410 PINNED_CLIENT_TTL,
2411 PINNED_CLIENT_MAX_ENTRIES,
2412 )),
2413 strict_tls_error: strict_err.or(build_err),
2414 }
2415 }
2416
2417 pub fn with_config(config: HttpConfig) -> Self {
2418 let strict_err = strict_tls_error(&config);
2419 let (client, build_err) = client_or_emergency(&config);
2420 Self {
2421 client,
2422 config,
2423 pinned_cache: std::sync::Arc::new(PinnedClientCache::new(
2424 PINNED_CLIENT_TTL,
2425 PINNED_CLIENT_MAX_ENTRIES,
2426 )),
2427 strict_tls_error: strict_err.or(build_err),
2428 }
2429 }
2430
2431 pub fn with_optional_config(config: Option<HttpConfig>) -> Self {
2432 match config {
2433 Some(cfg) => Self::with_config(cfg),
2434 None => Self::new(),
2435 }
2436 }
2437}
2438
2439impl Default for HttpsComponent {
2440 fn default() -> Self {
2441 Self::new()
2442 }
2443}
2444
2445impl Component for HttpsComponent {
2446 fn scheme(&self) -> &str {
2447 "https"
2448 }
2449
2450 fn metadata(&self) -> ComponentMetadata {
2451 let mut meta = HttpEndpointConfig::metadata();
2454 meta.scheme = "https".to_string();
2455 meta.description = "HTTPS client and server component (TLS over HTTP)".to_string();
2456 meta
2457 }
2458
2459 fn create_endpoint(
2460 &self,
2461 uri: &str,
2462 ctx: &dyn camel_component_api::ComponentContext,
2463 ) -> Result<Box<dyn Endpoint>, CamelError> {
2464 if let Some(err) = &self.strict_tls_error {
2465 return Err(err.clone());
2466 }
2467 self.config.validate()?;
2468 let config = HttpEndpointConfig::from_uri_with_defaults(uri, &self.config)?;
2469 let server_config = HttpServerConfig::from_uri_with_defaults(uri, &self.config)?;
2470 let listener_gate = ListenerGate::default();
2472 ctx.register_current_route_health_check(Arc::new(HttpHealthCheck::gated(
2473 server_config.host.clone(),
2474 server_config.port,
2475 listener_gate.clone(),
2476 )));
2477 self.pinned_cache
2478 .wire(HttpComponentKind::Https, ctx.metrics());
2479 Ok(Box::new(HttpEndpoint {
2480 uri: uri.to_string(),
2481 config,
2482 server_config,
2483 client: self.client.clone(),
2484 pinned_cache: std::sync::Arc::clone(&self.pinned_cache),
2485 http_config: self.config.clone(),
2486 listener_gate,
2487 }))
2488 }
2489}
2490
2491struct HttpEndpoint {
2496 uri: String,
2497 config: HttpEndpointConfig,
2498 server_config: HttpServerConfig,
2499 client: reqwest::Client,
2500 pinned_cache: std::sync::Arc<PinnedClientCache>,
2501 http_config: HttpConfig,
2502 listener_gate: ListenerGate,
2504}
2505
2506impl Endpoint for HttpEndpoint {
2507 fn uri(&self) -> &str {
2508 &self.uri
2509 }
2510
2511 fn create_consumer(
2512 &self,
2513 rt: Arc<dyn camel_component_api::RuntimeObservability>,
2514 ) -> Result<Box<dyn Consumer>, CamelError> {
2515 let scheme_is_https = self.server_config.scheme == "https";
2518 let has_tls = self.server_config.tls_config.is_some();
2519
2520 if scheme_is_https && !has_tls {
2521 return Err(CamelError::EndpointCreationFailed(
2522 "https:// consumer requires tlsCert and tlsKey parameters".to_string(),
2523 ));
2524 }
2525 if !scheme_is_https && has_tls {
2526 return Err(CamelError::EndpointCreationFailed(
2527 "http:// is incompatible with tlsCert/tlsKey — use https:// for TLS".to_string(),
2528 ));
2529 }
2530 max_inflight_requests_limit(self.server_config.max_inflight_requests)?;
2535 self.listener_gate.open();
2536 Ok(Box::new(HttpConsumer::new(self.server_config.clone(), rt)))
2537 }
2538
2539 fn create_producer(
2540 &self,
2541 rt: Arc<dyn camel_component_api::RuntimeObservability>,
2542 _ctx: &ProducerContext,
2543 ) -> Result<BoxProcessor, CamelError> {
2544 let producer = HttpProducer {
2545 config: Arc::new(self.config.clone()),
2546 client: self.client.clone(),
2547 pinned_cache: std::sync::Arc::clone(&self.pinned_cache),
2548 http_config: Arc::new(self.http_config.clone()),
2549 runtime: rt,
2550 };
2551 if let Some(ref provider) = self.config.token_provider {
2552 let layer = BearerTokenLayer::new(Arc::clone(provider));
2553 Ok(BoxProcessor::new(layer.layer(producer)))
2554 } else {
2555 Ok(BoxProcessor::new(producer))
2556 }
2557 }
2558}
2559
2560#[derive(Clone)]
2565struct HttpProducer {
2566 config: Arc<HttpEndpointConfig>,
2567 client: reqwest::Client,
2568 pinned_cache: std::sync::Arc<PinnedClientCache>,
2569 http_config: Arc<HttpConfig>,
2570 runtime: Arc<dyn RuntimeObservability>,
2576}
2577
2578impl HttpProducer {
2579 fn resolve_method(exchange: &Exchange, config: &HttpEndpointConfig) -> String {
2580 if let Some(ref method) = config.http_method {
2581 return method.to_uppercase();
2582 }
2583 if let Some(method) = exchange
2584 .input
2585 .header("CamelHttpMethod")
2586 .and_then(|v| v.as_str())
2587 {
2588 return method.to_uppercase();
2589 }
2590 if !exchange.input.body.is_empty() {
2591 return "POST".to_string();
2592 }
2593 "GET".to_string()
2594 }
2595
2596 fn resolve_url(exchange: &Exchange, config: &HttpEndpointConfig) -> Result<String, CamelError> {
2597 if config.bridge_endpoint {
2606 let Some(query) = resolve_endpoint_query(config)? else {
2607 return Ok(config.base_url.clone());
2608 };
2609 let _: url::Url = url::Url::parse(&config.base_url).map_err(|e| {
2617 CamelError::ProcessorError(format!(
2618 "invalid base URL '{}': {e}",
2619 redact_url_for_diagnostics(&config.base_url)
2620 ))
2621 })?;
2622 let mut url = config.base_url.clone();
2623 url.push('?');
2624 url.push_str(&query);
2625 return Ok(url);
2626 }
2627
2628 if let Some(uri) = exchange
2629 .input
2630 .header("CamelHttpUri")
2631 .and_then(|v| v.as_str())
2632 {
2633 if let Some(fence) = &config.allowed_uri_hosts
2638 && !uri_host_allowed(uri, fence)?
2639 {
2640 return Err(CamelError::ProcessorError(format!(
2641 "CamelHttpUri host not allowed by allowedUriHosts fence: {}",
2642 redact_url_for_diagnostics(uri)
2643 )));
2644 }
2645 let (base, override_query) = match uri.split_once('?') {
2652 Some((base, query)) => (base, Some(query)),
2653 None => (uri, None),
2654 };
2655 if let Some(query) = override_query {
2661 for (_key, span) in raw_query_pairs(query)? {
2662 validate_raw_query_span(span)?;
2663 }
2664 }
2665 let mut url = base.to_string();
2666 if let Some(path) = exchange
2667 .input
2668 .header("CamelHttpPath")
2669 .and_then(|v| v.as_str())
2670 {
2671 if !url.ends_with('/') && !path.starts_with('/') {
2672 url.push('/');
2673 }
2674 url.push_str(path);
2675 }
2676 if let Some(query) = exchange
2677 .input
2678 .header("CamelHttpQuery")
2679 .and_then(|v| v.as_str())
2680 {
2681 if let Some(merged) = merge_header_query(override_query, query)? {
2682 url.push('?');
2683 url.push_str(&merged);
2684 }
2685 return Ok(url);
2686 }
2687 if let Some(query) = override_query {
2688 url.push('?');
2689 url.push_str(query);
2690 }
2691 return Ok(url);
2692 }
2693
2694 let mut url = config.base_url.clone();
2695
2696 if let Some(path) = exchange
2697 .input
2698 .header("CamelHttpPath")
2699 .and_then(|v| v.as_str())
2700 {
2701 if !url.ends_with('/') && !path.starts_with('/') {
2702 url.push('/');
2703 }
2704 url.push_str(path);
2705 }
2706
2707 if let Some(query) = exchange
2708 .input
2709 .header("CamelHttpQuery")
2710 .and_then(|v| v.as_str())
2711 {
2712 if let Some(merged) =
2717 merge_header_query(resolve_endpoint_query(config)?.as_deref(), query)?
2718 {
2719 url.push('?');
2720 url.push_str(&merged);
2721 }
2722 return Ok(url);
2723 }
2724
2725 if let Some(query) = resolve_endpoint_query(config)? {
2726 url.push('?');
2727 url.push_str(&query);
2728 }
2729
2730 Ok(url)
2731 }
2732
2733 fn is_ok_status(status: u16, range: (u16, u16)) -> bool {
2734 status >= range.0 && status <= range.1
2735 }
2736}
2737
2738#[derive(Clone, Debug, PartialEq, Eq)]
2743pub struct AllowedUriHost {
2744 pub host: String,
2746 pub port: Option<u16>,
2748}
2749
2750fn parse_allowed_uri_hosts(raw: &str) -> Result<Vec<AllowedUriHost>, CamelError> {
2758 let mut entries = Vec::new();
2759 for segment in raw.split(',') {
2760 let segment = segment.trim();
2761 if segment.is_empty() {
2762 continue;
2763 }
2764 let parsed = url::Url::parse(&format!("http://{segment}"))
2765 .map_err(|_| invalid_allowed_uri_host_entry(segment))?;
2766 if parsed.path() != "/" || !parsed.username().is_empty() || parsed.password().is_some() {
2770 return Err(invalid_allowed_uri_host_entry(segment));
2771 }
2772 let Some(host) = parsed.host_str() else {
2773 return Err(invalid_allowed_uri_host_entry(segment));
2774 };
2775 entries.push(AllowedUriHost {
2776 host: host.to_string(),
2777 port: parsed.port(),
2778 });
2779 }
2780 if entries.is_empty() {
2781 return Err(CamelError::InvalidUri(
2782 "allowedUriHosts declares no valid host entries".to_string(),
2783 ));
2784 }
2785 Ok(entries)
2786}
2787
2788fn invalid_allowed_uri_host_entry(segment: &str) -> CamelError {
2789 CamelError::InvalidUri(format!("invalid allowedUriHosts entry '{segment}'"))
2790}
2791
2792pub(crate) fn uri_host_allowed(
2799 url_str: &str,
2800 fence: &[AllowedUriHost],
2801) -> Result<bool, CamelError> {
2802 let Ok(parsed) = url::Url::parse(url_str) else {
2803 return Ok(false);
2804 };
2805 let Some(host) = parsed.host_str() else {
2806 return Ok(false);
2807 };
2808 let effective_port = parsed.port().or(match parsed.scheme() {
2809 "https" => Some(443_u16),
2810 "http" => Some(80),
2811 _ => None,
2812 });
2813 Ok(fence.iter().any(|entry| {
2814 entry.host == host
2815 && match entry.port {
2816 None => true,
2817 Some(port) => effective_port == Some(port),
2818 }
2819 }))
2820}
2821
2822fn resolve_endpoint_query(config: &HttpEndpointConfig) -> Result<Option<String>, CamelError> {
2835 let mut parts: Vec<String> = Vec::new();
2836 let mut authored_keys = std::collections::HashSet::new();
2837
2838 if let Some(raw) = config.raw_query.as_deref() {
2839 for (key, span) in raw_query_pairs(raw)? {
2840 authored_keys.insert(key.clone());
2841 if is_consumed_option(&key) {
2842 continue;
2843 }
2844 validate_raw_query_span(span)?;
2845 parts.push(span.to_string());
2846 }
2847 }
2848
2849 for (key, value) in &config.query_params {
2850 if !authored_keys.contains(key.as_str()) {
2851 parts.push(format!(
2852 "{}={}",
2853 encode_query_component(key),
2854 encode_query_component(value)
2855 ));
2856 }
2857 }
2858
2859 if parts.is_empty() && config.raw_query.as_deref() != Some("") {
2860 return Ok(None);
2861 }
2862 Ok(Some(parts.join("&")))
2863}
2864
2865fn merge_header_query(
2874 higher_precedence: Option<&str>,
2875 header_query: &str,
2876) -> Result<Option<String>, CamelError> {
2877 if header_query.is_empty() {
2878 return Ok(higher_precedence.map(str::to_string));
2879 }
2880 let mut parts: Vec<String> = Vec::new();
2881 let mut higher_keys = std::collections::HashSet::new();
2882 for (key, span) in raw_query_pairs(higher_precedence.unwrap_or(""))? {
2883 higher_keys.insert(key);
2884 parts.push(span.to_string());
2885 }
2886 for (key, span) in raw_query_pairs(header_query)? {
2887 validate_raw_query_span(span)?;
2888 if !higher_keys.contains(key.as_str()) {
2889 parts.push(span.to_string());
2890 }
2891 }
2892 if parts.is_empty() {
2893 return Ok(None);
2894 }
2895 Ok(Some(parts.join("&")))
2896}
2897
2898fn is_legal_query_byte(byte: u8) -> bool {
2909 matches!(byte,
2910 b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z'
2911 | b'-' | b'.' | b'_' | b'~'
2912 | b'!' | b'$' | b'&' | b'(' | b')' | b'*' | b'+' | b',' | b';' | b'='
2913 | b':' | b'@' | b'/' | b'?'
2914 | b'%')
2915}
2916
2917fn validate_raw_query_span(span: &str) -> Result<(), CamelError> {
2923 for &byte in span.as_bytes() {
2924 if !is_legal_query_byte(byte) {
2925 return Err(CamelError::ProcessorError(format!(
2926 "raw query pair '{span}' contains byte 0x{byte:02X}, which is not legal in a URL query component"
2927 )));
2928 }
2929 }
2930 Ok(())
2931}
2932
2933fn encode_query_component(component: &str) -> String {
2937 const HEX: &[u8; 16] = b"0123456789ABCDEF";
2938 let mut out = String::with_capacity(component.len());
2939 for &byte in component.as_bytes() {
2940 match byte {
2941 b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'-' | b'.' | b'_' | b'~' => {
2942 out.push(byte as char);
2943 }
2944 _ => {
2945 out.push('%');
2946 out.push(HEX[(byte >> 4) as usize] as char);
2947 out.push(HEX[(byte & 0x0f) as usize] as char);
2948 }
2949 }
2950 }
2951 out
2952}
2953
2954pub(crate) fn redact_url_for_diagnostics(raw: &str) -> String {
2982 match url::Url::parse(raw) {
2983 Ok(mut u) => {
2984 if !u.cannot_be_a_base()
2991 && u.host_str().is_none()
2992 && camel_api::redact::window_has_at_sign(raw)
2993 {
2994 return "[redacted]".to_string();
2995 }
2996 if !u.username().is_empty() || u.password().is_some() {
2997 let _ = u.set_username("***");
2998 let _ = u.set_password(None);
2999 }
3000 let s = u.to_string();
3003 camel_api::redact::redact_url(&s)
3004 }
3005 Err(_) => camel_api::redact::redact_url_fail_closed(raw),
3006 }
3007}
3008
3009const MAX_ERROR_RESPONSE_BODY_BYTES: usize = 4096;
3014
3015fn truncate_error_body(body: &[u8]) -> String {
3016 if body.len() <= MAX_ERROR_RESPONSE_BODY_BYTES {
3017 String::from_utf8_lossy(body).into_owned()
3018 } else {
3019 let mut s = String::from_utf8_lossy(&body[..MAX_ERROR_RESPONSE_BODY_BYTES]).into_owned();
3020 s.push_str("...[truncated]");
3021 s
3022 }
3023}
3024
3025impl HttpProducer {
3026 fn is_entity_enclosing(method: &str) -> bool {
3031 matches!(method, "POST" | "PUT" | "PATCH")
3032 }
3033}
3034
3035impl Service<Exchange> for HttpProducer {
3036 type Response = Exchange;
3037 type Error = CamelError;
3038 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
3039
3040 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
3041 Poll::Ready(Ok(()))
3042 }
3043
3044 fn call(&mut self, exchange: Exchange) -> Self::Future {
3045 let config = self.config.clone();
3046 let shared_client = self.client.clone();
3047 let pinned_cache = std::sync::Arc::clone(&self.pinned_cache);
3048 let http_config = self.http_config.clone();
3049 let component_metrics = self.runtime.component_metrics();
3050
3051 Box::pin(async move {
3052 let mut exchange = exchange;
3053 let outcome = async {
3054 let method_str = HttpProducer::resolve_method(&exchange, &config);
3055 let suppress_body = !HttpProducer::is_entity_enclosing(&method_str);
3060 let url = HttpProducer::resolve_url(&exchange, &config)?;
3061
3062 ssrf::validate_url_for_ssrf(&url, &config)?;
3064
3065 let resolved = ssrf::resolve_initial_url_for_ssrf(
3073 &url,
3074 config.allow_internal,
3075 config.allow_cleartext,
3076 )
3077 .await?;
3078 let client: reqwest::Client = if let Some((ref host, ref addrs)) = resolved {
3079 pinned_cache
3083 .get_or_build(host.as_str(), addrs, || {
3084 build_client(&http_config, Some((host.as_str(), addrs)))
3085 })
3086 .await?
3087 } else {
3088 shared_client.clone()
3089 };
3090
3091 debug!(
3092 correlation_id = %exchange.correlation_id(),
3093 method = %method_str,
3094 url = %redact_url_for_diagnostics(&url),
3095 "HTTP request"
3096 );
3097
3098 let method = method_str.parse::<reqwest::Method>().map_err(|e| {
3099 CamelError::ProcessorError(format!(
3100 "Invalid HTTP method '{}': {}",
3101 method_str, e
3102 ))
3103 })?;
3104
3105 let mut collected_headers: Vec<(
3107 reqwest::header::HeaderName,
3108 reqwest::header::HeaderValue,
3109 )> = Vec::new();
3110
3111 if let Some(user_agent) = &config.user_agent
3112 && !config.bridge_endpoint
3113 {
3114 match constructed_header("user-agent", user_agent) {
3115 Ok((_, val)) => {
3116 collected_headers.push((reqwest::header::USER_AGENT, val));
3117 }
3118 Err(drop) => debug!(
3119 correlation_id = %exchange.correlation_id(),
3120 header = %drop.name,
3121 "outbound header dropped: {}",
3122 drop.reason
3123 ),
3124 }
3125 }
3126
3127 #[cfg(feature = "otel")]
3129 let should_inject_otel = !config.bridge_endpoint;
3130 #[cfg(feature = "otel")]
3131 if should_inject_otel {
3132 let mut otel_headers = HashMap::new();
3133 camel_otel::inject_from_exchange(&exchange, &mut otel_headers);
3134 for (k, v) in otel_headers {
3135 match constructed_header(&k, &v) {
3136 Ok((name, val)) => collected_headers.push((name, val)),
3137 Err(drop) => debug!(
3138 correlation_id = %exchange.correlation_id(),
3139 header = %drop.name,
3140 "outbound header dropped: {}",
3141 drop.reason
3142 ),
3143 }
3144 }
3145 }
3146
3147 let conn_tokens = header_policy::connection_tokens(
3148 exchange
3149 .input
3150 .headers
3151 .iter()
3152 .filter(|(k, _)| k.eq_ignore_ascii_case("connection"))
3153 .filter_map(|(_, v)| v.as_str()),
3154 );
3155
3156 let outbound = select_outbound_headers(
3157 &exchange.input.headers,
3158 &config.skip_request_headers,
3159 &conn_tokens,
3160 );
3161 for drop in &outbound.drops {
3162 if let Some(value_kind) = drop.value_kind {
3163 debug!(
3164 correlation_id = %exchange.correlation_id(),
3165 header = %drop.name,
3166 value_kind = value_kind,
3167 "outbound header dropped: {}",
3168 drop.reason
3169 );
3170 } else {
3171 debug!(
3172 correlation_id = %exchange.correlation_id(),
3173 header = %drop.name,
3174 "outbound header dropped: {}",
3175 drop.reason
3176 );
3177 }
3178 }
3179 collected_headers.extend(outbound.accepted);
3180
3181 if !config.bridge_endpoint {
3183 match &config.auth {
3184 HttpAuth::None => {}
3185 HttpAuth::Basic { username, password } => {
3186 use base64::Engine;
3187 let credentials = format!("{username}:{password}");
3189 let encoded =
3190 base64::engine::general_purpose::STANDARD.encode(credentials);
3191 match constructed_header("authorization", &format!("Basic {encoded}")) {
3194 Ok((_, val)) => {
3195 collected_headers.push((reqwest::header::AUTHORIZATION, val));
3196 }
3197 Err(drop) => debug!(
3198 correlation_id = %exchange.correlation_id(),
3199 header = %drop.name,
3200 "outbound header dropped: {}",
3201 drop.reason
3202 ),
3203 }
3204 }
3205 HttpAuth::Bearer { token } => {
3206 let bearer = format!("Bearer {token}");
3208 match constructed_header("authorization", &bearer) {
3209 Ok((_, val)) => {
3210 collected_headers.push((reqwest::header::AUTHORIZATION, val));
3211 }
3212 Err(drop) => debug!(
3213 correlation_id = %exchange.correlation_id(),
3214 header = %drop.name,
3215 "outbound header dropped: {}",
3216 drop.reason
3217 ),
3218 }
3219 }
3220 }
3221
3222 if config.connection_close {
3223 collected_headers.push((
3224 reqwest::header::CONNECTION,
3225 reqwest::header::HeaderValue::from_static("close"),
3226 ));
3227 }
3228 }
3229
3230 let is_stream_body = matches!(exchange.input.body, Body::Stream(_));
3232 let materialized_body: Option<Vec<u8>> = if is_stream_body {
3233 if suppress_body {
3234 std::mem::take(&mut exchange.input.body);
3242 tracing::warn!(
3244 correlation_id = %exchange.correlation_id(),
3245 method = %method_str,
3246 "dropping request body for non-entity-enclosing HTTP method"
3247 );
3248 }
3249 None } else {
3251 let body = std::mem::take(&mut exchange.input.body);
3252 let bytes = body.into_bytes(config.max_body_size).await?;
3253 if bytes.is_empty() {
3254 None
3256 } else if suppress_body {
3257 tracing::warn!(
3259 correlation_id = %exchange.correlation_id(),
3260 method = %method_str,
3261 "dropping request body for non-entity-enclosing HTTP method"
3262 );
3263 None
3264 } else {
3265 Some(bytes.to_vec())
3266 }
3267 };
3268
3269 let response = if config.follow_redirects && !is_stream_body {
3270 ssrf::send_with_ssrf_safe_redirects(
3276 &client,
3277 &shared_client,
3278 &pinned_cache,
3279 &http_config,
3280 &config,
3281 method,
3282 &url,
3283 collected_headers,
3284 materialized_body,
3285 config.max_redirects,
3286 config.response_timeout,
3287 )
3288 .await?
3289 } else {
3290 let mut request = client.request(method, &url);
3292
3293 if let Some(timeout) = config.response_timeout {
3294 request = request.timeout(timeout);
3295 }
3296
3297 for (name, value) in &collected_headers {
3298 request = request.header(name, value);
3299 }
3300
3301 if is_stream_body {
3302 if let Body::Stream(ref s) = exchange.input.body {
3303 let mut stream_lock = s.stream.lock().await;
3304 if let Some(stream) = stream_lock.take() {
3305 request = request.body(reqwest::Body::wrap_stream(stream));
3306 } else {
3307 return Err(CamelError::AlreadyConsumed);
3308 }
3309 }
3310 } else if let Some(ref body_bytes) = materialized_body {
3311 request = request.body(body_bytes.clone());
3312 }
3313
3314 request.send().await.map_err(|e| {
3315 CamelError::ProcessorError(format!("HTTP request failed: {e}"))
3316 })?
3317 };
3318
3319 let status_code = response.status().as_u16();
3320 let status_text = response
3321 .status()
3322 .canonical_reason()
3323 .unwrap_or("Unknown")
3324 .to_string();
3325
3326 for (key, value) in response.headers() {
3327 if config
3328 .skip_response_headers
3329 .iter()
3330 .any(|h| h.eq_ignore_ascii_case(key.as_str()))
3331 {
3332 continue;
3333 }
3334 if let Ok(val_str) = value.to_str() {
3335 exchange.input.set_header(
3336 title_case_header(key.as_str()),
3337 serde_json::Value::String(val_str.to_string()),
3338 );
3339 }
3340 }
3341
3342 exchange.input.set_header(
3343 "CamelHttpResponseCode",
3344 serde_json::Value::Number(status_code.into()),
3345 );
3346 exchange.input.set_header(
3347 "CamelHttpResponseText",
3348 serde_json::Value::String(status_text.clone()),
3349 );
3350
3351 let read_timeout = Duration::from_millis(config.read_timeout_ms);
3353 let response_body = tokio::time::timeout(read_timeout, async {
3354 if let Some(content_len) = response.content_length()
3356 && content_len > config.max_response_bytes as u64
3357 {
3358 return Err(CamelError::ProcessorError(format!(
3359 "Response body too large: {} bytes exceeds limit of {} bytes",
3360 content_len, config.max_response_bytes
3361 )));
3362 }
3363 use futures::TryStreamExt;
3365 let mut stream = response.bytes_stream();
3366 let mut total: usize = 0;
3367 let mut collected = Vec::new();
3368 while let Some(chunk) = stream.try_next().await.map_err(|e| {
3369 CamelError::ProcessorError(format!("Failed to read response body: {e}"))
3370 })? {
3371 total += chunk.len();
3372 if total > config.max_response_bytes {
3373 return Err(CamelError::ProcessorError(format!(
3374 "Response body too large: {} bytes exceeds limit of {} bytes",
3375 total, config.max_response_bytes
3376 )));
3377 }
3378 collected.push(chunk);
3379 }
3380 let mut result = bytes::BytesMut::with_capacity(total);
3381 for chunk in collected {
3382 result.extend_from_slice(&chunk);
3383 }
3384 Ok::<bytes::Bytes, CamelError>(result.freeze())
3385 })
3386 .await
3387 .map_err(|_| {
3388 CamelError::ProcessorError(format!(
3389 "Read timeout after {}ms",
3390 config.read_timeout_ms
3391 ))
3392 })??;
3393
3394 if config.throw_exception_on_failure
3395 && !HttpProducer::is_ok_status(status_code, config.ok_status_code_range)
3396 {
3397 return Err(CamelError::HttpOperationFailed {
3398 method: method_str,
3399 url: redact_url_for_diagnostics(&url),
3402 status_code,
3403 status_text,
3404 response_body: Some(truncate_error_body(&response_body)),
3405 });
3406 }
3407
3408 if !response_body.is_empty() {
3409 exchange.input.body = Body::Bytes(bytes::Bytes::from(response_body.to_vec()));
3410 }
3411
3412 debug!(
3413 correlation_id = %exchange.correlation_id(),
3414 status = status_code,
3415 url = %redact_url_for_diagnostics(&url),
3416 "HTTP response"
3417 );
3418 Ok(exchange)
3419 }
3420 .await;
3421 component_metrics.observe("http", "request", outcome.is_err());
3428 outcome
3429 })
3430 }
3431}
3432
3433#[cfg(test)]
3447pub(crate) static REGISTRY_TEST_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
3448
3449#[cfg(test)]
3457pub(crate) fn lock_registry_test_mutex() -> std::sync::MutexGuard<'static, ()> {
3458 REGISTRY_TEST_MUTEX
3459 .lock()
3460 .unwrap_or_else(|poisoned| poisoned.into_inner())
3461}
3462
3463#[cfg(test)]
3467static CA_STORE_TEST_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
3468
3469#[cfg(test)]
3470pub(crate) fn lock_ca_store_test_mutex() -> std::sync::MutexGuard<'static, ()> {
3471 CA_STORE_TEST_MUTEX
3472 .lock()
3473 .unwrap_or_else(|poisoned| poisoned.into_inner())
3474}
3475
3476fn pipeline_error_to_reply(e: CamelError, path: &str) -> HttpReply {
3486 match e {
3487 CamelError::Unauthenticated(msg) => {
3488 tracing::warn!(error = %msg, path = %path, "Authentication failed");
3489 HttpReply {
3490 status: 401,
3491 headers: vec![("WWW-Authenticate".to_string(), "Bearer".to_string())],
3492 body: HttpReplyBody::Bytes(bytes::Bytes::from("Unauthorized")),
3493 }
3494 }
3495 CamelError::Unauthorized(msg) => {
3496 tracing::warn!(error = %msg, path = %path, "Authorization failed");
3497 HttpReply {
3498 status: 403,
3499 headers: vec![],
3500 body: HttpReplyBody::Bytes(bytes::Bytes::from("Forbidden")),
3501 }
3502 }
3503 CamelError::TypeConversionFailed(msg) => {
3504 tracing::warn!(error = %msg, path = %path, "Type conversion failed (bad request)");
3505 json_error_reply(400, "bad_request", msg)
3506 }
3507 CamelError::ValidationError(msg) => {
3508 tracing::warn!(error = %msg, path = %path, "Schema validation failed (bad request)");
3509 json_error_reply(400, "validation_error", msg)
3510 }
3511 CamelError::ConsumerStopping => {
3512 tracing::debug!(path = %path, "Pipeline aborted during route shutdown");
3513 HttpReply {
3514 status: 503,
3515 headers: vec![],
3516 body: HttpReplyBody::Bytes(bytes::Bytes::from("Service Unavailable")),
3517 }
3518 }
3519 CamelError::UnsupportedMediaType { consumed, declared } => {
3520 tracing::warn!(error = %consumed, declared = %declared, path = %path, "Unsupported media type (bad request)");
3521 json_error_reply(
3522 415,
3523 "unsupported_media_type",
3524 format!("consumed {consumed}, declared {declared}"),
3525 )
3526 }
3527 CamelError::NotAcceptable { accept, produced } => {
3528 tracing::warn!(error = %accept, produced = %produced, path = %path, "Not acceptable (bad request)");
3529 json_error_reply(
3530 406,
3531 "not_acceptable",
3532 format!("accept {accept}, produced {produced}"),
3533 )
3534 }
3535 e => {
3536 tracing::warn!(error = %e, path = %path, "Pipeline error processing HTTP request");
3538 HttpReply {
3539 status: 500,
3540 headers: vec![],
3541 body: HttpReplyBody::Bytes(bytes::Bytes::from("Internal Server Error")),
3542 }
3543 }
3544 }
3545}
3546
3547fn json_error_reply(status: u16, code: &str, message: String) -> HttpReply {
3555 let body = serde_json::to_string(&serde_json::json!({
3556 "error": code,
3557 "message": message,
3558 }))
3559 .unwrap_or_else(|_| "{}".to_string()); HttpReply {
3561 status,
3562 headers: vec![("Content-Type".to_string(), "application/json".to_string())],
3563 body: HttpReplyBody::Bytes(bytes::Bytes::from(body)),
3564 }
3565}
3566
3567const fn json_value_kind(v: &serde_json::Value) -> &'static str {
3571 match v {
3572 serde_json::Value::Null => "null",
3573 serde_json::Value::Bool(_) => "bool",
3574 serde_json::Value::Number(_) => "number",
3575 serde_json::Value::String(_) => "string",
3576 serde_json::Value::Array(_) => "array",
3577 serde_json::Value::Object(_) => "object",
3578 }
3579}
3580
3581fn scalar_string_form(v: &serde_json::Value) -> Option<String> {
3586 match v {
3587 serde_json::Value::String(s) => Some(s.clone()),
3588 serde_json::Value::Number(n) => Some(n.to_string()),
3589 serde_json::Value::Bool(b) => Some(b.to_string()),
3590 _ => None,
3591 }
3592}
3593
3594fn select_response_headers(
3609 headers: &HashMap<String, serde_json::Value>,
3610 user_content_type: Option<String>,
3611 inferred_content_type: Option<String>,
3612) -> Vec<(String, String)> {
3613 let conn_tokens = header_policy::connection_tokens(
3614 headers
3615 .iter()
3616 .filter(|(k, _)| k.eq_ignore_ascii_case("connection"))
3617 .filter_map(|(_, v)| v.as_str()),
3618 );
3619 let mut selected: Vec<(String, String)> = Vec::new();
3620 for (k, v) in headers {
3621 if k.starts_with("Camel") {
3622 debug!(header = %k, "reply header dropped: Camel namespace");
3623 continue;
3624 }
3625 if header_policy::excluded_response(k, &conn_tokens) {
3626 debug!(header = %k, "reply header dropped: emission policy");
3627 continue;
3628 }
3629 match scalar_string_form(v) {
3630 Some(s) => selected.push((k.clone(), s)),
3631 None => debug!(
3632 header = %k,
3633 value_kind = json_value_kind(v),
3634 "reply header dropped: no scalar string form"
3635 ),
3636 }
3637 }
3638 if let Some(ct) = user_content_type.or(inferred_content_type) {
3639 selected.push(("Content-Type".to_string(), ct));
3640 }
3641 selected
3642}
3643
3644#[derive(Debug)]
3649struct OutboundHeaderDrop<'a> {
3650 name: &'a str,
3651 reason: &'static str,
3652 value_kind: Option<&'static str>,
3653}
3654
3655struct OutboundHeaderSelection<'a> {
3658 accepted: Vec<(reqwest::header::HeaderName, reqwest::header::HeaderValue)>,
3659 drops: Vec<OutboundHeaderDrop<'a>>,
3660}
3661
3662fn select_outbound_headers<'a>(
3676 headers: &'a HashMap<String, serde_json::Value>,
3677 skip_request_headers: &[String],
3678 conn_tokens: &[String],
3679) -> OutboundHeaderSelection<'a> {
3680 let mut accepted = Vec::new();
3681 let mut drops = Vec::new();
3682 for (key, value) in headers {
3683 if key.starts_with("Camel") {
3684 drops.push(OutboundHeaderDrop {
3685 name: key,
3686 reason: "Camel namespace",
3687 value_kind: None,
3688 });
3689 continue;
3690 }
3691 if skip_request_headers
3692 .iter()
3693 .any(|h| h.eq_ignore_ascii_case(key))
3694 {
3695 drops.push(OutboundHeaderDrop {
3696 name: key,
3697 reason: "skip_request_headers",
3698 value_kind: None,
3699 });
3700 continue;
3701 }
3702 if header_policy::excluded_outbound(key, conn_tokens) {
3703 drops.push(OutboundHeaderDrop {
3704 name: key,
3705 reason: "outbound emission policy",
3706 value_kind: None,
3707 });
3708 continue;
3709 }
3710 let Some(val_str) = scalar_string_form(value) else {
3711 drops.push(OutboundHeaderDrop {
3712 name: key,
3713 reason: "no scalar string form",
3714 value_kind: Some(json_value_kind(value)),
3715 });
3716 continue;
3717 };
3718 match constructed_header(key, &val_str) {
3719 Ok((name, val)) => accepted.push((name, val)),
3720 Err(drop) => drops.push(drop),
3721 }
3722 }
3723 OutboundHeaderSelection { accepted, drops }
3724}
3725
3726fn constructed_header<'a>(
3731 name: &'a str,
3732 value: &str,
3733) -> Result<(reqwest::header::HeaderName, reqwest::header::HeaderValue), OutboundHeaderDrop<'a>> {
3734 let header_name = match reqwest::header::HeaderName::from_bytes(name.as_bytes()) {
3735 Ok(header_name) => header_name,
3736 Err(_) => {
3737 return Err(OutboundHeaderDrop {
3738 name,
3739 reason: "invalid header name",
3740 value_kind: None,
3741 });
3742 }
3743 };
3744 let header_value = match reqwest::header::HeaderValue::from_str(value) {
3745 Ok(header_value) => header_value,
3746 Err(_) => {
3747 return Err(OutboundHeaderDrop {
3748 name,
3749 reason: "invalid header value",
3750 value_kind: None,
3751 });
3752 }
3753 };
3754 Ok((header_name, header_value))
3755}
3756
3757#[cfg(test)]
3758#[path = "lib_tests.rs"]
3759mod tests;