1use std::collections::{HashMap, VecDeque};
40use std::net::SocketAddr;
41use std::pin::Pin;
42use std::sync::{Arc, Mutex, OnceLock};
43use std::task::{Context, Poll};
44use std::time::Duration;
45
46use async_trait::async_trait;
47use http_body_util::combinators::BoxBody;
48use http_body_util::{BodyExt, Full};
49use hyper::body::{Bytes, Frame};
50use hyper::service::service_fn;
51use hyper::{Request, Response, StatusCode};
52use hyper_util::rt::TokioIo;
53use tokio::sync::{mpsc, oneshot};
54
55use crate::envelope::EventEnvelope;
56use crate::event_stream;
57use crate::function::{AppError, ComposableFunction};
58use crate::platform::Platform;
59use crate::post_office::PostOffice;
60use crate::trace;
61use crate::util::app_config_reader::AppConfigReader;
62use crate::util::config_reader::ConfigReader;
63use crate::util::w3c_trace;
64
65use super::routing::{AssignedRoute, RouteInfo, RoutingTable};
66
67pub const MY_CORRELATION_ID: &str = "my_correlation_id";
70
71pub const ASYNC_HTTP_RESPONSE: &str = "async.http.response";
74
75pub const ASYNC_HTTP_RESPONSE_STREAM_POOL: &str = "async.http.response.stream";
81
82const RESPONSE_HANDLER_INSTANCES: usize = 500;
85
86const STREAM_EVENT_BUFFER: usize = 64;
88const STREAM_FRAME_BUFFER: usize = 64;
90
91type HttpBody = BoxBody<Bytes, std::convert::Infallible>;
94
95fn full(bytes: Bytes) -> HttpBody {
97 BoxBody::new(Full::new(bytes))
98}
99
100fn lane_pool() -> &'static Mutex<VecDeque<String>> {
107 static POOL: OnceLock<Mutex<VecDeque<String>>> = OnceLock::new();
108 POOL.get_or_init(|| Mutex::new(VecDeque::new()))
109}
110
111pub fn checkout_lane() -> Option<String> {
114 lane_pool().lock().expect("lane pool poisoned").pop_front()
115}
116
117pub fn release_lane(route: String) {
120 lane_pool()
121 .lock()
122 .expect("lane pool poisoned")
123 .push_back(route);
124}
125
126pub fn available_lanes() -> usize {
128 lane_pool().lock().expect("lane pool poisoned").len()
129}
130
131fn pending_streams() -> &'static Mutex<HashMap<String, mpsc::Sender<EventEnvelope>>> {
135 static PENDING: OnceLock<Mutex<HashMap<String, mpsc::Sender<EventEnvelope>>>> = OnceLock::new();
136 PENDING.get_or_init(|| Mutex::new(HashMap::new()))
137}
138
139fn cleanup_stream(context_id: &str, lane: &str) {
142 let removed = pending_streams()
143 .lock()
144 .expect("pending streams poisoned")
145 .remove(context_id);
146 if removed.is_some() {
147 release_lane(lane.to_string());
148 }
149}
150
151pub struct StreamLaneService;
157
158#[async_trait]
159impl ComposableFunction for StreamLaneService {
160 async fn handle_event(
161 &self,
162 _headers: HashMap<String, String>,
163 input: EventEnvelope,
164 _instance: usize,
165 ) -> Result<EventEnvelope, AppError> {
166 if let Some(context_id) = input.correlation_id().map(str::to_string) {
167 let sender = pending_streams()
168 .lock()
169 .expect("pending streams poisoned")
170 .get(&context_id)
171 .cloned();
172 if let Some(sender) = sender {
173 let _ = sender.send(input).await;
176 }
177 }
178 Ok(EventEnvelope::new())
179 }
180}
181
182struct ChannelBody {
186 rx: mpsc::Receiver<Frame<Bytes>>,
187}
188
189impl hyper::body::Body for ChannelBody {
190 type Data = Bytes;
191 type Error = std::convert::Infallible;
192
193 fn poll_frame(
194 mut self: Pin<&mut Self>,
195 cx: &mut Context<'_>,
196 ) -> Poll<Option<Result<Frame<Bytes>, Self::Error>>> {
197 self.rx.poll_recv(cx).map(|frame| frame.map(Ok))
198 }
199}
200
201fn keep_alive_ms() -> u64 {
204 static KEEP_ALIVE: OnceLock<u64> = OnceLock::new();
205 *KEEP_ALIVE.get_or_init(|| {
206 let config = AppConfigReader::get_instance();
207 let text = config.get_property_or("event.stream.keep.alive", "30s");
208 let trimmed = text.trim().to_lowercase();
209 if trimmed == "0" || trimmed == "0s" || trimmed == "0ms" || trimmed == "0m" {
210 0
211 } else {
212 super::routing::parse_timeout(Some(&trimmed)).as_millis() as u64
213 }
214 })
215}
216
217const PROTECTED_METADATA: [&str; 5] = [
220 "my_route",
221 "my_trace_id",
222 "my_trace_path",
223 MY_CORRELATION_ID,
224 "x-event-api",
225];
226
227fn pending_responses() -> &'static Mutex<HashMap<String, oneshot::Sender<EventEnvelope>>> {
231 static PENDING: OnceLock<Mutex<HashMap<String, oneshot::Sender<EventEnvelope>>>> =
232 OnceLock::new();
233 PENDING.get_or_init(|| Mutex::new(HashMap::new()))
234}
235
236pub struct AsyncHttpResponseService;
245
246#[async_trait]
247impl ComposableFunction for AsyncHttpResponseService {
248 async fn handle_event(
249 &self,
250 _headers: HashMap<String, String>,
251 input: EventEnvelope,
252 _instance: usize,
253 ) -> Result<EventEnvelope, AppError> {
254 if let Some(context_id) = input.correlation_id().map(str::to_string) {
255 let sender = pending_responses()
256 .lock()
257 .expect("pending http contexts poisoned")
258 .remove(&context_id);
259 if let Some(sender) = sender {
260 let _ = sender.send(input);
261 }
262 }
263 Ok(EventEnvelope::new())
264 }
265}
266
267static SERVER_ADDR: OnceLock<SocketAddr> = OnceLock::new();
272
273pub fn server_address() -> Option<SocketAddr> {
277 SERVER_ADDR.get().copied()
278}
279
280struct RouterState {
281 table: RoutingTable,
282 platform: Platform,
283 trace_header: String,
284 cid_header: String,
285 traceparent_header: String,
294}
295
296pub async fn start_http_server(platform: &Platform) -> Result<SocketAddr, AppError> {
302 let config = AppConfigReader::get_instance();
303 if !platform.has_route(ASYNC_HTTP_RESPONSE) {
307 if let Err(e) = platform.register_private(
308 ASYNC_HTTP_RESPONSE,
309 Arc::new(AsyncHttpResponseService),
310 RESPONSE_HANDLER_INSTANCES,
311 ) {
312 if !platform.has_route(ASYNC_HTTP_RESPONSE) {
313 return Err(e);
314 }
315 }
316 }
317 let members = platform.register_route_pool(
328 ASYNC_HTTP_RESPONSE_STREAM_POOL,
329 Arc::new(StreamLaneService),
330 RESPONSE_HANDLER_INSTANCES,
331 )?;
332 static POOL_FILLED: OnceLock<()> = OnceLock::new();
333 POOL_FILLED.get_or_init(|| {
334 for lane_route in members {
335 release_lane(lane_route);
336 }
337 });
338 let rest_yaml = config.get_property_or("yaml.rest.automation", "classpath:/rest.yaml");
339 let reader = ConfigReader::load(&rest_yaml)
340 .map_err(|e| AppError::new(500, format!("Unable to load {rest_yaml} - {e}")))?;
341 let mut table = RoutingTable::load(&reader)?;
342 merge_default_endpoints(&mut table)?;
343 for (methods, url, service) in table.retain_available(|service| platform.has_route(service)) {
348 log::warn!("Skip {methods} {url} - Service {service} not available");
349 }
350 let table = table;
351 for route in table.routes() {
352 log::info!(
353 "{} {} -> {}",
354 route.methods.join(","),
355 route.url,
356 route.service
357 );
358 }
359 let port: u16 = config
360 .get_property_or("rest.server.port", "8085")
361 .parse()
362 .map_err(|_| AppError::new(500, "Invalid rest.server.port"))?;
363 let state = Arc::new(RouterState {
364 table,
365 platform: platform.clone(),
366 trace_header: config.get_property_or("http.trace.id.header", "X-Trace-Id"),
367 cid_header: config.get_property_or("http.correlation.id.header", "X-Correlation-Id"),
368 traceparent_header: config
369 .get_property_or("http.traceparent.header", w3c_trace::TRACEPARENT),
370 });
371 log::info!("Correlation-id HTTP header is '{}'", state.cid_header);
374 log::info!("Trace-id HTTP header is '{}'", state.trace_header);
375 log::info!("Traceparent HTTP header is '{}'", state.traceparent_header);
376 let listener = tokio::net::TcpListener::bind(("0.0.0.0", port))
377 .await
378 .map_err(|e| AppError::new(500, format!("Unable to bind port {port} - {e}")))?;
379 let addr = listener
380 .local_addr()
381 .map_err(|e| AppError::new(500, e.to_string()))?;
382 let _ = SERVER_ADDR.set(addr);
383 log::info!("REST automation service started on port {}", addr.port());
384 tokio::spawn(async move {
385 loop {
386 let Ok((stream, peer)) = listener.accept().await else {
387 break;
388 };
389 let state = state.clone();
390 tokio::spawn(async move {
391 let io = TokioIo::new(stream);
392 let service = service_fn(move |request| {
393 let state = state.clone();
394 async move { handle(state, request, peer).await }
395 });
396 if let Err(e) = hyper::server::conn::http1::Builder::new()
397 .serve_connection(io, service)
398 .with_upgrades()
399 .await
400 {
401 log::debug!("HTTP connection ended - {e}");
402 }
403 });
404 }
405 });
406 Ok(addr)
407}
408
409async fn handle(
410 state: Arc<RouterState>,
411 request: Request<hyper::body::Incoming>,
412 peer: SocketAddr,
413) -> Result<Response<HttpBody>, hyper::Error> {
414 if super::ws_server::is_ws_upgrade(&request) {
417 return Ok(super::ws_server::handle_ws_upgrade(
418 &state.platform,
419 request,
420 peer.ip().to_string(),
421 )
422 .map(BoxBody::new));
423 }
424 let method = request.method().as_str().to_uppercase();
425 let path = request.uri().path().to_string();
426 let query_text = request.uri().query().unwrap_or("").to_string();
427 let mut headers: HashMap<String, String> = HashMap::new();
429 for (name, value) in request.headers() {
430 if let Ok(value) = value.to_str() {
431 headers.insert(name.as_str().to_lowercase(), value.to_string());
432 }
433 }
434 let body_bytes = match request.into_body().collect().await {
435 Ok(collected) => collected.to_bytes(),
436 Err(_) => Bytes::new(),
437 };
438 let assigned = state.table.find(&method, &path).or_else(|| {
443 if path == "/" {
444 state.table.find(&method, "/index.html")
445 } else {
446 None
447 }
448 });
449 let Some(assigned) = assigned else {
450 if state.table.path_matches_any_method(&path) {
453 return Ok(error_response(405, "Method not allowed"));
454 }
455 if method == "GET" || method == "HEAD" {
459 if let Some(response) =
460 serve_static(&state, &path, &query_text, &headers, peer, method == "HEAD").await
461 {
462 return Ok(response);
463 }
464 }
465 return Ok(error_response(404, "Resource not found"));
466 };
467 if method == "OPTIONS" {
472 let Some(cors) = assigned
473 .info
474 .cors
475 .as_ref()
476 .filter(|c| !c.options.is_empty())
477 else {
478 return Ok(error_response(405, "Method not allowed"));
479 };
480 let mut response = Response::builder().status(StatusCode::NO_CONTENT);
481 for (name, value) in &cors.options {
482 response = response.header(name, value);
483 }
484 return Ok(response.body(full(Bytes::new())).expect("static response"));
485 }
486 match process(
487 &state, assigned, method, path, query_text, headers, body_bytes, peer,
488 )
489 .await
490 {
491 Ok(response) => Ok(response),
492 Err(e) => Ok(error_response(e.status(), e.message())),
493 }
494}
495
496#[allow(clippy::too_many_arguments)]
497async fn process(
498 state: &RouterState,
499 assigned: AssignedRoute<'_>,
500 method: String,
501 path: String,
502 query_text: String,
503 mut headers: HashMap<String, String>,
504 body_bytes: Bytes,
505 peer: SocketAddr,
506) -> Result<Response<HttpBody>, AppError> {
507 let info = assigned.info;
508 if let Some(header_info) = &info.headers {
510 header_info.request.apply(&mut headers);
511 }
512 if let Some(flow) = &info.flow {
515 headers.insert("x-flow-id".to_string(), flow.clone());
516 }
517 let trace_header = info
519 .trace_id_header
520 .as_deref()
521 .unwrap_or(&state.trace_header)
522 .to_lowercase();
523 let cid_header = info
524 .correlation_id_header
525 .as_deref()
526 .unwrap_or(&state.cid_header)
527 .to_lowercase();
528 let traceparent = headers
537 .get(w3c_trace::TRACEPARENT)
538 .and_then(|value| w3c_trace::parse(value))
539 .or_else(|| {
540 let traceparent_header = info
541 .traceparent_header
542 .as_deref()
543 .unwrap_or(&state.traceparent_header)
544 .to_lowercase();
545 if traceparent_header == w3c_trace::TRACEPARENT {
546 None
547 } else {
548 headers
549 .get(&traceparent_header)
550 .and_then(|value| w3c_trace::parse(value))
551 }
552 });
553 let (trace_id, parent_span) = match &traceparent {
554 Some((trace_id, parent)) => (Some(trace_id.clone()), Some(parent.clone())),
555 None => (headers.get(&trace_header).cloned(), None),
556 };
557 let trace_id = if info.tracing {
558 Some(trace_id.unwrap_or_else(trace::new_trace_id))
559 } else {
560 None
561 };
562 let cid = headers.get(&cid_header).cloned().unwrap_or_else(|| {
565 if cid_header == trace_header {
566 trace_id
567 .clone()
568 .unwrap_or_else(|| uuid::Uuid::new_v4().simple().to_string())
569 } else {
570 uuid::Uuid::new_v4().simple().to_string()
571 }
572 });
573 headers.insert(cid_header.clone(), cid.clone());
577 headers
585 .entry("x-ttl".to_string())
586 .or_insert_with(|| (info.timeout.as_secs().max(1) * 1000).to_string());
587 let mut query: HashMap<String, serde_json::Value> = HashMap::new();
592 for pair in query_text.split('&').filter(|p| !p.is_empty()) {
593 let (name, value) = pair.split_once('=').unwrap_or((pair, ""));
594 let (name, value) = (url_decode(name), url_decode(value));
595 match query.get_mut(&name) {
596 None => {
597 query.insert(name, serde_json::Value::String(value));
598 }
599 Some(serde_json::Value::Array(values)) => {
600 values.push(serde_json::Value::String(value));
601 }
602 Some(existing) => {
603 let first = existing.clone();
604 *existing = serde_json::Value::Array(vec![first, serde_json::Value::String(value)]);
605 }
606 }
607 }
608 let path_params: HashMap<String, String> = assigned
609 .path_params
610 .iter()
611 .map(|(k, v)| (k.clone(), url_decode(v)))
612 .collect();
613 let cookies: HashMap<String, String> = headers
617 .remove("cookie")
618 .map(|header| {
619 header
620 .split(';')
621 .filter_map(|item| item.split_once('='))
622 .map(|(name, value)| (name.trim().to_string(), value.trim().to_string()))
623 .collect()
624 })
625 .unwrap_or_default();
626 let accept = headers.get("accept").cloned();
630 let parsed = parse_body(&headers, &body_bytes);
631 if let ParsedBody::Form(form) = &parsed {
635 for (name, value) in form {
636 query.insert(name.clone(), serde_json::Value::String(value.clone()));
637 }
638 }
639 let mut http_request = crate::automation::AsyncHttpRequest::new()
646 .set_method(&method)
647 .set_url(&path)
648 .set_remote_ip(&peer.ip().to_string())
649 .set_secure(headers.get("x-forwarded-proto").map(String::as_str) == Some("https"))
652 .set_target_host(&headers.get("host").cloned().unwrap_or_default())
653 .set_route_timeout_seconds(info.timeout.as_secs());
656 for (key, value) in &headers {
657 http_request = http_request.set_header(key, value);
658 }
659 for (key, value) in &path_params {
660 http_request = http_request.set_path_parameter(key, value);
661 }
662 for (key, value) in &query {
663 http_request = match value {
664 serde_json::Value::Array(values) => {
665 let values: Vec<&str> = values
666 .iter()
667 .map(|v| v.as_str().unwrap_or_default())
668 .collect();
669 http_request.set_query_parameter_values(key, &values)
670 }
671 serde_json::Value::String(value) => http_request.set_query_parameter(key, value),
672 other => http_request.set_query_parameter(key, &other.to_string()),
673 };
674 }
675 http_request = match &parsed {
680 ParsedBody::Value(value) => http_request
681 .set_body(rmpv::ext::to_value(value).map_err(|e| AppError::new(500, e.to_string()))?),
682 ParsedBody::Bytes(bytes) => http_request.set_body(rmpv::Value::Binary(bytes.clone())),
683 ParsedBody::Form(_) => http_request.set_body(rmpv::Value::Nil),
684 };
685 if !query_text.is_empty() {
688 http_request = http_request.set_query_string(&query_text);
689 }
690 for (key, value) in &cookies {
691 http_request = http_request.set_cookie(key, value);
692 }
693 let po = PostOffice::new(&state.platform);
694 let trace_path = if query_text.is_empty() {
696 format!("{method} {path}")
697 } else {
698 format!("{method} {path}?{query_text}")
699 };
700 if let Some(auth_route) = &info.authentication {
703 let auth_event = build_event(
704 auth_route,
705 &http_request,
706 &cid,
707 &trace_id,
708 &trace_path,
709 &parent_span,
710 )?;
711 let verdict = po.request(auth_event, info.timeout).await?;
712 if verdict.has_error() {
713 return Err(AppError::new(
714 verdict.status(),
715 verdict
716 .body_as::<String>()
717 .unwrap_or_else(|_| "Unauthorized".to_string()),
718 ));
719 }
720 if !verdict.body_as::<bool>().unwrap_or(false) {
721 return Err(AppError::new(401, "Unauthorized"));
722 }
723 for (key, value) in verdict.headers() {
727 http_request = http_request.set_session_info(key, value);
728 }
729 }
730 let is_head = method == "HEAD";
731 let envelope_stream = !is_head && is_event_api_stream(info, &http_request);
736 let result = if (info.stream_response && !is_head) || envelope_stream {
739 match stream_dispatch(
740 state,
741 info,
742 &http_request,
743 &cid,
744 &cid_header,
745 &trace_id,
746 &trace_path,
747 &parent_span,
748 accept.clone(),
749 envelope_stream,
750 )
751 .await?
752 {
753 StreamOutcome::Streaming(response) => return Ok(response),
754 StreamOutcome::SingleShot(envelope) => envelope,
755 }
756 } else {
757 let context_id = uuid::Uuid::new_v4().simple().to_string();
763 let (tx, rx) = oneshot::channel();
764 pending_responses()
765 .lock()
766 .expect("pending http contexts poisoned")
767 .insert(context_id.clone(), tx);
768 let event = build_event(
769 &info.service,
770 &http_request,
771 &cid,
772 &trace_id,
773 &trace_path,
774 &parent_span,
775 )?
776 .set_correlation_id(&context_id)
777 .set_reply_to(ASYNC_HTTP_RESPONSE);
778 if let Err(e) = po.send(event).await {
779 pending_responses()
780 .lock()
781 .expect("pending http contexts poisoned")
782 .remove(&context_id);
783 return Err(e);
784 }
785 match tokio::time::timeout(info.timeout, rx).await {
786 Ok(Ok(envelope)) => envelope,
787 Ok(Err(_)) => {
788 return Err(AppError::new(500, "Response channel closed unexpectedly"));
789 }
790 Err(_) => {
791 pending_responses()
792 .lock()
793 .expect("pending http contexts poisoned")
794 .remove(&context_id);
795 return Err(AppError::new(
796 408,
797 format!("Timeout for {} ms", info.timeout.as_millis()),
798 ));
799 }
800 }
801 };
802 let status = status_of(result.status());
805 let (result, standard_error) = match result.body() {
813 rmpv::Value::String(text) if result.status() >= 400 && result.headers().is_empty() => {
814 let message = text.as_str().unwrap_or_default().trim().to_string();
815 if message.starts_with('{') || message.starts_with('[') || message.starts_with('<') {
816 (result, false)
817 } else {
818 (
819 result.set_raw_body(rmpv::Value::Map(vec![
820 (
821 rmpv::Value::from("status"),
822 rmpv::Value::from(status.as_u16()),
823 ),
824 (rmpv::Value::from("message"), rmpv::Value::from(message)),
825 (rmpv::Value::from("type"), rmpv::Value::from("error")),
826 ])),
827 true,
828 )
829 }
830 }
831 _ => (result, false),
832 };
833 let mut content_type: Option<String> = None;
834 let mut set_cookies: Vec<String> = Vec::new();
835 let mut response_headers: HashMap<String, String> = HashMap::new();
836 for (name, value) in result.headers() {
837 let key = name.to_lowercase();
838 if PROTECTED_METADATA.contains(&key.as_str()) {
841 continue;
842 }
843 match key.as_str() {
844 "x-stream-id" if value.starts_with("stream.") && value.contains(".in") => {}
848 "x-ttl" => {}
849 "content-type" => {
852 if !is_head {
853 content_type = Some(value.to_lowercase());
854 }
855 }
856 "set-cookie" => {
859 set_cookies.extend(value.split('|').map(|c| c.trim().to_string()));
860 }
861 _ => {
862 response_headers.insert(key, value.clone());
863 }
864 }
865 }
866 if content_type.is_none() && !is_head {
871 content_type = accept_fallback_type(accept.as_deref(), result.body());
872 }
873 if standard_error && content_type.is_none() && !is_head {
876 content_type = Some("application/json".to_string());
877 }
878 let payload = render_payload(result.body(), content_type.as_deref());
879 if let Some(header_info) = &info.headers {
882 header_info.response.apply(&mut response_headers);
883 }
884 response_headers.entry(cid_header.clone()).or_insert(cid);
889 if let Some(content_type) = content_type {
890 response_headers.insert("content-type".to_string(), content_type);
891 }
892 if let Some(cors) = &info.cors {
893 for (name, value) in &cors.headers {
894 response_headers.insert(name.to_lowercase(), value.clone());
895 }
896 }
897 let mut response = Response::builder().status(status);
898 for (name, value) in response_headers {
899 response = response.header(name, value);
900 }
901 for cookie in set_cookies {
902 if !cookie.is_empty() {
903 response = response.header("set-cookie", cookie);
904 }
905 }
906 let payload = if is_head { Bytes::new() } else { payload };
908 response
909 .body(full(payload))
910 .map_err(|e| AppError::new(500, e.to_string()))
911}
912
913#[allow(clippy::large_enum_variant)]
918enum StreamOutcome {
919 Streaming(Response<HttpBody>),
920 SingleShot(EventEnvelope),
921}
922
923fn stream_marker(event: &EventEnvelope) -> Result<Option<&'static str>, ()> {
927 for (name, value) in event.headers() {
928 if name.eq_ignore_ascii_case(event_stream::X_EVENT_STREAM) {
929 return match value.to_lowercase().as_str() {
930 event_stream::DATA => Ok(Some(event_stream::DATA)),
931 event_stream::EOF => Ok(Some(event_stream::EOF)),
932 event_stream::EXCEPTION => Ok(Some(event_stream::EXCEPTION)),
933 _ => Err(()),
934 };
935 }
936 }
937 Ok(None)
938}
939
940fn stream_error_message(event: &EventEnvelope) -> String {
942 match event.body() {
943 rmpv::Value::Map(entries) => entries
944 .iter()
945 .find(|(key, _)| key.as_str() == Some("message"))
946 .map(|(_, value)| stream_text(value))
947 .unwrap_or_else(|| "Stream failed".to_string()),
948 rmpv::Value::Nil => "Stream failed".to_string(),
949 other => stream_text(other),
950 }
951}
952
953fn negotiate_stream_type(accept: Option<&str>) -> String {
956 let Some(accept) = accept else {
957 return "application/json".to_string();
958 };
959 if accept.contains("*/*") || accept.contains("application/json") {
960 "application/json".to_string()
961 } else if accept.contains("text/event-stream") {
962 "text/event-stream".to_string()
963 } else if accept.contains("text/html") {
964 "text/html".to_string()
965 } else if accept.contains("application/xml") {
966 "application/xml".to_string()
967 } else {
968 "text/plain".to_string()
969 }
970}
971
972fn stream_text(body: &rmpv::Value) -> String {
976 match body {
977 rmpv::Value::Nil => String::new(),
978 rmpv::Value::String(text) => text.as_str().unwrap_or_default().to_string(),
979 rmpv::Value::Binary(bytes) => String::from_utf8_lossy(bytes).to_string(),
980 other => {
981 let stripped = crate::serializer::strip_nulls(other);
982 let json = serde_json::to_value(&stripped).unwrap_or_default();
983 serde_json::to_string(&json).unwrap_or_default()
984 }
985 }
986}
987
988fn sse_frame(event_name: Option<&str>, text: &str) -> Bytes {
991 let mut frame = String::new();
992 if let Some(name) = event_name.filter(|n| !n.is_empty()) {
993 frame.push_str("event: ");
994 frame.push_str(name);
995 frame.push('\n');
996 }
997 for line in text.split('\n') {
998 frame.push_str("data: ");
999 frame.push_str(line);
1000 frame.push('\n');
1001 }
1002 frame.push('\n');
1003 Bytes::from(frame)
1004}
1005
1006fn chunk_bytes(body: &rmpv::Value) -> Bytes {
1009 match body {
1010 rmpv::Value::Nil => Bytes::new(),
1011 rmpv::Value::String(text) => Bytes::from(text.as_str().unwrap_or_default().to_string()),
1012 rmpv::Value::Binary(bytes) => Bytes::from(bytes.clone()),
1013 other => {
1014 let mut line = stream_text(other);
1015 line.push('\n');
1016 Bytes::from(line)
1017 }
1018 }
1019}
1020
1021fn stream_event_name(event: &EventEnvelope) -> Option<&str> {
1023 event
1024 .headers()
1025 .iter()
1026 .find(|(name, _)| name.eq_ignore_ascii_case(event_stream::X_EVENT_NAME))
1027 .map(|(_, value)| value.as_str())
1028}
1029
1030fn is_event_api_stream(info: &RouteInfo, request: &crate::automation::AsyncHttpRequest) -> bool {
1036 info.service == super::event_api::EVENT_API_SERVICE
1037 && request.header("x-async") != Some("true")
1038 && request
1039 .header("accept")
1040 .is_some_and(|accept| accept.contains("text/event-stream"))
1041}
1042
1043fn event_api_idle(request: &crate::automation::AsyncHttpRequest) -> Duration {
1047 let ttl_ms = request
1048 .header("x-ttl")
1049 .and_then(|v| v.trim().parse::<u64>().ok())
1050 .unwrap_or(0)
1051 .max(1000);
1052 Duration::from_millis(ttl_ms)
1053}
1054
1055#[allow(clippy::too_many_arguments)]
1064async fn stream_dispatch(
1065 state: &RouterState,
1066 info: &RouteInfo,
1067 http_request: &crate::automation::AsyncHttpRequest,
1068 cid: &str,
1069 cid_header: &str,
1070 trace_id: &Option<String>,
1071 trace_path: &str,
1072 parent_span: &Option<String>,
1073 accept: Option<String>,
1074 envelope_mode: bool,
1075) -> Result<StreamOutcome, AppError> {
1076 let Some(lane) = checkout_lane() else {
1079 return Err(AppError::new(503, "Streaming response pool exhausted"));
1080 };
1081 let po = PostOffice::new(&state.platform);
1082 let context_id = uuid::Uuid::new_v4().simple().to_string();
1083 let (tx, mut rx) = mpsc::channel::<EventEnvelope>(STREAM_EVENT_BUFFER);
1084 pending_streams()
1085 .lock()
1086 .expect("pending streams poisoned")
1087 .insert(context_id.clone(), tx);
1088 let event = build_event(
1089 &info.service,
1090 http_request,
1091 cid,
1092 trace_id,
1093 trace_path,
1094 parent_span,
1095 )?
1096 .set_correlation_id(&context_id)
1097 .set_reply_to(&lane);
1098 if let Err(e) = po.send(event).await {
1099 cleanup_stream(&context_id, &lane);
1100 return Err(e);
1101 }
1102 let base_idle = if envelope_mode {
1105 event_api_idle(http_request)
1106 } else {
1107 info.timeout
1108 };
1109 let (first, marker) = loop {
1111 match tokio::time::timeout(base_idle, rx.recv()).await {
1112 Ok(Some(envelope)) => match stream_marker(&envelope) {
1113 Ok(Some(marker)) => break (envelope, Some(marker)),
1114 Ok(None) => break (envelope, None),
1115 Err(()) => {
1116 log::warn!(
1118 "Dropping event for {context_id} - invalid {} signal",
1119 event_stream::X_EVENT_STREAM
1120 );
1121 }
1122 },
1123 Ok(None) => {
1124 cleanup_stream(&context_id, &lane);
1125 return Err(AppError::new(500, "Response channel closed unexpectedly"));
1126 }
1127 Err(_) => {
1128 cleanup_stream(&context_id, &lane);
1129 return Err(AppError::new(
1130 408,
1131 format!("Timeout for {} ms", base_idle.as_millis()),
1132 ));
1133 }
1134 }
1135 };
1136 let Some(marker) = marker else {
1137 cleanup_stream(&context_id, &lane);
1142 let reply = if envelope_mode {
1143 wire_single_shot(first)?
1144 } else {
1145 first
1146 };
1147 return Ok(StreamOutcome::SingleShot(reply));
1148 };
1149 if marker == event_stream::EXCEPTION && !envelope_mode {
1150 cleanup_stream(&context_id, &lane);
1154 let status = if first.status() >= 400 {
1155 first.status()
1156 } else {
1157 500
1158 };
1159 return Err(AppError::new(status, stream_error_message(&first)));
1160 }
1161 if first
1163 .headers()
1164 .keys()
1165 .any(|k| k.eq_ignore_ascii_case("x-stream-id"))
1166 {
1167 log::warn!("Ignoring x-stream-id on a streaming response for {context_id}");
1169 }
1170 let mut response_headers: HashMap<String, String> = HashMap::new();
1171 let mut set_cookies: Vec<String> = Vec::new();
1172 let mut content_type: Option<String> = None;
1173 let mut idle_override: Option<Duration> = None;
1174 for (name, value) in first.headers() {
1175 let key = name.to_lowercase();
1176 match key.as_str() {
1177 event_stream::X_EVENT_STREAM | event_stream::X_EVENT_NAME | "x-stream-id" => {}
1179 "x-ttl" => {
1181 if let Ok(seconds) = value.trim().parse::<u64>() {
1182 if seconds > 0 {
1183 idle_override = Some(Duration::from_secs(seconds));
1184 }
1185 }
1186 }
1187 _ if envelope_mode => {}
1190 "content-type" => content_type = Some(value.to_lowercase()),
1191 "set-cookie" => {
1192 set_cookies.extend(value.split('|').map(|c| c.trim().to_string()));
1193 }
1194 _ => {
1195 response_headers.insert(key, value.clone());
1196 }
1197 }
1198 }
1199 if let Some(header_info) = &info.headers {
1202 header_info.response.apply(&mut response_headers);
1203 }
1204 response_headers
1206 .entry(cid_header.to_string())
1207 .or_insert_with(|| cid.to_string());
1208 if let Some(cors) = &info.cors {
1209 for (name, value) in &cors.headers {
1210 response_headers.insert(name.to_lowercase(), value.clone());
1211 }
1212 }
1213 let content_type = if envelope_mode {
1215 "text/event-stream".to_string()
1216 } else {
1217 content_type.unwrap_or_else(|| negotiate_stream_type(accept.as_deref()))
1218 };
1219 let sse = content_type.starts_with("text/event-stream");
1220 if sse {
1221 response_headers
1223 .entry("cache-control".to_string())
1224 .or_insert_with(|| "no-cache".to_string());
1225 }
1226 let idle = idle_override.unwrap_or(base_idle);
1227 let mut builder = Response::builder().status(status_of(first.status()));
1228 for (name, value) in &response_headers {
1229 builder = builder.header(name, value);
1230 }
1231 for cookie in set_cookies {
1232 if !cookie.is_empty() {
1233 builder = builder.header("set-cookie", cookie);
1234 }
1235 }
1236 builder = builder.header("content-type", &content_type);
1237 let (body_tx, body_rx) = mpsc::channel::<Frame<Bytes>>(STREAM_FRAME_BUFFER);
1238 let response = builder
1239 .body(BoxBody::new(ChannelBody { rx: body_rx }))
1240 .map_err(|e| AppError::new(500, e.to_string()))?;
1241 tokio::spawn(render_stream(
1242 rx,
1243 body_tx,
1244 sse,
1245 idle,
1246 context_id,
1247 lane,
1248 first,
1249 marker,
1250 envelope_mode,
1251 ));
1252 Ok(StreamOutcome::Streaming(response))
1253}
1254
1255#[allow(clippy::large_enum_variant)]
1259enum Waited {
1260 Event(EventEnvelope),
1261 Idle,
1262 Closed,
1263}
1264
1265async fn next_stream_event(
1269 rx: &mut mpsc::Receiver<EventEnvelope>,
1270 body_tx: &mpsc::Sender<Frame<Bytes>>,
1271 sse: bool,
1272 idle: Duration,
1273) -> Waited {
1274 let ping_every = keep_alive_ms();
1275 let idle_deadline = tokio::time::sleep(idle);
1276 tokio::pin!(idle_deadline);
1277 loop {
1278 if sse && ping_every > 0 {
1279 let ping = tokio::time::sleep(Duration::from_millis(ping_every));
1280 tokio::pin!(ping);
1281 tokio::select! {
1282 received = rx.recv() => {
1283 return match received {
1284 Some(event) => Waited::Event(event),
1285 None => Waited::Closed,
1286 };
1287 }
1288 _ = &mut idle_deadline => return Waited::Idle,
1289 _ = &mut ping => {
1290 let _ = body_tx.try_send(Frame::data(Bytes::from_static(b": ping\n\n")));
1291 }
1292 }
1293 } else {
1294 tokio::select! {
1295 received = rx.recv() => {
1296 return match received {
1297 Some(event) => Waited::Event(event),
1298 None => Waited::Closed,
1299 };
1300 }
1301 _ = &mut idle_deadline => return Waited::Idle,
1302 }
1303 }
1304 }
1305}
1306
1307async fn push_frame(
1312 body_tx: &mpsc::Sender<Frame<Bytes>>,
1313 idle: Duration,
1314 context_id: &str,
1315 bytes: Bytes,
1316) -> bool {
1317 if bytes.is_empty() {
1318 return true;
1319 }
1320 match tokio::time::timeout(idle, body_tx.send(Frame::data(bytes))).await {
1321 Ok(Ok(())) => true,
1322 Ok(Err(_)) => {
1323 log::debug!("Client disconnected from event stream {context_id}");
1324 false
1325 }
1326 Err(_) => {
1327 log::error!("Closing event stream for {context_id} - client too slow");
1328 false
1329 }
1330 }
1331}
1332
1333#[allow(clippy::too_many_arguments)]
1342async fn render_stream(
1343 mut rx: mpsc::Receiver<EventEnvelope>,
1344 body_tx: mpsc::Sender<Frame<Bytes>>,
1345 sse: bool,
1346 idle: Duration,
1347 context_id: String,
1348 lane: String,
1349 first: EventEnvelope,
1350 first_marker: &'static str,
1351 envelope_mode: bool,
1352) {
1353 let mut pending = Some((first, first_marker));
1354 let mut first_frame = true;
1355 loop {
1356 let (event, marker) = match pending.take() {
1357 Some(next) => next,
1358 None => match next_stream_event(&mut rx, &body_tx, sse, idle).await {
1359 Waited::Event(event) => match stream_marker(&event) {
1360 Ok(Some(marker)) => (event, marker),
1361 Ok(None) | Err(()) => {
1362 log::warn!(
1363 "Dropping event for {context_id} - invalid {} signal",
1364 event_stream::X_EVENT_STREAM
1365 );
1366 continue;
1367 }
1368 },
1369 Waited::Idle => {
1370 if envelope_mode {
1372 let frame = idle_timeout_envelope_frame(idle);
1373 let _ = push_frame(&body_tx, idle, &context_id, frame).await;
1374 } else if sse {
1375 let error = serde_json::json!({
1376 "status": 408,
1377 "message": format!("Timeout for {} seconds", idle.as_secs()),
1378 "type": "error",
1379 });
1380 let frame = sse_frame(Some("error"), &error.to_string());
1381 let _ = push_frame(&body_tx, idle, &context_id, frame).await;
1382 }
1383 break;
1384 }
1385 Waited::Closed => break,
1386 },
1387 };
1388 match marker {
1389 event_stream::DATA => {
1390 let bytes = if envelope_mode {
1391 envelope_mode_data_frame(&event, first_frame)
1392 } else if sse {
1393 if matches!(event.body(), rmpv::Value::Nil) {
1394 Bytes::new()
1395 } else {
1396 sse_frame(stream_event_name(&event), &stream_text(event.body()))
1397 }
1398 } else {
1399 chunk_bytes(event.body())
1400 };
1401 first_frame = false;
1402 if !push_frame(&body_tx, idle, &context_id, bytes).await {
1403 break;
1404 }
1405 }
1406 event_stream::EOF => {
1407 if envelope_mode {
1408 let frame = envelope_wire_frame(&event);
1409 let _ = push_frame(&body_tx, idle, &context_id, frame).await;
1410 } else if sse {
1411 let text = if matches!(event.body(), rmpv::Value::Nil) {
1412 "{}".to_string()
1413 } else {
1414 stream_text(event.body())
1415 };
1416 let frame = sse_frame(Some("done"), &text);
1417 let _ = push_frame(&body_tx, idle, &context_id, frame).await;
1418 }
1419 break;
1420 }
1421 _ => {
1422 if envelope_mode {
1426 let frame = envelope_wire_frame(&event);
1427 let _ = push_frame(&body_tx, idle, &context_id, frame).await;
1428 } else if sse {
1429 let status = if event.status() >= 400 {
1430 event.status()
1431 } else {
1432 500
1433 };
1434 let error = serde_json::json!({
1435 "status": status,
1436 "message": stream_error_message(&event),
1437 "type": "error",
1438 });
1439 let frame = sse_frame(Some("error"), &error.to_string());
1440 let _ = push_frame(&body_tx, idle, &context_id, frame).await;
1441 }
1442 break;
1443 }
1444 }
1445 }
1446 cleanup_stream(&context_id, &lane);
1447}
1448
1449fn envelope_mode_data_frame(event: &EventEnvelope, first_frame: bool) -> Bytes {
1454 if first_frame || !raw_streamable(event) {
1455 envelope_wire_frame(event)
1456 } else if matches!(event.body(), rmpv::Value::Nil) {
1457 Bytes::new()
1458 } else {
1459 sse_frame(stream_event_name(event), &stream_text(event.body()))
1460 }
1461}
1462
1463fn raw_streamable(event: &EventEnvelope) -> bool {
1469 if event.status() != 200 {
1470 return false;
1471 }
1472 for (name, value) in event.headers() {
1473 let key = name.to_lowercase();
1474 let reserved = key == event_stream::X_EVENT_STREAM
1475 || key == event_stream::X_EVENT_NAME
1476 || key == "x-ttl";
1477 if !reserved || (key == event_stream::X_EVENT_NAME && value == event_stream::ENVELOPE) {
1478 return false;
1479 }
1480 }
1481 match event.body() {
1482 rmpv::Value::Nil => true,
1483 rmpv::Value::String(text) => !text.as_str().unwrap_or_default().contains('\r'),
1484 _ => false,
1485 }
1486}
1487
1488fn wire_single_shot(result: EventEnvelope) -> Result<EventEnvelope, AppError> {
1492 let bytes = result.clear_to().clear_reply_to().to_bytes()?;
1493 Ok(EventEnvelope::new()
1494 .set_status(200)
1495 .set_header("content-type", "application/octet-stream")
1496 .set_raw_body(rmpv::Value::Binary(bytes)))
1497}
1498
1499fn envelope_wire_frame(event: &EventEnvelope) -> Bytes {
1504 use base64::Engine as _;
1505 let wire = event.clone().clear_to().clear_reply_to();
1506 match wire.to_bytes() {
1507 Ok(bytes) => sse_frame(
1508 Some(event_stream::ENVELOPE),
1509 &base64::engine::general_purpose::STANDARD.encode(bytes),
1510 ),
1511 Err(_) => Bytes::new(),
1512 }
1513}
1514
1515fn idle_timeout_envelope_frame(idle: Duration) -> Bytes {
1519 let message = format!("Timeout for {} seconds", idle.as_secs());
1520 let error = EventEnvelope::new()
1521 .set_header(event_stream::X_EVENT_STREAM, event_stream::EXCEPTION)
1522 .set_status(408)
1523 .set_body(serde_json::json!({"type": "error", "status": 408, "message": message}));
1524 match error {
1525 Ok(envelope) => envelope_wire_frame(&envelope),
1526 Err(_) => Bytes::new(),
1527 }
1528}
1529
1530fn build_event(
1531 to: &str,
1532 http_request: &crate::automation::AsyncHttpRequest,
1533 cid: &str,
1534 trace_id: &Option<String>,
1535 trace_path: &str,
1536 parent_span: &Option<String>,
1537) -> Result<EventEnvelope, AppError> {
1538 let mut event = EventEnvelope::new()
1539 .set_to(to)
1540 .set_from("http.request")
1541 .set_correlation_id(cid)
1542 .add_tag(crate::post_office::BUSINESS_CID_TAG, cid)
1547 .set_raw_body(http_request.to_value());
1551 if let Some(trace_id) = trace_id {
1552 event = event.set_trace(trace_id, trace_path);
1553 if let Some(parent) = parent_span {
1554 event = event.set_span_id(parent);
1556 }
1557 }
1558 Ok(event)
1559}
1560
1561enum ParsedBody {
1563 Value(serde_json::Value),
1565 Form(HashMap<String, String>),
1567 Bytes(Vec<u8>),
1569}
1570
1571fn base_content_type(headers: &HashMap<String, String>) -> Option<String> {
1576 headers
1577 .get("content-type")
1578 .map(|ct| ct.split(';').next().unwrap_or(ct).trim().to_string())
1579}
1580
1581fn parse_body(headers: &HashMap<String, String>, bytes: &Bytes) -> ParsedBody {
1598 let content_type = base_content_type(headers);
1599 let ct = content_type.as_deref().unwrap_or("?");
1600 if ct.starts_with("application/json") {
1601 let text = String::from_utf8_lossy(bytes).to_string();
1602 let trimmed = text.trim();
1603 let parsed = if trimmed.is_empty() {
1604 Some(serde_json::Value::Object(serde_json::Map::new()))
1605 } else if (trimmed.starts_with('{') && trimmed.ends_with('}'))
1606 || (trimmed.starts_with('[') && trimmed.ends_with(']'))
1607 {
1608 serde_json::from_str(&text).ok()
1609 } else {
1610 None
1611 };
1612 ParsedBody::Value(parsed.unwrap_or(serde_json::Value::String(text)))
1613 } else if ct == "application/x-www-form-urlencoded" {
1614 let text = String::from_utf8_lossy(bytes);
1615 let mut form = HashMap::new();
1616 for pair in text.split('&').filter(|p| !p.is_empty()) {
1617 let (name, value) = pair.split_once('=').unwrap_or((pair, ""));
1618 form.insert(url_decode(name), url_decode(value));
1619 }
1620 ParsedBody::Form(form)
1621 } else if ct.starts_with("application/xml")
1622 || ct.starts_with("text/html")
1623 || ct.starts_with("text/plain")
1624 {
1625 ParsedBody::Value(serde_json::Value::String(
1626 String::from_utf8_lossy(bytes).to_string(),
1627 ))
1628 } else if bytes.is_empty() {
1629 ParsedBody::Value(serde_json::Value::Null)
1630 } else {
1631 ParsedBody::Bytes(bytes.to_vec())
1632 }
1633}
1634
1635fn url_decode(text: &str) -> String {
1637 let bytes = text.as_bytes();
1638 let mut out = Vec::with_capacity(bytes.len());
1639 let mut i = 0;
1640 while i < bytes.len() {
1641 match bytes[i] {
1642 b'+' => {
1643 out.push(b' ');
1644 i += 1;
1645 }
1646 b'%' if i + 2 < bytes.len() => {
1647 let hex = std::str::from_utf8(&bytes[i + 1..i + 3]).ok();
1648 match hex.and_then(|h| u8::from_str_radix(h, 16).ok()) {
1649 Some(byte) => {
1650 out.push(byte);
1651 i += 3;
1652 }
1653 None => {
1654 out.push(bytes[i]);
1655 i += 1;
1656 }
1657 }
1658 }
1659 other => {
1660 out.push(other);
1661 i += 1;
1662 }
1663 }
1664 }
1665 String::from_utf8_lossy(&out).to_string()
1666}
1667
1668const DEFAULT_REST_YAML: &str = include_str!("../../resources/default-rest.yaml");
1675
1676fn merge_default_endpoints(table: &mut RoutingTable) -> Result<(), AppError> {
1677 let defaults = RoutingTable::from_yaml_text(DEFAULT_REST_YAML)?;
1678 for route in defaults.routes() {
1679 if !table.has_url(&route.url) {
1680 table.add_route(route.clone());
1681 }
1682 }
1683 Ok(())
1684}
1685
1686async fn serve_static(
1704 state: &RouterState,
1705 path: &str,
1706 query_text: &str,
1707 headers: &HashMap<String, String>,
1708 peer: SocketAddr,
1709 head_only: bool,
1710) -> Option<Response<HttpBody>> {
1711 let (bytes, filename) = resolve_static_file(path)?;
1712 let static_content = state.table.static_content();
1713 let no_cache = super::routing::matched_element(&static_content.no_cache_pages, path);
1714 let mut filter_headers: Vec<(String, String)> = Vec::new();
1716 if let Some(filter) = &static_content.filter {
1717 let applies = super::routing::matched_element(&filter.path_list, path)
1718 && !super::routing::matched_element(&filter.exclusion_list, path);
1719 if applies {
1720 if state.platform.has_route(&filter.service) {
1721 match run_static_filter(state, filter, path, query_text, headers, peer).await {
1722 Ok(filtered) => {
1723 for (name, value) in filtered.headers() {
1725 filter_headers.push((name.clone(), value.clone()));
1726 }
1727 if filtered.status() != 200 {
1728 let (content_type, payload) = envelope_payload(&filtered);
1730 let mut response =
1731 Response::builder().status(status_of(filtered.status()));
1732 let mut has_content_type = false;
1733 for (name, value) in &filter_headers {
1734 has_content_type |= name.eq_ignore_ascii_case("content-type");
1735 response = response.header(name, value);
1736 }
1737 if let (Some(content_type), false) = (content_type, has_content_type) {
1738 response = response.header("content-type", content_type);
1739 }
1740 return response.body(full(payload)).ok();
1741 }
1742 }
1743 Err(e) => {
1744 log::error!(
1747 "Unable to filter static content HTTP-GET {} - {}",
1748 filter.service,
1749 e.message()
1750 );
1751 }
1752 }
1753 } else {
1754 log::warn!(
1755 "Static content filter {} ignored because it does not exist",
1756 filter.service
1757 );
1758 }
1759 }
1760 }
1761 let mime = mime_for(
1763 std::path::Path::new(&filename)
1764 .extension()
1765 .and_then(|e| e.to_str())
1766 .unwrap_or(""),
1767 );
1768 let mut response = Response::builder().status(StatusCode::OK);
1769 for (name, value) in &filter_headers {
1770 response = response.header(name, value);
1771 }
1772 response = response.header("content-type", mime);
1773 if no_cache {
1774 response = response
1775 .header("Cache-Control", "no-cache, no-store")
1776 .header("Pragma", "no-cache")
1777 .header("Expires", "Thu, 01 Jan 1970 00:00:00 GMT");
1778 } else {
1779 use sha2::Digest;
1780 let etag = format!("\"{:x}\"", sha2::Sha256::digest(&bytes));
1781 let matched = headers
1783 .get("if-none-match")
1784 .is_some_and(|inm| inm.split(',').any(|tag| tag.trim() == etag));
1785 if matched {
1786 return Response::builder()
1787 .status(StatusCode::NOT_MODIFIED)
1788 .header("content-length", "0")
1789 .body(full(Bytes::new()))
1790 .ok();
1791 }
1792 response = response.header("ETag", etag);
1793 }
1794 let payload = if head_only {
1795 Bytes::new()
1796 } else {
1797 Bytes::from(bytes)
1798 };
1799 response.body(full(payload)).ok()
1800}
1801
1802fn resolve_static_file(path: &str) -> Option<(Vec<u8>, String)> {
1805 if path.contains("..") {
1806 return None; }
1808 let rel = path.trim_start_matches('/');
1809 let relative = if rel.is_empty() || path.ends_with('/') {
1810 format!("{rel}/index.html")
1811 .trim_start_matches('/')
1812 .to_string()
1813 } else {
1814 let filename = rel.rsplit('/').next().unwrap_or(rel);
1815 if filename.contains('.') {
1816 rel.to_string()
1817 } else {
1818 format!("{rel}.html") }
1820 };
1821 let file = crate::util::resources::resolve_classpath(&format!("public/{relative}"))?;
1822 let bytes = std::fs::read(&file).ok()?;
1823 let filename = relative.rsplit('/').next().unwrap_or(&relative).to_string();
1824 Some((bytes, filename))
1825}
1826
1827async fn run_static_filter(
1830 state: &RouterState,
1831 filter: &super::routing::SimpleHttpFilter,
1832 path: &str,
1833 query_text: &str,
1834 headers: &HashMap<String, String>,
1835 peer: SocketAddr,
1836) -> Result<EventEnvelope, AppError> {
1837 let mut request = crate::automation::AsyncHttpRequest::new()
1841 .set_method("GET")
1842 .set_url(path)
1843 .set_remote_ip(&peer.ip().to_string())
1844 .set_secure(false)
1845 .set_target_host(&headers.get("host").cloned().unwrap_or_default())
1846 .set_body(rmpv::Value::Nil);
1847 for (key, value) in headers {
1848 request = request.set_header(key, value);
1849 }
1850 for pair in query_text.split('&').filter(|p| !p.is_empty()) {
1851 let (name, value) = pair.split_once('=').unwrap_or((pair, ""));
1852 request = request.set_query_parameter(&url_decode(name), &url_decode(value));
1853 }
1854 let event = EventEnvelope::new()
1855 .set_to(&filter.service)
1856 .set_raw_body(request.to_value());
1857 let po = PostOffice::new(&state.platform);
1858 po.request(event, std::time::Duration::from_secs(10)).await
1860}
1861
1862fn accept_fallback_type(accept: Option<&str>, _body: &rmpv::Value) -> Option<String> {
1871 let accept = accept?;
1872 if accept.contains("text/html") {
1873 Some("text/html".to_string())
1874 } else if accept.contains("application/json")
1875 || accept.contains("*/*")
1876 || accept.contains("application/xml")
1877 {
1878 Some("application/json".to_string())
1879 } else {
1880 Some("text/plain".to_string())
1881 }
1882}
1883
1884fn render_payload(body: &rmpv::Value, content_type: Option<&str>) -> Bytes {
1890 match body {
1891 rmpv::Value::Nil => Bytes::new(),
1892 rmpv::Value::String(text) => Bytes::from(text.as_str().unwrap_or_default().to_string()),
1893 rmpv::Value::Binary(bytes) => Bytes::from(bytes.clone()),
1894 _ => {
1895 let stripped = crate::serializer::strip_nulls(body);
1897 let json = serde_json::to_value(&stripped).unwrap_or_default();
1898 let text = serde_json::to_string_pretty(&json).unwrap_or_default();
1905 if content_type.is_some_and(|t| t.starts_with("text/html"))
1906 && matches!(body, rmpv::Value::Map(_) | rmpv::Value::Array(_))
1907 {
1908 Bytes::from(format!("<html><body><pre>\n{text}\n</pre></body></html>"))
1909 } else {
1910 Bytes::from(text)
1911 }
1912 }
1913 }
1914}
1915
1916fn envelope_payload(result: &EventEnvelope) -> (Option<&'static str>, Bytes) {
1917 match result.body() {
1918 rmpv::Value::Nil => (None, Bytes::new()),
1919 rmpv::Value::String(text) => (
1920 Some("text/plain"),
1921 Bytes::from(text.as_str().unwrap_or_default().to_string()),
1922 ),
1923 rmpv::Value::Binary(bytes) => {
1924 (Some("application/octet-stream"), Bytes::from(bytes.clone()))
1925 }
1926 _ => {
1927 let body = crate::serializer::strip_nulls(result.body());
1929 let json = serde_json::to_value(&body).unwrap_or_default();
1930 (
1932 Some("application/json"),
1933 Bytes::from(serde_json::to_string_pretty(&json).unwrap_or_default()),
1934 )
1935 }
1936 }
1937}
1938
1939fn status_of(code: i32) -> StatusCode {
1940 StatusCode::from_u16(code as u16).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
1941}
1942
1943fn mime_for(extension: &str) -> &'static str {
1946 match extension.to_ascii_lowercase().as_str() {
1947 "html" | "htm" => "text/html",
1948 "css" => "text/css",
1949 "js" | "mjs" => "text/javascript",
1950 "json" => "application/json",
1951 "png" => "image/png",
1952 "jpg" | "jpeg" => "image/jpeg",
1953 "gif" => "image/gif",
1954 "svg" => "image/svg+xml",
1955 "ico" => "image/x-icon",
1956 "txt" => "text/plain",
1957 "pdf" => "application/pdf",
1958 "woff2" => "font/woff2",
1959 "xml" => "application/xml",
1960 _ => "application/octet-stream",
1961 }
1962}
1963
1964fn error_response(status: i32, message: &str) -> Response<HttpBody> {
1966 let body = serde_json::json!({"status": status, "message": message, "type": "error"});
1967 Response::builder()
1968 .status(StatusCode::from_u16(status as u16).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR))
1969 .header("content-type", "application/json")
1970 .body(full(Bytes::from(body.to_string())))
1971 .expect("static response")
1972}
1973
1974#[cfg(test)]
1975mod tests {
1976 use super::*;
1977
1978 #[test]
1979 fn url_decoding() {
1980 assert_eq!(url_decode("hello%20world"), "hello world");
1981 assert_eq!(url_decode("a+b"), "a b");
1982 assert_eq!(url_decode("plain"), "plain");
1983 assert_eq!(url_decode("bad%zz"), "bad%zz");
1984 }
1985
1986 fn headers_of(content_type: &str) -> HashMap<String, String> {
1987 HashMap::from([("content-type".to_string(), content_type.to_string())])
1988 }
1989
1990 fn value_of(parsed: ParsedBody) -> serde_json::Value {
1991 match parsed {
1992 ParsedBody::Value(value) => value,
1993 ParsedBody::Form(_) => panic!("expected a value, got form fields"),
1994 ParsedBody::Bytes(_) => panic!("expected a value, got bytes"),
1995 }
1996 }
1997
1998 #[test]
2001 fn body_parsing() {
2002 let json = headers_of("application/json; charset=utf-8");
2004 let value = value_of(parse_body(&json, &Bytes::from(r#"{"a":1}"#)));
2005 assert_eq!(value["a"], 1);
2006 let text = value_of(parse_body(&json, &Bytes::from("import graph from x")));
2008 assert_eq!(
2009 text,
2010 serde_json::Value::String("import graph from x".into())
2011 );
2012 let bad = value_of(parse_body(&json, &Bytes::from("{broken")));
2014 assert_eq!(bad, serde_json::Value::String("{broken".into()));
2015 let empty = value_of(parse_body(&json, &Bytes::new()));
2017 assert_eq!(empty, serde_json::json!({}));
2018 let plain = headers_of("text/plain");
2020 let unsniffed = value_of(parse_body(&plain, &Bytes::from(r#"{"a":1}"#)));
2021 assert_eq!(unsniffed, serde_json::Value::String(r#"{"a":1}"#.into()));
2022 let xml = value_of(parse_body(
2024 &headers_of("application/xml"),
2025 &Bytes::from("<a>1</a>"),
2026 ));
2027 assert_eq!(xml, serde_json::Value::String("<a>1</a>".into()));
2028 let form = parse_body(
2030 &headers_of("application/x-www-form-urlencoded"),
2031 &Bytes::from("a=1&b=hello+world"),
2032 );
2033 match form {
2034 ParsedBody::Form(fields) => {
2035 assert_eq!(fields["a"], "1");
2036 assert_eq!(fields["b"], "hello world");
2037 }
2038 _ => panic!("expected form fields"),
2039 }
2040 match parse_body(&HashMap::new(), &Bytes::from("hello")) {
2042 ParsedBody::Bytes(bytes) => assert_eq!(bytes, b"hello"),
2043 _ => panic!("expected bytes for a missing content type"),
2044 }
2045 assert_eq!(
2047 value_of(parse_body(&HashMap::new(), &Bytes::new())),
2048 serde_json::Value::Null
2049 );
2050 }
2051}