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 let table = table;
344 for route in table.routes() {
345 log::info!(
346 "{} {} -> {}",
347 route.methods.join(","),
348 route.url,
349 route.service
350 );
351 }
352 let port: u16 = config
353 .get_property_or("rest.server.port", "8085")
354 .parse()
355 .map_err(|_| AppError::new(500, "Invalid rest.server.port"))?;
356 let state = Arc::new(RouterState {
357 table,
358 platform: platform.clone(),
359 trace_header: config.get_property_or("http.trace.id.header", "X-Trace-Id"),
360 cid_header: config.get_property_or("http.correlation.id.header", "X-Correlation-Id"),
361 traceparent_header: config
362 .get_property_or("http.traceparent.header", w3c_trace::TRACEPARENT),
363 });
364 log::info!("Correlation-id HTTP header is '{}'", state.cid_header);
367 log::info!("Trace-id HTTP header is '{}'", state.trace_header);
368 log::info!("Traceparent HTTP header is '{}'", state.traceparent_header);
369 let listener = tokio::net::TcpListener::bind(("0.0.0.0", port))
370 .await
371 .map_err(|e| AppError::new(500, format!("Unable to bind port {port} - {e}")))?;
372 let addr = listener
373 .local_addr()
374 .map_err(|e| AppError::new(500, e.to_string()))?;
375 let _ = SERVER_ADDR.set(addr);
376 log::info!("REST automation service started on port {}", addr.port());
377 tokio::spawn(async move {
378 loop {
379 let Ok((stream, peer)) = listener.accept().await else {
380 break;
381 };
382 let state = state.clone();
383 tokio::spawn(async move {
384 let io = TokioIo::new(stream);
385 let service = service_fn(move |request| {
386 let state = state.clone();
387 async move { handle(state, request, peer).await }
388 });
389 if let Err(e) = hyper::server::conn::http1::Builder::new()
390 .serve_connection(io, service)
391 .with_upgrades()
392 .await
393 {
394 log::debug!("HTTP connection ended - {e}");
395 }
396 });
397 }
398 });
399 Ok(addr)
400}
401
402async fn handle(
403 state: Arc<RouterState>,
404 request: Request<hyper::body::Incoming>,
405 peer: SocketAddr,
406) -> Result<Response<HttpBody>, hyper::Error> {
407 if super::ws_server::is_ws_upgrade(&request) {
410 return Ok(super::ws_server::handle_ws_upgrade(
411 &state.platform,
412 request,
413 peer.ip().to_string(),
414 )
415 .map(BoxBody::new));
416 }
417 let method = request.method().as_str().to_uppercase();
418 let path = request.uri().path().to_string();
419 let query_text = request.uri().query().unwrap_or("").to_string();
420 let mut headers: HashMap<String, String> = HashMap::new();
422 for (name, value) in request.headers() {
423 if let Ok(value) = value.to_str() {
424 headers.insert(name.as_str().to_lowercase(), value.to_string());
425 }
426 }
427 let body_bytes = match request.into_body().collect().await {
428 Ok(collected) => collected.to_bytes(),
429 Err(_) => Bytes::new(),
430 };
431 let Some(assigned) = state.table.find(&method, &path) else {
432 if state.table.path_matches_any_method(&path) {
435 return Ok(error_response(405, "Method not allowed"));
436 }
437 if method == "GET" || method == "HEAD" {
441 if let Some(response) =
442 serve_static(&state, &path, &query_text, &headers, peer, method == "HEAD").await
443 {
444 return Ok(response);
445 }
446 }
447 return Ok(error_response(404, "Resource not found"));
448 };
449 if method == "OPTIONS" {
454 let Some(cors) = assigned
455 .info
456 .cors
457 .as_ref()
458 .filter(|c| !c.options.is_empty())
459 else {
460 return Ok(error_response(405, "Method not allowed"));
461 };
462 let mut response = Response::builder().status(StatusCode::NO_CONTENT);
463 for (name, value) in &cors.options {
464 response = response.header(name, value);
465 }
466 return Ok(response.body(full(Bytes::new())).expect("static response"));
467 }
468 match process(
469 &state, assigned, method, path, query_text, headers, body_bytes, peer,
470 )
471 .await
472 {
473 Ok(response) => Ok(response),
474 Err(e) => Ok(error_response(e.status(), e.message())),
475 }
476}
477
478#[allow(clippy::too_many_arguments)]
479async fn process(
480 state: &RouterState,
481 assigned: AssignedRoute<'_>,
482 method: String,
483 path: String,
484 query_text: String,
485 mut headers: HashMap<String, String>,
486 body_bytes: Bytes,
487 peer: SocketAddr,
488) -> Result<Response<HttpBody>, AppError> {
489 let info = assigned.info;
490 if let Some(header_info) = &info.headers {
492 header_info.request.apply(&mut headers);
493 }
494 if let Some(flow) = &info.flow {
497 headers.insert("x-flow-id".to_string(), flow.clone());
498 }
499 let trace_header = info
501 .trace_id_header
502 .as_deref()
503 .unwrap_or(&state.trace_header)
504 .to_lowercase();
505 let cid_header = info
506 .correlation_id_header
507 .as_deref()
508 .unwrap_or(&state.cid_header)
509 .to_lowercase();
510 let traceparent = headers
519 .get(w3c_trace::TRACEPARENT)
520 .and_then(|value| w3c_trace::parse(value))
521 .or_else(|| {
522 let traceparent_header = info
523 .traceparent_header
524 .as_deref()
525 .unwrap_or(&state.traceparent_header)
526 .to_lowercase();
527 if traceparent_header == w3c_trace::TRACEPARENT {
528 None
529 } else {
530 headers
531 .get(&traceparent_header)
532 .and_then(|value| w3c_trace::parse(value))
533 }
534 });
535 let (trace_id, parent_span) = match &traceparent {
536 Some((trace_id, parent)) => (Some(trace_id.clone()), Some(parent.clone())),
537 None => (headers.get(&trace_header).cloned(), None),
538 };
539 let trace_id = if info.tracing {
540 Some(trace_id.unwrap_or_else(trace::new_trace_id))
541 } else {
542 None
543 };
544 let cid = headers.get(&cid_header).cloned().unwrap_or_else(|| {
547 if cid_header == trace_header {
548 trace_id
549 .clone()
550 .unwrap_or_else(|| uuid::Uuid::new_v4().simple().to_string())
551 } else {
552 uuid::Uuid::new_v4().simple().to_string()
553 }
554 });
555 headers.insert(cid_header.clone(), cid.clone());
559 headers
567 .entry("x-ttl".to_string())
568 .or_insert_with(|| (info.timeout.as_secs().max(1) * 1000).to_string());
569 let mut query: HashMap<String, serde_json::Value> = HashMap::new();
574 for pair in query_text.split('&').filter(|p| !p.is_empty()) {
575 let (name, value) = pair.split_once('=').unwrap_or((pair, ""));
576 let (name, value) = (url_decode(name), url_decode(value));
577 match query.get_mut(&name) {
578 None => {
579 query.insert(name, serde_json::Value::String(value));
580 }
581 Some(serde_json::Value::Array(values)) => {
582 values.push(serde_json::Value::String(value));
583 }
584 Some(existing) => {
585 let first = existing.clone();
586 *existing = serde_json::Value::Array(vec![first, serde_json::Value::String(value)]);
587 }
588 }
589 }
590 let path_params: HashMap<String, String> = assigned
591 .path_params
592 .iter()
593 .map(|(k, v)| (k.clone(), url_decode(v)))
594 .collect();
595 let cookies: HashMap<String, String> = headers
599 .remove("cookie")
600 .map(|header| {
601 header
602 .split(';')
603 .filter_map(|item| item.split_once('='))
604 .map(|(name, value)| (name.trim().to_string(), value.trim().to_string()))
605 .collect()
606 })
607 .unwrap_or_default();
608 let accept = headers.get("accept").cloned();
612 let parsed = parse_body(&headers, &body_bytes);
613 if let ParsedBody::Form(form) = &parsed {
617 for (name, value) in form {
618 query.insert(name.clone(), serde_json::Value::String(value.clone()));
619 }
620 }
621 let mut http_request = crate::automation::AsyncHttpRequest::new()
628 .set_method(&method)
629 .set_url(&path)
630 .set_remote_ip(&peer.ip().to_string())
631 .set_secure(headers.get("x-forwarded-proto").map(String::as_str) == Some("https"))
634 .set_target_host(&headers.get("host").cloned().unwrap_or_default())
635 .set_route_timeout_seconds(info.timeout.as_secs());
638 for (key, value) in &headers {
639 http_request = http_request.set_header(key, value);
640 }
641 for (key, value) in &path_params {
642 http_request = http_request.set_path_parameter(key, value);
643 }
644 for (key, value) in &query {
645 http_request = match value {
646 serde_json::Value::Array(values) => {
647 let values: Vec<&str> = values
648 .iter()
649 .map(|v| v.as_str().unwrap_or_default())
650 .collect();
651 http_request.set_query_parameter_values(key, &values)
652 }
653 serde_json::Value::String(value) => http_request.set_query_parameter(key, value),
654 other => http_request.set_query_parameter(key, &other.to_string()),
655 };
656 }
657 http_request = match &parsed {
662 ParsedBody::Value(value) => http_request
663 .set_body(rmpv::ext::to_value(value).map_err(|e| AppError::new(500, e.to_string()))?),
664 ParsedBody::Bytes(bytes) => http_request.set_body(rmpv::Value::Binary(bytes.clone())),
665 ParsedBody::Form(_) => http_request.set_body(rmpv::Value::Nil),
666 };
667 if !query_text.is_empty() {
670 http_request = http_request.set_query_string(&query_text);
671 }
672 for (key, value) in &cookies {
673 http_request = http_request.set_cookie(key, value);
674 }
675 let po = PostOffice::new(&state.platform);
676 let trace_path = if query_text.is_empty() {
678 format!("{method} {path}")
679 } else {
680 format!("{method} {path}?{query_text}")
681 };
682 if let Some(auth_route) = &info.authentication {
685 let auth_event = build_event(
686 auth_route,
687 &http_request,
688 &cid,
689 &trace_id,
690 &trace_path,
691 &parent_span,
692 )?;
693 let verdict = po.request(auth_event, info.timeout).await?;
694 if verdict.has_error() {
695 return Err(AppError::new(
696 verdict.status(),
697 verdict
698 .body_as::<String>()
699 .unwrap_or_else(|_| "Unauthorized".to_string()),
700 ));
701 }
702 if !verdict.body_as::<bool>().unwrap_or(false) {
703 return Err(AppError::new(401, "Unauthorized"));
704 }
705 for (key, value) in verdict.headers() {
709 http_request = http_request.set_session_info(key, value);
710 }
711 }
712 let is_head = method == "HEAD";
713 let envelope_stream = !is_head && is_event_api_stream(info, &http_request);
718 let result = if (info.stream_response && !is_head) || envelope_stream {
721 match stream_dispatch(
722 state,
723 info,
724 &http_request,
725 &cid,
726 &cid_header,
727 &trace_id,
728 &trace_path,
729 &parent_span,
730 accept.clone(),
731 envelope_stream,
732 )
733 .await?
734 {
735 StreamOutcome::Streaming(response) => return Ok(response),
736 StreamOutcome::SingleShot(envelope) => envelope,
737 }
738 } else {
739 let context_id = uuid::Uuid::new_v4().simple().to_string();
745 let (tx, rx) = oneshot::channel();
746 pending_responses()
747 .lock()
748 .expect("pending http contexts poisoned")
749 .insert(context_id.clone(), tx);
750 let event = build_event(
751 &info.service,
752 &http_request,
753 &cid,
754 &trace_id,
755 &trace_path,
756 &parent_span,
757 )?
758 .set_correlation_id(&context_id)
759 .set_reply_to(ASYNC_HTTP_RESPONSE);
760 if let Err(e) = po.send(event).await {
761 pending_responses()
762 .lock()
763 .expect("pending http contexts poisoned")
764 .remove(&context_id);
765 return Err(e);
766 }
767 match tokio::time::timeout(info.timeout, rx).await {
768 Ok(Ok(envelope)) => envelope,
769 Ok(Err(_)) => {
770 return Err(AppError::new(500, "Response channel closed unexpectedly"));
771 }
772 Err(_) => {
773 pending_responses()
774 .lock()
775 .expect("pending http contexts poisoned")
776 .remove(&context_id);
777 return Err(AppError::new(
778 408,
779 format!("Timeout for {} ms", info.timeout.as_millis()),
780 ));
781 }
782 }
783 };
784 let status = status_of(result.status());
787 let mut content_type: Option<String> = None;
788 let mut set_cookies: Vec<String> = Vec::new();
789 let mut response_headers: HashMap<String, String> = HashMap::new();
790 for (name, value) in result.headers() {
791 let key = name.to_lowercase();
792 if PROTECTED_METADATA.contains(&key.as_str()) {
795 continue;
796 }
797 match key.as_str() {
798 "x-stream-id" if value.starts_with("stream.") && value.contains(".in") => {}
802 "x-ttl" => {}
803 "content-type" => {
806 if !is_head {
807 content_type = Some(value.to_lowercase());
808 }
809 }
810 "set-cookie" => {
813 set_cookies.extend(value.split('|').map(|c| c.trim().to_string()));
814 }
815 _ => {
816 response_headers.insert(key, value.clone());
817 }
818 }
819 }
820 if content_type.is_none() && !is_head {
825 content_type = accept_fallback_type(accept.as_deref(), result.body());
826 }
827 let payload = render_payload(result.body(), content_type.as_deref());
828 if let Some(header_info) = &info.headers {
831 header_info.response.apply(&mut response_headers);
832 }
833 response_headers.entry(cid_header.clone()).or_insert(cid);
838 if let Some(content_type) = content_type {
839 response_headers.insert("content-type".to_string(), content_type);
840 }
841 if let Some(cors) = &info.cors {
842 for (name, value) in &cors.headers {
843 response_headers.insert(name.to_lowercase(), value.clone());
844 }
845 }
846 let mut response = Response::builder().status(status);
847 for (name, value) in response_headers {
848 response = response.header(name, value);
849 }
850 for cookie in set_cookies {
851 if !cookie.is_empty() {
852 response = response.header("set-cookie", cookie);
853 }
854 }
855 let payload = if is_head { Bytes::new() } else { payload };
857 response
858 .body(full(payload))
859 .map_err(|e| AppError::new(500, e.to_string()))
860}
861
862#[allow(clippy::large_enum_variant)]
867enum StreamOutcome {
868 Streaming(Response<HttpBody>),
869 SingleShot(EventEnvelope),
870}
871
872fn stream_marker(event: &EventEnvelope) -> Result<Option<&'static str>, ()> {
876 for (name, value) in event.headers() {
877 if name.eq_ignore_ascii_case(event_stream::X_EVENT_STREAM) {
878 return match value.to_lowercase().as_str() {
879 event_stream::DATA => Ok(Some(event_stream::DATA)),
880 event_stream::EOF => Ok(Some(event_stream::EOF)),
881 event_stream::EXCEPTION => Ok(Some(event_stream::EXCEPTION)),
882 _ => Err(()),
883 };
884 }
885 }
886 Ok(None)
887}
888
889fn stream_error_message(event: &EventEnvelope) -> String {
891 match event.body() {
892 rmpv::Value::Map(entries) => entries
893 .iter()
894 .find(|(key, _)| key.as_str() == Some("message"))
895 .map(|(_, value)| stream_text(value))
896 .unwrap_or_else(|| "Stream failed".to_string()),
897 rmpv::Value::Nil => "Stream failed".to_string(),
898 other => stream_text(other),
899 }
900}
901
902fn negotiate_stream_type(accept: Option<&str>) -> String {
905 let Some(accept) = accept else {
906 return "application/json".to_string();
907 };
908 if accept.contains("*/*") || accept.contains("application/json") {
909 "application/json".to_string()
910 } else if accept.contains("text/event-stream") {
911 "text/event-stream".to_string()
912 } else if accept.contains("text/html") {
913 "text/html".to_string()
914 } else if accept.contains("application/xml") {
915 "application/xml".to_string()
916 } else {
917 "text/plain".to_string()
918 }
919}
920
921fn stream_text(body: &rmpv::Value) -> String {
925 match body {
926 rmpv::Value::Nil => String::new(),
927 rmpv::Value::String(text) => text.as_str().unwrap_or_default().to_string(),
928 rmpv::Value::Binary(bytes) => String::from_utf8_lossy(bytes).to_string(),
929 other => {
930 let stripped = crate::serializer::strip_nulls(other);
931 let json = serde_json::to_value(&stripped).unwrap_or_default();
932 serde_json::to_string(&json).unwrap_or_default()
933 }
934 }
935}
936
937fn sse_frame(event_name: Option<&str>, text: &str) -> Bytes {
940 let mut frame = String::new();
941 if let Some(name) = event_name.filter(|n| !n.is_empty()) {
942 frame.push_str("event: ");
943 frame.push_str(name);
944 frame.push('\n');
945 }
946 for line in text.split('\n') {
947 frame.push_str("data: ");
948 frame.push_str(line);
949 frame.push('\n');
950 }
951 frame.push('\n');
952 Bytes::from(frame)
953}
954
955fn chunk_bytes(body: &rmpv::Value) -> Bytes {
958 match body {
959 rmpv::Value::Nil => Bytes::new(),
960 rmpv::Value::String(text) => Bytes::from(text.as_str().unwrap_or_default().to_string()),
961 rmpv::Value::Binary(bytes) => Bytes::from(bytes.clone()),
962 other => {
963 let mut line = stream_text(other);
964 line.push('\n');
965 Bytes::from(line)
966 }
967 }
968}
969
970fn stream_event_name(event: &EventEnvelope) -> Option<&str> {
972 event
973 .headers()
974 .iter()
975 .find(|(name, _)| name.eq_ignore_ascii_case(event_stream::X_EVENT_NAME))
976 .map(|(_, value)| value.as_str())
977}
978
979fn is_event_api_stream(info: &RouteInfo, request: &crate::automation::AsyncHttpRequest) -> bool {
985 info.service == super::event_api::EVENT_API_SERVICE
986 && request.header("x-async") != Some("true")
987 && request
988 .header("accept")
989 .is_some_and(|accept| accept.contains("text/event-stream"))
990}
991
992fn event_api_idle(request: &crate::automation::AsyncHttpRequest) -> Duration {
996 let ttl_ms = request
997 .header("x-ttl")
998 .and_then(|v| v.trim().parse::<u64>().ok())
999 .unwrap_or(0)
1000 .max(1000);
1001 Duration::from_millis(ttl_ms)
1002}
1003
1004#[allow(clippy::too_many_arguments)]
1013async fn stream_dispatch(
1014 state: &RouterState,
1015 info: &RouteInfo,
1016 http_request: &crate::automation::AsyncHttpRequest,
1017 cid: &str,
1018 cid_header: &str,
1019 trace_id: &Option<String>,
1020 trace_path: &str,
1021 parent_span: &Option<String>,
1022 accept: Option<String>,
1023 envelope_mode: bool,
1024) -> Result<StreamOutcome, AppError> {
1025 let Some(lane) = checkout_lane() else {
1028 return Err(AppError::new(503, "Streaming response pool exhausted"));
1029 };
1030 let po = PostOffice::new(&state.platform);
1031 let context_id = uuid::Uuid::new_v4().simple().to_string();
1032 let (tx, mut rx) = mpsc::channel::<EventEnvelope>(STREAM_EVENT_BUFFER);
1033 pending_streams()
1034 .lock()
1035 .expect("pending streams poisoned")
1036 .insert(context_id.clone(), tx);
1037 let event = build_event(
1038 &info.service,
1039 http_request,
1040 cid,
1041 trace_id,
1042 trace_path,
1043 parent_span,
1044 )?
1045 .set_correlation_id(&context_id)
1046 .set_reply_to(&lane);
1047 if let Err(e) = po.send(event).await {
1048 cleanup_stream(&context_id, &lane);
1049 return Err(e);
1050 }
1051 let base_idle = if envelope_mode {
1054 event_api_idle(http_request)
1055 } else {
1056 info.timeout
1057 };
1058 let (first, marker) = loop {
1060 match tokio::time::timeout(base_idle, rx.recv()).await {
1061 Ok(Some(envelope)) => match stream_marker(&envelope) {
1062 Ok(Some(marker)) => break (envelope, Some(marker)),
1063 Ok(None) => break (envelope, None),
1064 Err(()) => {
1065 log::warn!(
1067 "Dropping event for {context_id} - invalid {} signal",
1068 event_stream::X_EVENT_STREAM
1069 );
1070 }
1071 },
1072 Ok(None) => {
1073 cleanup_stream(&context_id, &lane);
1074 return Err(AppError::new(500, "Response channel closed unexpectedly"));
1075 }
1076 Err(_) => {
1077 cleanup_stream(&context_id, &lane);
1078 return Err(AppError::new(
1079 408,
1080 format!("Timeout for {} ms", base_idle.as_millis()),
1081 ));
1082 }
1083 }
1084 };
1085 let Some(marker) = marker else {
1086 cleanup_stream(&context_id, &lane);
1091 let reply = if envelope_mode {
1092 wire_single_shot(first)?
1093 } else {
1094 first
1095 };
1096 return Ok(StreamOutcome::SingleShot(reply));
1097 };
1098 if marker == event_stream::EXCEPTION && !envelope_mode {
1099 cleanup_stream(&context_id, &lane);
1103 let status = if first.status() >= 400 {
1104 first.status()
1105 } else {
1106 500
1107 };
1108 return Err(AppError::new(status, stream_error_message(&first)));
1109 }
1110 if first
1112 .headers()
1113 .keys()
1114 .any(|k| k.eq_ignore_ascii_case("x-stream-id"))
1115 {
1116 log::warn!("Ignoring x-stream-id on a streaming response for {context_id}");
1118 }
1119 let mut response_headers: HashMap<String, String> = HashMap::new();
1120 let mut set_cookies: Vec<String> = Vec::new();
1121 let mut content_type: Option<String> = None;
1122 let mut idle_override: Option<Duration> = None;
1123 for (name, value) in first.headers() {
1124 let key = name.to_lowercase();
1125 match key.as_str() {
1126 event_stream::X_EVENT_STREAM | event_stream::X_EVENT_NAME | "x-stream-id" => {}
1128 "x-ttl" => {
1130 if let Ok(seconds) = value.trim().parse::<u64>() {
1131 if seconds > 0 {
1132 idle_override = Some(Duration::from_secs(seconds));
1133 }
1134 }
1135 }
1136 _ if envelope_mode => {}
1139 "content-type" => content_type = Some(value.to_lowercase()),
1140 "set-cookie" => {
1141 set_cookies.extend(value.split('|').map(|c| c.trim().to_string()));
1142 }
1143 _ => {
1144 response_headers.insert(key, value.clone());
1145 }
1146 }
1147 }
1148 if let Some(header_info) = &info.headers {
1151 header_info.response.apply(&mut response_headers);
1152 }
1153 response_headers
1155 .entry(cid_header.to_string())
1156 .or_insert_with(|| cid.to_string());
1157 if let Some(cors) = &info.cors {
1158 for (name, value) in &cors.headers {
1159 response_headers.insert(name.to_lowercase(), value.clone());
1160 }
1161 }
1162 let content_type = if envelope_mode {
1164 "text/event-stream".to_string()
1165 } else {
1166 content_type.unwrap_or_else(|| negotiate_stream_type(accept.as_deref()))
1167 };
1168 let sse = content_type.starts_with("text/event-stream");
1169 if sse {
1170 response_headers
1172 .entry("cache-control".to_string())
1173 .or_insert_with(|| "no-cache".to_string());
1174 }
1175 let idle = idle_override.unwrap_or(base_idle);
1176 let mut builder = Response::builder().status(status_of(first.status()));
1177 for (name, value) in &response_headers {
1178 builder = builder.header(name, value);
1179 }
1180 for cookie in set_cookies {
1181 if !cookie.is_empty() {
1182 builder = builder.header("set-cookie", cookie);
1183 }
1184 }
1185 builder = builder.header("content-type", &content_type);
1186 let (body_tx, body_rx) = mpsc::channel::<Frame<Bytes>>(STREAM_FRAME_BUFFER);
1187 let response = builder
1188 .body(BoxBody::new(ChannelBody { rx: body_rx }))
1189 .map_err(|e| AppError::new(500, e.to_string()))?;
1190 tokio::spawn(render_stream(
1191 rx,
1192 body_tx,
1193 sse,
1194 idle,
1195 context_id,
1196 lane,
1197 first,
1198 marker,
1199 envelope_mode,
1200 ));
1201 Ok(StreamOutcome::Streaming(response))
1202}
1203
1204#[allow(clippy::large_enum_variant)]
1208enum Waited {
1209 Event(EventEnvelope),
1210 Idle,
1211 Closed,
1212}
1213
1214async fn next_stream_event(
1218 rx: &mut mpsc::Receiver<EventEnvelope>,
1219 body_tx: &mpsc::Sender<Frame<Bytes>>,
1220 sse: bool,
1221 idle: Duration,
1222) -> Waited {
1223 let ping_every = keep_alive_ms();
1224 let idle_deadline = tokio::time::sleep(idle);
1225 tokio::pin!(idle_deadline);
1226 loop {
1227 if sse && ping_every > 0 {
1228 let ping = tokio::time::sleep(Duration::from_millis(ping_every));
1229 tokio::pin!(ping);
1230 tokio::select! {
1231 received = rx.recv() => {
1232 return match received {
1233 Some(event) => Waited::Event(event),
1234 None => Waited::Closed,
1235 };
1236 }
1237 _ = &mut idle_deadline => return Waited::Idle,
1238 _ = &mut ping => {
1239 let _ = body_tx.try_send(Frame::data(Bytes::from_static(b": ping\n\n")));
1240 }
1241 }
1242 } else {
1243 tokio::select! {
1244 received = rx.recv() => {
1245 return match received {
1246 Some(event) => Waited::Event(event),
1247 None => Waited::Closed,
1248 };
1249 }
1250 _ = &mut idle_deadline => return Waited::Idle,
1251 }
1252 }
1253 }
1254}
1255
1256async fn push_frame(
1261 body_tx: &mpsc::Sender<Frame<Bytes>>,
1262 idle: Duration,
1263 context_id: &str,
1264 bytes: Bytes,
1265) -> bool {
1266 if bytes.is_empty() {
1267 return true;
1268 }
1269 match tokio::time::timeout(idle, body_tx.send(Frame::data(bytes))).await {
1270 Ok(Ok(())) => true,
1271 Ok(Err(_)) => {
1272 log::debug!("Client disconnected from event stream {context_id}");
1273 false
1274 }
1275 Err(_) => {
1276 log::error!("Closing event stream for {context_id} - client too slow");
1277 false
1278 }
1279 }
1280}
1281
1282#[allow(clippy::too_many_arguments)]
1291async fn render_stream(
1292 mut rx: mpsc::Receiver<EventEnvelope>,
1293 body_tx: mpsc::Sender<Frame<Bytes>>,
1294 sse: bool,
1295 idle: Duration,
1296 context_id: String,
1297 lane: String,
1298 first: EventEnvelope,
1299 first_marker: &'static str,
1300 envelope_mode: bool,
1301) {
1302 let mut pending = Some((first, first_marker));
1303 let mut first_frame = true;
1304 loop {
1305 let (event, marker) = match pending.take() {
1306 Some(next) => next,
1307 None => match next_stream_event(&mut rx, &body_tx, sse, idle).await {
1308 Waited::Event(event) => match stream_marker(&event) {
1309 Ok(Some(marker)) => (event, marker),
1310 Ok(None) | Err(()) => {
1311 log::warn!(
1312 "Dropping event for {context_id} - invalid {} signal",
1313 event_stream::X_EVENT_STREAM
1314 );
1315 continue;
1316 }
1317 },
1318 Waited::Idle => {
1319 if envelope_mode {
1321 let frame = idle_timeout_envelope_frame(idle);
1322 let _ = push_frame(&body_tx, idle, &context_id, frame).await;
1323 } else if sse {
1324 let error = serde_json::json!({
1325 "status": 408,
1326 "message": format!("Timeout for {} seconds", idle.as_secs()),
1327 "type": "error",
1328 });
1329 let frame = sse_frame(Some("error"), &error.to_string());
1330 let _ = push_frame(&body_tx, idle, &context_id, frame).await;
1331 }
1332 break;
1333 }
1334 Waited::Closed => break,
1335 },
1336 };
1337 match marker {
1338 event_stream::DATA => {
1339 let bytes = if envelope_mode {
1340 envelope_mode_data_frame(&event, first_frame)
1341 } else if sse {
1342 if matches!(event.body(), rmpv::Value::Nil) {
1343 Bytes::new()
1344 } else {
1345 sse_frame(stream_event_name(&event), &stream_text(event.body()))
1346 }
1347 } else {
1348 chunk_bytes(event.body())
1349 };
1350 first_frame = false;
1351 if !push_frame(&body_tx, idle, &context_id, bytes).await {
1352 break;
1353 }
1354 }
1355 event_stream::EOF => {
1356 if envelope_mode {
1357 let frame = envelope_wire_frame(&event);
1358 let _ = push_frame(&body_tx, idle, &context_id, frame).await;
1359 } else if sse {
1360 let text = if matches!(event.body(), rmpv::Value::Nil) {
1361 "{}".to_string()
1362 } else {
1363 stream_text(event.body())
1364 };
1365 let frame = sse_frame(Some("done"), &text);
1366 let _ = push_frame(&body_tx, idle, &context_id, frame).await;
1367 }
1368 break;
1369 }
1370 _ => {
1371 if envelope_mode {
1375 let frame = envelope_wire_frame(&event);
1376 let _ = push_frame(&body_tx, idle, &context_id, frame).await;
1377 } else if sse {
1378 let status = if event.status() >= 400 {
1379 event.status()
1380 } else {
1381 500
1382 };
1383 let error = serde_json::json!({
1384 "status": status,
1385 "message": stream_error_message(&event),
1386 "type": "error",
1387 });
1388 let frame = sse_frame(Some("error"), &error.to_string());
1389 let _ = push_frame(&body_tx, idle, &context_id, frame).await;
1390 }
1391 break;
1392 }
1393 }
1394 }
1395 cleanup_stream(&context_id, &lane);
1396}
1397
1398fn envelope_mode_data_frame(event: &EventEnvelope, first_frame: bool) -> Bytes {
1403 if first_frame || !raw_streamable(event) {
1404 envelope_wire_frame(event)
1405 } else if matches!(event.body(), rmpv::Value::Nil) {
1406 Bytes::new()
1407 } else {
1408 sse_frame(stream_event_name(event), &stream_text(event.body()))
1409 }
1410}
1411
1412fn raw_streamable(event: &EventEnvelope) -> bool {
1418 if event.status() != 200 {
1419 return false;
1420 }
1421 for (name, value) in event.headers() {
1422 let key = name.to_lowercase();
1423 let reserved = key == event_stream::X_EVENT_STREAM
1424 || key == event_stream::X_EVENT_NAME
1425 || key == "x-ttl";
1426 if !reserved || (key == event_stream::X_EVENT_NAME && value == event_stream::ENVELOPE) {
1427 return false;
1428 }
1429 }
1430 match event.body() {
1431 rmpv::Value::Nil => true,
1432 rmpv::Value::String(text) => !text.as_str().unwrap_or_default().contains('\r'),
1433 _ => false,
1434 }
1435}
1436
1437fn wire_single_shot(result: EventEnvelope) -> Result<EventEnvelope, AppError> {
1441 let bytes = result.clear_to().clear_reply_to().to_bytes()?;
1442 Ok(EventEnvelope::new()
1443 .set_status(200)
1444 .set_header("content-type", "application/octet-stream")
1445 .set_raw_body(rmpv::Value::Binary(bytes)))
1446}
1447
1448fn envelope_wire_frame(event: &EventEnvelope) -> Bytes {
1453 use base64::Engine as _;
1454 let wire = event.clone().clear_to().clear_reply_to();
1455 match wire.to_bytes() {
1456 Ok(bytes) => sse_frame(
1457 Some(event_stream::ENVELOPE),
1458 &base64::engine::general_purpose::STANDARD.encode(bytes),
1459 ),
1460 Err(_) => Bytes::new(),
1461 }
1462}
1463
1464fn idle_timeout_envelope_frame(idle: Duration) -> Bytes {
1468 let message = format!("Timeout for {} seconds", idle.as_secs());
1469 let error = EventEnvelope::new()
1470 .set_header(event_stream::X_EVENT_STREAM, event_stream::EXCEPTION)
1471 .set_status(408)
1472 .set_body(serde_json::json!({"type": "error", "status": 408, "message": message}));
1473 match error {
1474 Ok(envelope) => envelope_wire_frame(&envelope),
1475 Err(_) => Bytes::new(),
1476 }
1477}
1478
1479fn build_event(
1480 to: &str,
1481 http_request: &crate::automation::AsyncHttpRequest,
1482 cid: &str,
1483 trace_id: &Option<String>,
1484 trace_path: &str,
1485 parent_span: &Option<String>,
1486) -> Result<EventEnvelope, AppError> {
1487 let mut event = EventEnvelope::new()
1488 .set_to(to)
1489 .set_from("http.request")
1490 .set_correlation_id(cid)
1491 .add_tag(crate::post_office::BUSINESS_CID_TAG, cid)
1496 .set_raw_body(http_request.to_value());
1500 if let Some(trace_id) = trace_id {
1501 event = event.set_trace(trace_id, trace_path);
1502 if let Some(parent) = parent_span {
1503 event = event.set_span_id(parent);
1505 }
1506 }
1507 Ok(event)
1508}
1509
1510enum ParsedBody {
1512 Value(serde_json::Value),
1514 Form(HashMap<String, String>),
1516 Bytes(Vec<u8>),
1518}
1519
1520fn base_content_type(headers: &HashMap<String, String>) -> Option<String> {
1525 headers
1526 .get("content-type")
1527 .map(|ct| ct.split(';').next().unwrap_or(ct).trim().to_string())
1528}
1529
1530fn parse_body(headers: &HashMap<String, String>, bytes: &Bytes) -> ParsedBody {
1547 let content_type = base_content_type(headers);
1548 let ct = content_type.as_deref().unwrap_or("?");
1549 if ct.starts_with("application/json") {
1550 let text = String::from_utf8_lossy(bytes).to_string();
1551 let trimmed = text.trim();
1552 let parsed = if trimmed.is_empty() {
1553 Some(serde_json::Value::Object(serde_json::Map::new()))
1554 } else if (trimmed.starts_with('{') && trimmed.ends_with('}'))
1555 || (trimmed.starts_with('[') && trimmed.ends_with(']'))
1556 {
1557 serde_json::from_str(&text).ok()
1558 } else {
1559 None
1560 };
1561 ParsedBody::Value(parsed.unwrap_or(serde_json::Value::String(text)))
1562 } else if ct == "application/x-www-form-urlencoded" {
1563 let text = String::from_utf8_lossy(bytes);
1564 let mut form = HashMap::new();
1565 for pair in text.split('&').filter(|p| !p.is_empty()) {
1566 let (name, value) = pair.split_once('=').unwrap_or((pair, ""));
1567 form.insert(url_decode(name), url_decode(value));
1568 }
1569 ParsedBody::Form(form)
1570 } else if ct.starts_with("application/xml")
1571 || ct.starts_with("text/html")
1572 || ct.starts_with("text/plain")
1573 {
1574 ParsedBody::Value(serde_json::Value::String(
1575 String::from_utf8_lossy(bytes).to_string(),
1576 ))
1577 } else if bytes.is_empty() {
1578 ParsedBody::Value(serde_json::Value::Null)
1579 } else {
1580 ParsedBody::Bytes(bytes.to_vec())
1581 }
1582}
1583
1584fn url_decode(text: &str) -> String {
1586 let bytes = text.as_bytes();
1587 let mut out = Vec::with_capacity(bytes.len());
1588 let mut i = 0;
1589 while i < bytes.len() {
1590 match bytes[i] {
1591 b'+' => {
1592 out.push(b' ');
1593 i += 1;
1594 }
1595 b'%' if i + 2 < bytes.len() => {
1596 let hex = std::str::from_utf8(&bytes[i + 1..i + 3]).ok();
1597 match hex.and_then(|h| u8::from_str_radix(h, 16).ok()) {
1598 Some(byte) => {
1599 out.push(byte);
1600 i += 3;
1601 }
1602 None => {
1603 out.push(bytes[i]);
1604 i += 1;
1605 }
1606 }
1607 }
1608 other => {
1609 out.push(other);
1610 i += 1;
1611 }
1612 }
1613 }
1614 String::from_utf8_lossy(&out).to_string()
1615}
1616
1617const DEFAULT_REST_YAML: &str = include_str!("../../resources/default-rest.yaml");
1624
1625fn merge_default_endpoints(table: &mut RoutingTable) -> Result<(), AppError> {
1626 let defaults = RoutingTable::from_yaml_text(DEFAULT_REST_YAML)?;
1627 for route in defaults.routes() {
1628 if !table.has_url(&route.url) {
1629 table.add_route(route.clone());
1630 }
1631 }
1632 Ok(())
1633}
1634
1635async fn serve_static(
1653 state: &RouterState,
1654 path: &str,
1655 query_text: &str,
1656 headers: &HashMap<String, String>,
1657 peer: SocketAddr,
1658 head_only: bool,
1659) -> Option<Response<HttpBody>> {
1660 let (bytes, filename) = resolve_static_file(path)?;
1661 let static_content = state.table.static_content();
1662 let no_cache = super::routing::matched_element(&static_content.no_cache_pages, path);
1663 let mut filter_headers: Vec<(String, String)> = Vec::new();
1665 if let Some(filter) = &static_content.filter {
1666 let applies = super::routing::matched_element(&filter.path_list, path)
1667 && !super::routing::matched_element(&filter.exclusion_list, path);
1668 if applies {
1669 if state.platform.has_route(&filter.service) {
1670 match run_static_filter(state, filter, path, query_text, headers, peer).await {
1671 Ok(filtered) => {
1672 for (name, value) in filtered.headers() {
1674 filter_headers.push((name.clone(), value.clone()));
1675 }
1676 if filtered.status() != 200 {
1677 let (content_type, payload) = envelope_payload(&filtered);
1679 let mut response =
1680 Response::builder().status(status_of(filtered.status()));
1681 let mut has_content_type = false;
1682 for (name, value) in &filter_headers {
1683 has_content_type |= name.eq_ignore_ascii_case("content-type");
1684 response = response.header(name, value);
1685 }
1686 if let (Some(content_type), false) = (content_type, has_content_type) {
1687 response = response.header("content-type", content_type);
1688 }
1689 return response.body(full(payload)).ok();
1690 }
1691 }
1692 Err(e) => {
1693 log::error!(
1696 "Unable to filter static content HTTP-GET {} - {}",
1697 filter.service,
1698 e.message()
1699 );
1700 }
1701 }
1702 } else {
1703 log::warn!(
1704 "Static content filter {} ignored because it does not exist",
1705 filter.service
1706 );
1707 }
1708 }
1709 }
1710 let mime = mime_for(
1712 std::path::Path::new(&filename)
1713 .extension()
1714 .and_then(|e| e.to_str())
1715 .unwrap_or(""),
1716 );
1717 let mut response = Response::builder().status(StatusCode::OK);
1718 for (name, value) in &filter_headers {
1719 response = response.header(name, value);
1720 }
1721 response = response.header("content-type", mime);
1722 if no_cache {
1723 response = response
1724 .header("Cache-Control", "no-cache, no-store")
1725 .header("Pragma", "no-cache")
1726 .header("Expires", "Thu, 01 Jan 1970 00:00:00 GMT");
1727 } else {
1728 use sha2::Digest;
1729 let etag = format!("\"{:x}\"", sha2::Sha256::digest(&bytes));
1730 let matched = headers
1732 .get("if-none-match")
1733 .is_some_and(|inm| inm.split(',').any(|tag| tag.trim() == etag));
1734 if matched {
1735 return Response::builder()
1736 .status(StatusCode::NOT_MODIFIED)
1737 .header("content-length", "0")
1738 .body(full(Bytes::new()))
1739 .ok();
1740 }
1741 response = response.header("ETag", etag);
1742 }
1743 let payload = if head_only {
1744 Bytes::new()
1745 } else {
1746 Bytes::from(bytes)
1747 };
1748 response.body(full(payload)).ok()
1749}
1750
1751fn resolve_static_file(path: &str) -> Option<(Vec<u8>, String)> {
1754 if path.contains("..") {
1755 return None; }
1757 let rel = path.trim_start_matches('/');
1758 let relative = if rel.is_empty() || path.ends_with('/') {
1759 format!("{rel}/index.html")
1760 .trim_start_matches('/')
1761 .to_string()
1762 } else {
1763 let filename = rel.rsplit('/').next().unwrap_or(rel);
1764 if filename.contains('.') {
1765 rel.to_string()
1766 } else {
1767 format!("{rel}.html") }
1769 };
1770 let file = crate::util::resources::resolve_classpath(&format!("public/{relative}"))?;
1771 let bytes = std::fs::read(&file).ok()?;
1772 let filename = relative.rsplit('/').next().unwrap_or(&relative).to_string();
1773 Some((bytes, filename))
1774}
1775
1776async fn run_static_filter(
1779 state: &RouterState,
1780 filter: &super::routing::SimpleHttpFilter,
1781 path: &str,
1782 query_text: &str,
1783 headers: &HashMap<String, String>,
1784 peer: SocketAddr,
1785) -> Result<EventEnvelope, AppError> {
1786 let mut request = crate::automation::AsyncHttpRequest::new()
1790 .set_method("GET")
1791 .set_url(path)
1792 .set_remote_ip(&peer.ip().to_string())
1793 .set_secure(false)
1794 .set_target_host(&headers.get("host").cloned().unwrap_or_default())
1795 .set_body(rmpv::Value::Nil);
1796 for (key, value) in headers {
1797 request = request.set_header(key, value);
1798 }
1799 for pair in query_text.split('&').filter(|p| !p.is_empty()) {
1800 let (name, value) = pair.split_once('=').unwrap_or((pair, ""));
1801 request = request.set_query_parameter(&url_decode(name), &url_decode(value));
1802 }
1803 let event = EventEnvelope::new()
1804 .set_to(&filter.service)
1805 .set_raw_body(request.to_value());
1806 let po = PostOffice::new(&state.platform);
1807 po.request(event, std::time::Duration::from_secs(10)).await
1809}
1810
1811fn accept_fallback_type(accept: Option<&str>, _body: &rmpv::Value) -> Option<String> {
1820 let accept = accept?;
1821 if accept.contains("text/html") {
1822 Some("text/html".to_string())
1823 } else if accept.contains("application/json")
1824 || accept.contains("*/*")
1825 || accept.contains("application/xml")
1826 {
1827 Some("application/json".to_string())
1828 } else {
1829 Some("text/plain".to_string())
1830 }
1831}
1832
1833fn render_payload(body: &rmpv::Value, content_type: Option<&str>) -> Bytes {
1839 match body {
1840 rmpv::Value::Nil => Bytes::new(),
1841 rmpv::Value::String(text) => Bytes::from(text.as_str().unwrap_or_default().to_string()),
1842 rmpv::Value::Binary(bytes) => Bytes::from(bytes.clone()),
1843 _ => {
1844 let stripped = crate::serializer::strip_nulls(body);
1846 let json = serde_json::to_value(&stripped).unwrap_or_default();
1847 let text = serde_json::to_string_pretty(&json).unwrap_or_default();
1854 if content_type.is_some_and(|t| t.starts_with("text/html"))
1855 && matches!(body, rmpv::Value::Map(_) | rmpv::Value::Array(_))
1856 {
1857 Bytes::from(format!("<html><body><pre>\n{text}\n</pre></body></html>"))
1858 } else {
1859 Bytes::from(text)
1860 }
1861 }
1862 }
1863}
1864
1865fn envelope_payload(result: &EventEnvelope) -> (Option<&'static str>, Bytes) {
1866 match result.body() {
1867 rmpv::Value::Nil => (None, Bytes::new()),
1868 rmpv::Value::String(text) => (
1869 Some("text/plain"),
1870 Bytes::from(text.as_str().unwrap_or_default().to_string()),
1871 ),
1872 rmpv::Value::Binary(bytes) => {
1873 (Some("application/octet-stream"), Bytes::from(bytes.clone()))
1874 }
1875 _ => {
1876 let body = crate::serializer::strip_nulls(result.body());
1878 let json = serde_json::to_value(&body).unwrap_or_default();
1879 (
1881 Some("application/json"),
1882 Bytes::from(serde_json::to_string_pretty(&json).unwrap_or_default()),
1883 )
1884 }
1885 }
1886}
1887
1888fn status_of(code: i32) -> StatusCode {
1889 StatusCode::from_u16(code as u16).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
1890}
1891
1892fn mime_for(extension: &str) -> &'static str {
1895 match extension.to_ascii_lowercase().as_str() {
1896 "html" | "htm" => "text/html",
1897 "css" => "text/css",
1898 "js" | "mjs" => "text/javascript",
1899 "json" => "application/json",
1900 "png" => "image/png",
1901 "jpg" | "jpeg" => "image/jpeg",
1902 "gif" => "image/gif",
1903 "svg" => "image/svg+xml",
1904 "ico" => "image/x-icon",
1905 "txt" => "text/plain",
1906 "pdf" => "application/pdf",
1907 "woff2" => "font/woff2",
1908 "xml" => "application/xml",
1909 _ => "application/octet-stream",
1910 }
1911}
1912
1913fn error_response(status: i32, message: &str) -> Response<HttpBody> {
1915 let body = serde_json::json!({"status": status, "message": message, "type": "error"});
1916 Response::builder()
1917 .status(StatusCode::from_u16(status as u16).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR))
1918 .header("content-type", "application/json")
1919 .body(full(Bytes::from(body.to_string())))
1920 .expect("static response")
1921}
1922
1923#[cfg(test)]
1924mod tests {
1925 use super::*;
1926
1927 #[test]
1928 fn url_decoding() {
1929 assert_eq!(url_decode("hello%20world"), "hello world");
1930 assert_eq!(url_decode("a+b"), "a b");
1931 assert_eq!(url_decode("plain"), "plain");
1932 assert_eq!(url_decode("bad%zz"), "bad%zz");
1933 }
1934
1935 fn headers_of(content_type: &str) -> HashMap<String, String> {
1936 HashMap::from([("content-type".to_string(), content_type.to_string())])
1937 }
1938
1939 fn value_of(parsed: ParsedBody) -> serde_json::Value {
1940 match parsed {
1941 ParsedBody::Value(value) => value,
1942 ParsedBody::Form(_) => panic!("expected a value, got form fields"),
1943 ParsedBody::Bytes(_) => panic!("expected a value, got bytes"),
1944 }
1945 }
1946
1947 #[test]
1950 fn body_parsing() {
1951 let json = headers_of("application/json; charset=utf-8");
1953 let value = value_of(parse_body(&json, &Bytes::from(r#"{"a":1}"#)));
1954 assert_eq!(value["a"], 1);
1955 let text = value_of(parse_body(&json, &Bytes::from("import graph from x")));
1957 assert_eq!(
1958 text,
1959 serde_json::Value::String("import graph from x".into())
1960 );
1961 let bad = value_of(parse_body(&json, &Bytes::from("{broken")));
1963 assert_eq!(bad, serde_json::Value::String("{broken".into()));
1964 let empty = value_of(parse_body(&json, &Bytes::new()));
1966 assert_eq!(empty, serde_json::json!({}));
1967 let plain = headers_of("text/plain");
1969 let unsniffed = value_of(parse_body(&plain, &Bytes::from(r#"{"a":1}"#)));
1970 assert_eq!(unsniffed, serde_json::Value::String(r#"{"a":1}"#.into()));
1971 let xml = value_of(parse_body(
1973 &headers_of("application/xml"),
1974 &Bytes::from("<a>1</a>"),
1975 ));
1976 assert_eq!(xml, serde_json::Value::String("<a>1</a>".into()));
1977 let form = parse_body(
1979 &headers_of("application/x-www-form-urlencoded"),
1980 &Bytes::from("a=1&b=hello+world"),
1981 );
1982 match form {
1983 ParsedBody::Form(fields) => {
1984 assert_eq!(fields["a"], "1");
1985 assert_eq!(fields["b"], "hello world");
1986 }
1987 _ => panic!("expected form fields"),
1988 }
1989 match parse_body(&HashMap::new(), &Bytes::from("hello")) {
1991 ParsedBody::Bytes(bytes) => assert_eq!(bytes, b"hello"),
1992 _ => panic!("expected bytes for a missing content type"),
1993 }
1994 assert_eq!(
1996 value_of(parse_body(&HashMap::new(), &Bytes::new())),
1997 serde_json::Value::Null
1998 );
1999 }
2000}