1use std::collections::HashMap;
37use std::sync::{Arc, OnceLock};
38use std::time::Duration;
39
40use http_body_util::{BodyExt, Full};
41use hyper::body::Bytes;
42use rmpv::Value;
43use tokio_rustls::rustls;
44use tokio_rustls::rustls::pki_types::ServerName;
45use tokio_rustls::TlsConnector;
46
47use crate::envelope::EventEnvelope;
48use crate::event_stream;
49use crate::function::AppError;
50use crate::platform::Platform;
51use crate::post_office::PostOffice;
52use crate::util::app_config_reader::AppConfigReader;
53use crate::util::w3c_trace;
54
55pub const ASYNC_HTTP_REQUEST: &str = "async.http.request";
56pub const STREAM_RELAY: &str = "stream";
60const USER_AGENT_NAME: &str = "async-http-client";
61const DEFAULT_TTL_SECONDS: u64 = 30;
62const HEADERS_TO_IGNORE: &[&str] = &[
64 "content-length",
65 "user-agent",
66 "x-stream-id",
67 "content-encoding",
68 "transfer-encoding",
69 "host",
70 "connection",
71 "upgrade-insecure-requests",
72 "accept-encoding",
73 "sec-fetch-mode",
74 "sec-fetch-site",
75 "sec-fetch-user",
76 "x-event-api",
79];
80
81#[derive(Clone, Debug)]
91pub struct AsyncHttpRequest {
92 method: Option<String>,
93 url: Option<String>,
94 target_host: Option<String>,
95 headers: Vec<(String, String)>,
96 body: Option<Value>,
99 query_parameters: Vec<(String, Value)>,
100 path_parameters: Vec<(String, String)>,
101 cookies: Vec<(String, String)>,
102 session: Vec<(String, String)>,
103 trust_all_cert: Option<bool>,
107 ip: Option<String>,
110 https: Option<bool>,
111 timeout: Option<u64>,
112 query_string: Option<String>,
113}
114
115impl<'de> serde::Deserialize<'de> for AsyncHttpRequest {
126 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
127 where
128 D: serde::Deserializer<'de>,
129 {
130 let value = Value::deserialize(deserializer)?;
131 Ok(AsyncHttpRequest::from_value(&value))
132 }
133}
134
135impl serde::Serialize for AsyncHttpRequest {
136 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
137 where
138 S: serde::Serializer,
139 {
140 self.to_value().serialize(serializer)
141 }
142}
143
144impl Default for AsyncHttpRequest {
145 fn default() -> Self {
146 Self::new()
147 }
148}
149
150impl AsyncHttpRequest {
151 #[allow(clippy::new_without_default)]
152 pub fn new() -> Self {
153 AsyncHttpRequest {
154 method: None,
155 url: None,
156 target_host: None,
157 headers: Vec::new(),
158 body: None,
159 query_parameters: Vec::new(),
160 path_parameters: Vec::new(),
161 cookies: Vec::new(),
162 session: Vec::new(),
163 trust_all_cert: None,
164 ip: None,
165 https: None,
166 timeout: None,
167 query_string: None,
168 }
169 }
170
171 pub fn from_value(value: &Value) -> Self {
174 let mut request = AsyncHttpRequest::new();
175 let Value::Map(entries) = value else {
176 return request;
177 };
178 let get = |key: &str| -> Option<&Value> {
179 entries
180 .iter()
181 .find(|(k, _)| k.as_str() == Some(key))
182 .map(|(_, v)| v)
183 };
184 request.method = get("method").and_then(|v| v.as_str()).map(str::to_string);
185 request.url = get("url").and_then(|v| v.as_str()).map(str::to_string);
186 request.target_host = get("host").and_then(|v| v.as_str()).map(str::to_string);
187 request.trust_all_cert = get("trust_all_cert").and_then(|v| v.as_bool());
188 request.body = get("body").cloned();
189 request.ip = get("ip").and_then(|v| v.as_str()).map(str::to_string);
191 request.https = get("https").and_then(|v| v.as_bool());
192 request.timeout = get("timeout").and_then(|v| v.as_u64());
193 request.query_string = get("query").and_then(|v| v.as_str()).map(str::to_string);
194 if let Some(Value::Map(headers)) = get("headers") {
195 for (k, v) in headers {
196 if let Some(key) = k.as_str() {
197 request.headers.push((key.to_string(), display_text(v)));
198 }
199 }
200 }
201 if let Some(Value::Map(cookies)) = get("cookies") {
202 for (k, v) in cookies {
203 if let Some(key) = k.as_str() {
204 request.cookies.push((key.to_string(), display_text(v)));
205 }
206 }
207 }
208 if let Some(Value::Map(session)) = get("session") {
209 for (k, v) in session {
210 if let Some(key) = k.as_str() {
211 request.session.push((key.to_string(), display_text(v)));
212 }
213 }
214 }
215 if let Some(Value::Map(parameters)) = get("parameters") {
216 for (k, v) in parameters {
217 match (k.as_str(), v) {
218 (Some("query"), Value::Map(query)) => {
219 for (qk, qv) in query {
220 if let Some(key) = qk.as_str() {
221 request.query_parameters.push((key.to_string(), qv.clone()));
222 }
223 }
224 }
225 (Some("path"), Value::Map(path)) => {
226 for (pk, pv) in path {
227 if let Some(key) = pk.as_str() {
228 request
229 .path_parameters
230 .push((key.to_string(), display_text(pv)));
231 }
232 }
233 }
234 _ => {}
235 }
236 }
237 }
238 request
239 }
240
241 pub fn to_value(&self) -> Value {
243 let mut map: Vec<(Value, Value)> = Vec::new();
244 if let Some(method) = &self.method {
245 map.push((Value::from("method"), Value::from(method.as_str())));
246 }
247 if let Some(url) = &self.url {
248 map.push((Value::from("url"), Value::from(url.as_str())));
249 }
250 if let Some(host) = &self.target_host {
251 map.push((Value::from("host"), Value::from(host.as_str())));
252 }
253 if let Some(trust_all_cert) = self.trust_all_cert {
254 map.push((Value::from("trust_all_cert"), Value::from(trust_all_cert)));
256 }
257 if !self.headers.is_empty() {
258 map.push((Value::from("headers"), string_pairs(&self.headers)));
259 }
260 if let Some(body) = &self.body {
261 map.push((Value::from("body"), body.clone()));
264 }
265 if let Some(ip) = &self.ip {
268 map.push((Value::from("ip"), Value::from(ip.as_str())));
269 }
270 if let Some(https) = self.https {
271 map.push((Value::from("https"), Value::from(https)));
272 }
273 if let Some(timeout) = self.timeout {
274 map.push((Value::from("timeout"), Value::from(timeout)));
275 }
276 if let Some(query) = &self.query_string {
277 map.push((Value::from("query"), Value::from(query.as_str())));
278 }
279 if !self.cookies.is_empty() {
280 map.push((Value::from("cookies"), string_pairs(&self.cookies)));
281 }
282 if !self.session.is_empty() {
283 map.push((Value::from("session"), string_pairs(&self.session)));
284 }
285 let query: Vec<(Value, Value)> = self
289 .query_parameters
290 .iter()
291 .map(|(k, v)| (Value::from(k.as_str()), v.clone()))
292 .collect();
293 let path: Vec<(Value, Value)> = self
294 .path_parameters
295 .iter()
296 .map(|(k, v)| (Value::from(k.as_str()), Value::from(v.as_str())))
297 .collect();
298 map.push((
299 Value::from("parameters"),
300 Value::Map(vec![
301 (Value::from("query"), Value::Map(query)),
302 (Value::from("path"), Value::Map(path)),
303 ]),
304 ));
305 Value::Map(map)
306 }
307
308 pub fn set_method(mut self, method: &str) -> Self {
309 self.method = Some(method.to_string());
310 self
311 }
312
313 pub fn set_url(mut self, url: &str) -> Self {
314 self.url = Some(url.to_string());
315 self
316 }
317
318 pub fn set_target_host(mut self, host: &str) -> Self {
319 self.target_host = Some(host.to_string());
320 self
321 }
322
323 pub fn set_trust_all_cert(mut self, trust_all_cert: bool) -> Self {
326 self.trust_all_cert = Some(trust_all_cert);
327 self
328 }
329
330 pub fn set_header(mut self, key: &str, value: &str) -> Self {
332 self.headers.retain(|(k, _)| !k.eq_ignore_ascii_case(key));
333 self.headers.push((key.to_string(), value.to_string()));
334 self
335 }
336
337 pub fn set_body(mut self, body: Value) -> Self {
338 self.body = Some(body);
339 self
340 }
341
342 pub fn set_query_parameter(mut self, key: &str, value: &str) -> Self {
345 self.query_parameters.retain(|(k, _)| k != key);
346 self.query_parameters
347 .push((key.to_string(), Value::from(value)));
348 self
349 }
350
351 pub fn set_query_parameter_values(mut self, key: &str, values: &[&str]) -> Self {
354 self.query_parameters.retain(|(k, _)| k != key);
355 self.query_parameters.push((
356 key.to_string(),
357 Value::Array(values.iter().map(|v| Value::from(*v)).collect()),
358 ));
359 self
360 }
361
362 pub fn set_path_parameter(mut self, key: &str, value: &str) -> Self {
363 self.path_parameters
364 .push((key.to_string(), value.to_string()));
365 self
366 }
367
368 pub fn set_cookie(mut self, key: &str, value: &str) -> Self {
370 self.cookies.retain(|(k, _)| k != key);
371 self.cookies.push((key.to_string(), value.to_string()));
372 self
373 }
374
375 pub fn set_session_info(mut self, key: &str, value: &str) -> Self {
378 self.session.retain(|(k, _)| k != key);
379 self.session.push((key.to_string(), value.to_string()));
380 self
381 }
382
383 pub fn set_remote_ip(mut self, ip: &str) -> Self {
386 self.ip = Some(ip.to_string());
387 self
388 }
389
390 pub fn set_secure(mut self, https: bool) -> Self {
392 self.https = Some(https);
393 self
394 }
395
396 pub fn set_query_string(mut self, query: &str) -> Self {
398 self.query_string = Some(query.to_string());
399 self
400 }
401
402 pub fn set_route_timeout_seconds(mut self, seconds: u64) -> Self {
409 self.timeout = Some(seconds);
410 self
411 }
412
413 pub fn set_timeout_seconds(self, timeout_seconds: u64) -> Self {
415 let ms = timeout_seconds.max(1) * 1000;
416 self.set_header("x-ttl", &ms.to_string())
417 }
418
419 pub fn method(&self) -> &str {
420 self.method.as_deref().unwrap_or("GET")
421 }
422
423 pub fn url(&self) -> &str {
425 self.url.as_deref().unwrap_or("/")
426 }
427
428 pub fn target_host(&self) -> Option<&str> {
429 self.target_host.as_deref()
430 }
431
432 pub fn trust_all_cert(&self) -> bool {
433 self.trust_all_cert.unwrap_or(false)
434 }
435
436 pub fn headers(&self) -> &[(String, String)] {
438 &self.headers
439 }
440
441 pub fn session(&self) -> &[(String, String)] {
445 &self.session
446 }
447
448 pub fn header(&self, key: &str) -> Option<&str> {
449 self.headers
450 .iter()
451 .find(|(k, _)| k.eq_ignore_ascii_case(key))
452 .map(|(_, v)| v.as_str())
453 }
454
455 pub fn body(&self) -> &Value {
456 static NIL: Value = Value::Nil;
457 self.body.as_ref().unwrap_or(&NIL)
458 }
459
460 pub fn timeout_seconds(&self) -> u64 {
465 self.header("x-ttl")
466 .and_then(|v| v.parse::<u64>().ok())
467 .map(|ms| ms.max(1).div_ceil(1000))
468 .or(self.timeout)
472 .unwrap_or(DEFAULT_TTL_SECONDS)
473 }
474
475 pub fn body_as<T: serde::de::DeserializeOwned>(&self) -> Result<T, AppError> {
478 rmpv::ext::from_value(self.body().clone())
479 .map_err(|e| AppError::new(500, format!("unable to deserialize body: {e}")))
480 }
481
482 pub fn remote_ip(&self) -> Option<&str> {
485 self.ip.as_deref()
486 }
487
488 pub fn is_secure(&self) -> bool {
492 self.https.unwrap_or(false)
493 }
494
495 pub fn query_string(&self) -> Option<&str> {
497 self.query_string.as_deref()
498 }
499
500 pub fn path_parameter(&self, key: &str) -> Option<&str> {
502 self.path_parameters
503 .iter()
504 .find(|(k, _)| k == key)
505 .map(|(_, v)| v.as_str())
506 }
507
508 pub fn path_parameters(&self) -> &[(String, String)] {
510 &self.path_parameters
511 }
512
513 pub fn query_parameter(&self, key: &str) -> Option<String> {
516 self.query_parameters
517 .iter()
518 .find(|(k, _)| k == key)
519 .map(|(_, v)| match v {
520 Value::Array(values) => values.first().map(display_text).unwrap_or_default(),
521 other => display_text(other),
522 })
523 }
524
525 pub fn query_parameters(&self, key: &str) -> Vec<String> {
528 self.query_parameters
529 .iter()
530 .find(|(k, _)| k == key)
531 .map(|(_, v)| match v {
532 Value::Array(values) => values.iter().map(display_text).collect(),
533 other => vec![display_text(other)],
534 })
535 .unwrap_or_default()
536 }
537
538 pub fn cookie(&self, key: &str) -> Option<&str> {
540 self.cookies
541 .iter()
542 .find(|(k, _)| k == key)
543 .map(|(_, v)| v.as_str())
544 }
545
546 pub fn cookies(&self) -> &[(String, String)] {
548 &self.cookies
549 }
550
551 pub fn session_info(&self, key: &str) -> Option<&str> {
553 self.session
554 .iter()
555 .find(|(k, _)| k == key)
556 .map(|(_, v)| v.as_str())
557 }
558
559 pub fn finalized_url(&self) -> String {
562 let uri = self.url.as_deref().unwrap_or("/");
563 let (without_hash, hash) = match uri.rfind('#') {
564 Some(mark) => (&uri[..mark], Some(&uri[mark + 1..])),
565 None => (uri, None),
566 };
567 let (mut raw_uri, mut query_string) = match without_hash.rfind('?') {
568 Some(mark) => (
569 without_hash[..mark].to_string(),
570 Some(without_hash[mark + 1..].to_string()),
571 ),
572 None => (without_hash.to_string(), None),
573 };
574 let qs = self.query_parameters_to_string();
575 if let Some(qs) = qs {
576 query_string = Some(match query_string {
577 Some(existing) => format!("{existing}&{qs}"),
578 None => qs,
579 });
580 }
581 for (key, value) in &self.path_parameters {
582 let token = format!("{{{key}}}");
583 if raw_uri.contains(&token) {
584 raw_uri = raw_uri.replace(&token, value);
585 }
586 }
587 let mut out = raw_uri;
588 if let Some(qs) = query_string {
589 out.push('?');
590 out.push_str(&qs);
591 }
592 if let Some(hash) = hash {
593 out.push('#');
594 out.push_str(hash);
595 }
596 out.replace(' ', "%20")
598 }
599
600 fn query_parameters_to_string(&self) -> Option<String> {
603 if self.query_parameters.is_empty() {
604 return None;
605 }
606 let mut parts: Vec<String> = Vec::new();
607 for (key, value) in &self.query_parameters {
608 match value {
609 Value::String(_) => parts.push(format!("{key}={}", display_text(value))),
610 Value::Array(items) => {
611 for item in items {
612 if matches!(item, Value::String(_)) {
613 parts.push(format!("{key}={}", display_text(item)));
614 }
615 }
616 }
617 _ => {}
618 }
619 }
620 if parts.is_empty() {
621 None
622 } else {
623 Some(parts.join("&"))
624 }
625 }
626}
627
628fn string_pairs(pairs: &[(String, String)]) -> Value {
629 Value::Map(
630 pairs
631 .iter()
632 .map(|(k, v)| (Value::from(k.as_str()), Value::from(v.as_str())))
633 .collect(),
634 )
635}
636
637fn display_text(value: &Value) -> String {
638 match value {
639 Value::String(s) => s.as_str().unwrap_or_default().to_string(),
640 Value::Nil => String::new(),
641 other => other.to_string(),
642 }
643}
644
645struct StreamTarget {
653 reply_to: String,
654 cid: String,
655 envelope_mode: bool,
656 trace: Option<RelayTrace>,
658}
659
660#[derive(Clone)]
667struct RelayTrace {
668 trace_id: String,
669 trace_path: String,
670 span_id: Option<String>,
671}
672
673impl RelayTrace {
674 fn capture(po: &PostOffice) -> Option<Self> {
675 Some(RelayTrace {
676 trace_id: po.my_trace_id()?,
677 trace_path: po.my_trace_path()?,
678 span_id: po.my_span_id(),
679 })
680 }
681
682 fn stamp(trace: &Option<RelayTrace>, segment: EventEnvelope) -> EventEnvelope {
684 match trace {
685 Some(t) => {
686 let segment = segment.set_trace(&t.trace_id, &t.trace_path);
687 match &t.span_id {
688 Some(span) => segment.set_span_id(span),
689 None => segment,
690 }
691 }
692 None => segment,
693 }
694 }
695}
696
697pub(crate) async fn handle(
698 platform: &Platform,
699 _headers: HashMap<String, String>,
700 event: EventEnvelope,
701) -> Result<EventEnvelope, AppError> {
702 let po = PostOffice::new(platform);
703 let Some(reply_to) = event.reply_to().map(str::to_string) else {
704 if let Err(e) = process_request(platform, &po, &_headers, &event, None).await {
706 log::error!("Unhandled exception (no reply-to) - {}", e.message());
707 }
708 return EventEnvelope::new().set_body("ignored");
709 };
710 let cid = event.correlation_id().unwrap_or_default().to_string();
711 let envelope_mode = event.headers().iter().any(|(name, value)| {
712 name.eq_ignore_ascii_case(super::event_api::X_EVENT_API) && value == STREAM_RELAY
713 });
714 let stream_target = Some(StreamTarget {
715 reply_to: reply_to.clone(),
716 cid: cid.clone(),
717 envelope_mode,
718 trace: RelayTrace::capture(&po),
719 });
720 let response = match process_request(platform, &po, &_headers, &event, stream_target).await {
721 Ok(None) => return EventEnvelope::new().set_body("ignored"),
723 Ok(Some(response)) => response,
724 Err(e) => EventEnvelope::new()
725 .set_status(e.status())
726 .set_raw_body(Value::from(e.message())),
727 };
728 let _ = po
729 .send(response.set_to(&reply_to).set_correlation_id(&cid))
730 .await;
731 EventEnvelope::new().set_body("ignored")
732}
733
734fn accepts_event_stream(request: &AsyncHttpRequest) -> bool {
738 request.headers().iter().any(|(name, value)| {
739 name.eq_ignore_ascii_case("accept") && value.contains("text/event-stream")
740 })
741}
742
743async fn process_request(
744 platform: &Platform,
745 po: &PostOffice,
746 invocation_headers: &HashMap<String, String>,
747 event: &EventEnvelope,
748 stream_target: Option<StreamTarget>,
749) -> Result<Option<EventEnvelope>, AppError> {
750 let request = AsyncHttpRequest::from_value(event.body());
751 let (secure, host, port) = validate_url(&request)?;
752 let uri = request.finalized_url();
753 po.annotate_trace(
754 "destination",
755 format!(
756 "{}{}",
757 request.target_host().unwrap_or_default(),
758 raw_url(&uri)
759 ),
760 );
761 let method = request.method().to_string();
762 if !matches!(
763 method.as_str(),
764 "GET" | "HEAD" | "PUT" | "POST" | "PATCH" | "DELETE" | "OPTIONS"
765 ) {
766 return Err(AppError::new(405, "Method not allowed"));
767 }
768 let connect_ms = connect_timeout_ms();
770 let stream = tokio::time::timeout(
771 Duration::from_millis(connect_ms),
772 tokio::net::TcpStream::connect((host.as_str(), port)),
773 )
774 .await
775 .map_err(|_| AppError::new(408, format!("Connection timeout for {host}:{port}")))?
776 .map_err(|e| AppError::new(500, format!("Unable to connect to {host}:{port} - {e}")))?;
777 let mut sender = if secure {
779 let connector = tls_connector(request.trust_all_cert())?;
780 let server_name = ServerName::try_from(host.clone())
781 .map_err(|e| AppError::new(400, format!("Invalid TLS server name {host} - {e}")))?;
782 let tls_stream = connector
783 .connect(server_name, stream)
784 .await
785 .map_err(|e| AppError::new(500, format!("TLS handshake failed for {host} - {e}")))?;
786 let io = hyper_util::rt::TokioIo::new(tls_stream);
787 let (sender, connection) = hyper::client::conn::http1::handshake(io)
788 .await
789 .map_err(|e| AppError::new(500, format!("HTTP handshake failed - {e}")))?;
790 tokio::spawn(async move {
791 let _ = connection.await;
792 });
793 sender
794 } else {
795 let io = hyper_util::rt::TokioIo::new(stream);
796 let (sender, connection) = hyper::client::conn::http1::handshake(io)
797 .await
798 .map_err(|e| AppError::new(500, format!("HTTP handshake failed - {e}")))?;
799 tokio::spawn(async move {
800 let _ = connection.await;
801 });
802 sender
803 };
804 let mut builder = hyper::Request::builder()
805 .method(method.as_str())
806 .uri(if uri.is_empty() { "/" } else { &uri });
807 builder = apply_headers(
808 builder,
809 invocation_headers,
810 &request,
811 event,
812 &host,
813 port,
814 secure,
815 );
816 let body_bytes = request_body_bytes(&request, &method)?;
817 let http_request = builder
818 .body(Full::new(Bytes::from(body_bytes)))
819 .map_err(|e| AppError::new(400, format!("Invalid HTTP request - {e}")))?;
820 let ttl = Duration::from_secs(request.timeout_seconds() + 1);
826 let http_response = tokio::time::timeout(ttl, sender.send_request(http_request))
827 .await
828 .map_err(|_| AppError::new(408, format!("Timeout for {} ms", ttl.as_millis())))?
829 .map_err(|e| AppError::new(500, format!("HTTP request failed - {e}")))?;
830 let mut response = EventEnvelope::new().set_status(http_response.status().as_u16() as i32);
831 let mut content_type: Option<String> = None;
832 let mut has_content_length = false;
833 for (name, value) in http_response.headers() {
834 let key = name.as_str();
835 let text = value.to_str().unwrap_or_default();
836 if key.eq_ignore_ascii_case("content-type") {
837 content_type = Some(text.to_lowercase());
838 }
839 if key.eq_ignore_ascii_case("content-length") {
840 has_content_length = true;
841 }
842 response = response.set_header(key, text);
843 }
844 let envelope_mode = stream_target.as_ref().is_some_and(|t| t.envelope_mode);
850 if let Some(target) = stream_target {
851 let sse = content_type
852 .as_deref()
853 .is_some_and(|ct| ct.starts_with("text/event-stream"));
854 if sse && accepts_event_stream(&request) {
855 let status = http_response.status().as_u16() as i32;
856 let relay_platform = platform.clone();
857 if target.envelope_mode {
858 let idle = Duration::from_secs(request.timeout_seconds().max(1) + 1);
861 tokio::spawn(relay_envelope_sse(
862 relay_platform,
863 http_response.into_body(),
864 target.reply_to,
865 target.cid,
866 idle,
867 target.trace,
868 ));
869 } else {
870 let idle = Duration::from_secs(request.timeout_seconds().max(1));
871 tokio::spawn(relay_sse(
872 relay_platform,
873 http_response.into_body(),
874 status,
875 target.reply_to,
876 target.cid,
877 idle,
878 target.trace,
879 ));
880 }
881 return Ok(None);
882 }
883 }
884 let bytes = http_response
885 .into_body()
886 .collect()
887 .await
888 .map_err(|e| AppError::new(500, format!("Unable to read HTTP response - {e}")))?
889 .to_bytes();
890 if envelope_mode {
891 return Ok(Some(decode_relay_reply(&bytes, response.status())));
894 }
895 if !has_content_length {
896 response = response.set_header("x-content-length", &bytes.len().to_string());
897 }
898 Ok(Some(response.set_raw_body(decode_response_body(
899 &bytes,
900 content_type.as_deref(),
901 ))))
902}
903
904fn decode_relay_reply(bytes: &[u8], http_status: i32) -> EventEnvelope {
909 if bytes.is_empty() {
910 return EventEnvelope::new().set_status(http_status);
911 }
912 match EventEnvelope::from_bytes(bytes) {
913 Ok(envelope) => envelope.clear_reply_to(),
914 Err(e) => rest_error_reply(bytes, http_status).unwrap_or_else(|| {
915 EventEnvelope::new()
916 .set_status(400)
917 .set_raw_body(Value::from(format!(
918 "Did you configure rest.yaml correctly? Invalid result set - {}",
919 e.message()
920 )))
921 }),
922 }
923}
924
925fn rest_error_reply(bytes: &[u8], http_status: i32) -> Option<EventEnvelope> {
928 if http_status < 400 {
929 return None;
930 }
931 let data = serde_json::from_slice::<serde_json::Value>(bytes).ok()?;
932 if data.get("type").and_then(|v| v.as_str()) != Some("error") {
933 return None;
934 }
935 let message = data.get("message").and_then(|v| v.as_str())?;
936 Some(
937 EventEnvelope::new()
938 .set_status(http_status)
939 .set_raw_body(Value::from(message)),
940 )
941}
942
943#[derive(Default)]
948struct SseParser {
949 pending: Vec<u8>,
950 data_lines: Vec<String>,
951 event_name: Option<String>,
952}
953
954impl SseParser {
955 fn feed(&mut self, chunk: &[u8]) -> Vec<(Option<String>, String)> {
957 self.pending.extend_from_slice(chunk);
958 let mut events = Vec::new();
959 let mut start = 0;
960 let buffer = std::mem::take(&mut self.pending);
961 for i in 0..buffer.len() {
962 if buffer[i] == b'\n' {
963 let end = if i > start && buffer[i - 1] == b'\r' {
964 i - 1
965 } else {
966 i
967 };
968 let line = String::from_utf8_lossy(&buffer[start..end]).to_string();
969 start = i + 1;
970 if line.is_empty() {
971 if !self.data_lines.is_empty() {
973 events.push((self.event_name.take(), self.data_lines.join("\n")));
974 }
975 self.data_lines.clear();
976 self.event_name = None;
977 } else if !line.starts_with(':') {
978 let (field, value) = match line.find(':') {
980 Some(colon) => (&line[..colon], &line[colon + 1..]),
981 None => (line.as_str(), ""),
982 };
983 let value = value.strip_prefix(' ').unwrap_or(value);
984 match field {
985 "data" => self.data_lines.push(value.to_string()),
986 "event" => self.event_name = Some(value.to_string()),
987 _ => { }
988 }
989 }
990 }
991 }
992 self.pending = buffer[start..].to_vec();
993 events
994 }
995}
996
997#[allow(clippy::too_many_arguments)]
1003async fn relay_sse(
1004 platform: Platform,
1005 mut body: hyper::body::Incoming,
1006 status: i32,
1007 reply_to: String,
1008 cid: String,
1009 idle: Duration,
1010 trace: Option<RelayTrace>,
1011) {
1012 let po = PostOffice::new(&platform);
1013 let mut parser = SseParser::default();
1014 let mut head_sent = false;
1015 loop {
1016 match tokio::time::timeout(idle, body.frame()).await {
1017 Ok(Some(Ok(frame))) => {
1018 if let Some(data) = frame.data_ref() {
1019 for (name, text) in parser.feed(data) {
1020 let mut segment = EventEnvelope::new()
1021 .set_header(event_stream::X_EVENT_STREAM, event_stream::DATA);
1022 if let Some(name) = name.filter(|n| !n.is_empty()) {
1023 segment = segment.set_header(event_stream::X_EVENT_NAME, &name);
1024 }
1025 segment = match segment.set_body(text) {
1026 Ok(seg) => seg,
1027 Err(_) => continue,
1028 };
1029 if !head_sent {
1030 head_sent = true;
1031 segment = RelayTrace::stamp(
1036 &trace,
1037 segment
1038 .set_status(status)
1039 .set_header("content-type", "text/event-stream"),
1040 );
1041 }
1042 if send_segment(&po, segment, &reply_to, &cid).await.is_err() {
1043 return;
1044 }
1045 }
1046 }
1047 }
1048 Ok(None) => {
1049 let mut eof = EventEnvelope::new()
1052 .set_header(event_stream::X_EVENT_STREAM, event_stream::EOF);
1053 if !head_sent {
1054 eof = eof
1055 .set_status(status)
1056 .set_header("content-type", "text/event-stream");
1057 }
1058 let _ = send_segment(&po, RelayTrace::stamp(&trace, eof), &reply_to, &cid).await;
1059 return;
1060 }
1061 Ok(Some(Err(e))) => {
1062 fail_in_band(&po, &reply_to, &cid, 500, &e.to_string(), head_sent, &trace).await;
1063 return;
1064 }
1065 Err(_) => {
1066 let message = format!("Timeout for {} seconds", idle.as_secs());
1068 fail_in_band(&po, &reply_to, &cid, 408, &message, head_sent, &trace).await;
1069 return;
1070 }
1071 }
1072 }
1073}
1074
1075async fn relay_envelope_sse(
1084 platform: Platform,
1085 mut body: hyper::body::Incoming,
1086 reply_to: String,
1087 cid: String,
1088 idle: Duration,
1089 trace: Option<RelayTrace>,
1090) {
1091 let po = PostOffice::new(&platform);
1092 let mut parser = SseParser::default();
1093 let mut head_seen = false;
1094 loop {
1095 match tokio::time::timeout(idle, body.frame()).await {
1096 Ok(Some(Ok(frame))) => {
1097 if let Some(data) = frame.data_ref() {
1098 for (name, text) in parser.feed(data) {
1099 match relay_envelope_event(
1100 &po,
1101 name,
1102 text,
1103 &reply_to,
1104 &cid,
1105 &mut head_seen,
1106 &trace,
1107 )
1108 .await
1109 {
1110 RelayFlow::Next => {}
1111 RelayFlow::End => return,
1115 }
1116 }
1117 }
1118 }
1119 Ok(None) => {
1120 fail_in_band(
1123 &po,
1124 &reply_to,
1125 &cid,
1126 500,
1127 "Event stream ended without eof",
1128 head_seen,
1129 &trace,
1130 )
1131 .await;
1132 return;
1133 }
1134 Ok(Some(Err(e))) => {
1135 fail_in_band(&po, &reply_to, &cid, 500, &e.to_string(), head_seen, &trace).await;
1136 return;
1137 }
1138 Err(_) => {
1139 let message = format!("Timeout for {} seconds", idle.as_secs());
1140 fail_in_band(&po, &reply_to, &cid, 408, &message, head_seen, &trace).await;
1141 return;
1142 }
1143 }
1144 }
1145}
1146
1147enum RelayFlow {
1149 Next,
1150 End,
1151}
1152
1153async fn relay_envelope_event(
1154 po: &PostOffice,
1155 name: Option<String>,
1156 text: String,
1157 reply_to: &str,
1158 cid: &str,
1159 head_seen: &mut bool,
1160 trace: &Option<RelayTrace>,
1161) -> RelayFlow {
1162 use base64::Engine as _;
1163 if name.as_deref() == Some(event_stream::ENVELOPE) {
1164 let decoded = base64::engine::general_purpose::STANDARD
1165 .decode(&text)
1166 .ok()
1167 .and_then(|bytes| EventEnvelope::from_bytes(&bytes).ok());
1168 let Some(decoded) = decoded else {
1169 fail_in_band(
1170 po,
1171 reply_to,
1172 cid,
1173 500,
1174 "Invalid event stream - malformed envelope frame",
1175 *head_seen,
1176 trace,
1177 )
1178 .await;
1179 return RelayFlow::End;
1180 };
1181 *head_seen = true;
1182 let terminal = decoded.headers().iter().any(|(key, value)| {
1183 key.eq_ignore_ascii_case(event_stream::X_EVENT_STREAM)
1184 && (value.eq_ignore_ascii_case(event_stream::EOF)
1185 || value.eq_ignore_ascii_case(event_stream::EXCEPTION))
1186 });
1187 let _ = send_segment(po, decoded.clear_reply_to(), reply_to, cid).await;
1188 if terminal {
1189 RelayFlow::End
1190 } else {
1191 RelayFlow::Next
1192 }
1193 } else if !*head_seen {
1194 fail_in_band(
1196 po,
1197 reply_to,
1198 cid,
1199 500,
1200 "Invalid event stream - missing envelope head",
1201 false,
1202 trace,
1203 )
1204 .await;
1205 RelayFlow::End
1206 } else {
1207 let mut segment =
1209 EventEnvelope::new().set_header(event_stream::X_EVENT_STREAM, event_stream::DATA);
1210 if let Some(name) = name.filter(|n| !n.is_empty()) {
1211 segment = segment.set_header(event_stream::X_EVENT_NAME, &name);
1212 }
1213 if let Ok(segment) = segment.set_body(text) {
1214 let _ = send_segment(po, segment, reply_to, cid).await;
1215 }
1216 RelayFlow::Next
1217 }
1218}
1219
1220async fn fail_in_band(
1221 po: &PostOffice,
1222 reply_to: &str,
1223 cid: &str,
1224 status: i32,
1225 message: &str,
1226 head_sent: bool,
1227 trace: &Option<RelayTrace>,
1228) {
1229 let body = serde_json::json!({"type": "error", "status": status, "message": message});
1231 let Ok(mut error) = EventEnvelope::new()
1232 .set_header(event_stream::X_EVENT_STREAM, event_stream::EXCEPTION)
1233 .set_status(status)
1234 .set_body(body)
1235 else {
1236 return;
1237 };
1238 if !head_sent {
1239 error = error.set_header("content-type", "text/event-stream");
1240 }
1241 let _ = send_segment(po, RelayTrace::stamp(trace, error), reply_to, cid).await;
1243}
1244
1245async fn send_segment(
1246 po: &PostOffice,
1247 segment: EventEnvelope,
1248 reply_to: &str,
1249 cid: &str,
1250) -> Result<(), AppError> {
1251 po.send(segment.set_to(reply_to).set_correlation_id(cid))
1252 .await
1253}
1254
1255fn raw_url(uri: &str) -> &str {
1256 match uri.rfind('?') {
1257 Some(mark) => &uri[..mark],
1258 None => uri,
1259 }
1260}
1261
1262fn connect_timeout_ms() -> u64 {
1263 let config = AppConfigReader::get_instance();
1264 config
1265 .get_property_or("http.client.connection.timeout", "5000")
1266 .parse::<u64>()
1267 .unwrap_or(5000)
1268 .max(2000)
1269}
1270
1271fn validate_url(request: &AsyncHttpRequest) -> Result<(bool, String, u16), AppError> {
1274 let Some(target) = request.target_host() else {
1275 return Err(AppError::new(
1276 400,
1277 "Missing target host. e.g. https://hostname",
1278 ));
1279 };
1280 let (secure, rest) = if let Some(rest) = target.strip_prefix("http://") {
1281 (false, rest)
1282 } else if let Some(rest) = target.strip_prefix("https://") {
1283 (true, rest)
1284 } else {
1285 return Err(AppError::new(400, "Protocol must be http or https"));
1286 };
1287 let authority = rest.trim_end_matches('/');
1288 if authority.contains('/') {
1289 return Err(AppError::new(400, "Target host must not contain URI path"));
1290 }
1291 let default_port = if secure { 443 } else { 80 };
1292 let (host, port) = match authority.rsplit_once(':') {
1293 Some((h, p)) => (
1294 h.to_string(),
1295 p.parse::<u16>()
1296 .map_err(|_| AppError::new(400, "Invalid port number in target host"))?,
1297 ),
1298 None => (authority.to_string(), default_port),
1299 };
1300 if host.trim().is_empty() {
1301 return Err(AppError::new(
1302 400,
1303 "Unable to resolve target host as domain or IP address",
1304 ));
1305 }
1306 Ok((secure, host, port))
1307}
1308
1309fn tls_connector(trust_all_cert: bool) -> Result<TlsConnector, AppError> {
1315 static STRICT: OnceLock<Result<Arc<rustls::ClientConfig>, String>> = OnceLock::new();
1316 static TRUST_ALL: OnceLock<Arc<rustls::ClientConfig>> = OnceLock::new();
1317 let config = if trust_all_cert {
1318 TRUST_ALL
1319 .get_or_init(|| {
1320 let config = rustls::ClientConfig::builder()
1321 .dangerous()
1322 .with_custom_certificate_verifier(Arc::new(TrustAllVerifier))
1323 .with_no_client_auth();
1324 Arc::new(config)
1325 })
1326 .clone()
1327 } else {
1328 STRICT
1329 .get_or_init(|| {
1330 let loaded = rustls_native_certs::load_native_certs();
1331 let mut roots = rustls::RootCertStore::empty();
1332 for cert in loaded.certs {
1333 let _ = roots.add(cert);
1336 }
1337 if roots.is_empty() {
1338 return Err(format!(
1339 "No usable certificates in the OS trust store - {:?}",
1340 loaded.errors
1341 ));
1342 }
1343 Ok(Arc::new(
1344 rustls::ClientConfig::builder()
1345 .with_root_certificates(roots)
1346 .with_no_client_auth(),
1347 ))
1348 })
1349 .clone()
1350 .map_err(|e| AppError::new(500, e))?
1351 };
1352 Ok(TlsConnector::from(config))
1353}
1354
1355#[derive(Debug)]
1359struct TrustAllVerifier;
1360
1361impl rustls::client::danger::ServerCertVerifier for TrustAllVerifier {
1362 fn verify_server_cert(
1363 &self,
1364 _end_entity: &rustls::pki_types::CertificateDer<'_>,
1365 _intermediates: &[rustls::pki_types::CertificateDer<'_>],
1366 _server_name: &ServerName<'_>,
1367 _ocsp_response: &[u8],
1368 _now: rustls::pki_types::UnixTime,
1369 ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
1370 Ok(rustls::client::danger::ServerCertVerified::assertion())
1371 }
1372
1373 fn verify_tls12_signature(
1374 &self,
1375 message: &[u8],
1376 cert: &rustls::pki_types::CertificateDer<'_>,
1377 dss: &rustls::DigitallySignedStruct,
1378 ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
1379 rustls::crypto::verify_tls12_signature(
1380 message,
1381 cert,
1382 dss,
1383 &rustls::crypto::ring::default_provider().signature_verification_algorithms,
1384 )
1385 }
1386
1387 fn verify_tls13_signature(
1388 &self,
1389 message: &[u8],
1390 cert: &rustls::pki_types::CertificateDer<'_>,
1391 dss: &rustls::DigitallySignedStruct,
1392 ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
1393 rustls::crypto::verify_tls13_signature(
1394 message,
1395 cert,
1396 dss,
1397 &rustls::crypto::ring::default_provider().signature_verification_algorithms,
1398 )
1399 }
1400
1401 fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
1402 rustls::crypto::ring::default_provider()
1403 .signature_verification_algorithms
1404 .supported_schemes()
1405 }
1406}
1407
1408#[allow(clippy::too_many_arguments)]
1409fn apply_headers(
1410 mut builder: hyper::http::request::Builder,
1411 invocation_headers: &HashMap<String, String>,
1412 request: &AsyncHttpRequest,
1413 event: &EventEnvelope,
1414 host: &str,
1415 port: u16,
1416 secure: bool,
1417) -> hyper::http::request::Builder {
1418 let default_port = if secure { 443 } else { 80 };
1420 let authority = if port == default_port {
1421 host.to_string()
1422 } else {
1423 format!("{host}:{port}")
1424 };
1425 builder = builder.header("host", authority);
1426 builder = builder.header("user-agent", USER_AGENT_NAME);
1427 let mut merged: Vec<(String, String)> = request.headers.clone();
1429 merged.extend(request.session.iter().cloned());
1430 for (key, value) in &merged {
1431 if permitted_http_header(key) {
1432 builder = builder.header(key.as_str(), value.trim());
1436 }
1437 }
1438 if !merged.iter().any(|(k, _)| k.eq_ignore_ascii_case("accept")) {
1443 builder = builder.header("accept", "*/*");
1444 }
1445 let config = AppConfigReader::get_instance();
1456 if let Some(trace_id) = event.trace_id() {
1457 let trace_header = config.get_property_or("http.trace.id.header", "X-Trace-Id");
1458 stamp_header(&mut builder, &trace_header, trace_id);
1459 if let Some(traceparent) = w3c_trace::format(trace_id, event.span_id().unwrap_or_default())
1460 {
1461 stamp_header(&mut builder, w3c_trace::TRACEPARENT, &traceparent);
1462 let custom_traceparent =
1467 config.get_property_or("http.traceparent.header", w3c_trace::TRACEPARENT);
1468 if !custom_traceparent.eq_ignore_ascii_case(w3c_trace::TRACEPARENT) {
1469 stamp_header(&mut builder, &custom_traceparent, &traceparent);
1470 }
1471 }
1472 }
1473 if request.header(super::event_api::X_EVENT_API).is_none() {
1479 if let Some(business_cid) = invocation_headers.get(crate::automation::MY_CORRELATION_ID) {
1480 let cid_header =
1481 config.get_property_or("http.correlation.id.header", "X-Correlation-Id");
1482 if request.header(&cid_header).is_none() {
1483 stamp_header(&mut builder, &cid_header, business_cid.as_str());
1484 }
1485 }
1486 }
1487 if !request.cookies.is_empty() {
1489 let cookie = request
1490 .cookies
1491 .iter()
1492 .map(|(k, v)| format!("{k}={}", url_encode(v)))
1493 .collect::<Vec<_>>()
1494 .join("; ");
1495 builder = builder.header("cookie", cookie.as_str());
1496 }
1497 builder
1498}
1499
1500fn permitted_http_header(header: &str) -> bool {
1501 !HEADERS_TO_IGNORE
1502 .iter()
1503 .any(|ignored| header.eq_ignore_ascii_case(ignored))
1504}
1505
1506fn stamp_header(builder: &mut hyper::http::request::Builder, name: &str, value: &str) {
1512 if let Some(headers) = builder.headers_mut() {
1513 if let (Ok(name), Ok(value)) = (
1514 hyper::header::HeaderName::from_bytes(name.as_bytes()),
1515 hyper::header::HeaderValue::from_str(value),
1516 ) {
1517 headers.insert(name, value);
1518 }
1519 }
1520}
1521
1522fn url_encode(text: &str) -> String {
1523 let mut out = String::with_capacity(text.len());
1524 for byte in text.bytes() {
1525 match byte {
1526 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' | b'*' => {
1527 out.push(byte as char)
1528 }
1529 b' ' => out.push('+'),
1530 other => out.push_str(&format!("%{other:02X}")),
1531 }
1532 }
1533 out
1534}
1535
1536fn request_body_bytes(request: &AsyncHttpRequest, method: &str) -> Result<Vec<u8>, AppError> {
1537 if !matches!(method, "POST" | "PUT" | "PATCH") {
1538 return Ok(Vec::new());
1539 }
1540 match request.body() {
1541 Value::Nil => Ok(Vec::new()),
1542 Value::Binary(bytes) => Ok(bytes.clone()),
1543 Value::String(text) => Ok(text.as_str().unwrap_or_default().as_bytes().to_vec()),
1544 value @ (Value::Map(_) | Value::Array(_)) => {
1545 let json = serde_json::to_value(crate::serializer::strip_nulls(value))
1549 .map_err(|e| AppError::new(400, format!("Invalid HTTP request body - {e}")))?;
1550 serde_json::to_vec(&json)
1551 .map_err(|e| AppError::new(400, format!("Invalid HTTP request body - {e}")))
1552 }
1553 _ => Err(AppError::new(400, "Invalid HTTP request body")),
1554 }
1555}
1556
1557fn decode_response_body(bytes: &[u8], content_type: Option<&str>) -> Value {
1561 let Some(content_type) = content_type else {
1562 return Value::from(bytes.to_vec());
1563 };
1564 if content_type.starts_with("application/json") {
1565 let text = String::from_utf8_lossy(bytes).trim().to_string();
1566 if text.is_empty() {
1567 return Value::Map(vec![]);
1568 }
1569 if (text.starts_with('{') && text.ends_with('}'))
1570 || (text.starts_with('[') && text.ends_with(']'))
1571 {
1572 if let Ok(json) = serde_json::from_str::<serde_json::Value>(&text) {
1573 if let Ok(value) = rmpv::ext::to_value(&json) {
1574 return value;
1575 }
1576 }
1577 }
1578 return Value::from(text);
1579 }
1580 if content_type.starts_with("text/")
1581 || content_type.starts_with("application/javascript")
1582 || content_type.starts_with("application/xml")
1583 {
1584 return Value::from(String::from_utf8_lossy(bytes).to_string());
1585 }
1586 Value::from(bytes.to_vec())
1587}
1588
1589pub struct AsyncHttpClientService {
1594 platform: Platform,
1595}
1596
1597impl AsyncHttpClientService {
1598 pub fn new(platform: &Platform) -> Self {
1599 AsyncHttpClientService {
1600 platform: platform.clone(),
1601 }
1602 }
1603}
1604
1605#[async_trait::async_trait]
1606impl crate::function::ComposableFunction for AsyncHttpClientService {
1607 async fn handle_event(
1608 &self,
1609 headers: HashMap<String, String>,
1610 input: EventEnvelope,
1611 _instance: usize,
1612 ) -> Result<EventEnvelope, AppError> {
1613 handle(&self.platform, headers, input).await
1617 }
1618}
1619
1620#[cfg(test)]
1621mod tests {
1622 use super::*;
1623
1624 fn server_dataset_request() -> AsyncHttpRequest {
1629 AsyncHttpRequest::new()
1630 .set_method("POST")
1631 .set_url("/api/typed/alice")
1632 .set_remote_ip("127.0.0.1")
1633 .set_secure(true)
1634 .set_target_host("localhost:8085")
1635 .set_header("accept", "application/json")
1636 .set_header("x-api-key", "open-sesame")
1637 .set_header("x-ttl", "5000")
1638 .set_path_parameter("user", "alice")
1639 .set_query_parameter_values("q", &["a", "b"])
1640 .set_query_parameter("single", "1")
1641 .set_query_string("q=a&q=b&single=1")
1642 .set_route_timeout_seconds(30)
1643 .set_cookie("first", "alpha")
1644 .set_session_info("user_id", "u-1")
1645 .set_body(Value::Map(vec![(
1646 Value::from("note"),
1647 Value::from("hello"),
1648 )]))
1649 }
1650
1651 #[test]
1663 fn server_dataset_round_trips_through_the_typed_contract() {
1664 let request = server_dataset_request();
1665 assert_eq!(request.method(), "POST");
1667 assert_eq!(request.url(), "/api/typed/alice");
1668 assert_eq!(request.remote_ip(), Some("127.0.0.1"));
1669 assert!(request.is_secure());
1670 assert_eq!(request.target_host(), Some("localhost:8085"));
1671 assert_eq!(request.path_parameter("user"), Some("alice"));
1672 assert_eq!(request.query_parameter("single").as_deref(), Some("1"));
1673 assert_eq!(request.query_parameter("q").as_deref(), Some("a"));
1674 assert_eq!(request.query_parameters("q"), vec!["a", "b"]);
1675 assert_eq!(request.query_parameters("single"), vec!["1"]);
1676 assert_eq!(request.query_string(), Some("q=a&q=b&single=1"));
1677 assert_eq!(
1678 request.header("Accept"),
1679 Some("application/json"),
1680 "case-insensitive"
1681 );
1682 assert_eq!(request.cookie("first"), Some("alpha"));
1683 assert_eq!(request.session_info("user_id"), Some("u-1"));
1684 assert_eq!(request.timeout_seconds(), 5);
1686 #[derive(serde::Deserialize)]
1687 struct Note {
1688 note: String,
1689 }
1690 assert_eq!(request.body_as::<Note>().expect("typed body").note, "hello");
1691 let round = AsyncHttpRequest::from_value(&request.to_value());
1693 assert_eq!(round.to_value(), request.to_value());
1694 let deserialized: AsyncHttpRequest =
1696 rmpv::ext::from_value(request.to_value()).expect("serde deserialize");
1697 assert_eq!(deserialized.to_value(), request.to_value());
1698 let serialized = rmpv::ext::to_value(&request).expect("serde serialize");
1699 assert_eq!(
1700 AsyncHttpRequest::from_value(&serialized).to_value(),
1701 request.to_value()
1702 );
1703 }
1704
1705 #[test]
1709 fn route_timeout_is_the_fallback_without_x_ttl() {
1710 let request = AsyncHttpRequest::new()
1711 .set_method("GET")
1712 .set_url("/x")
1713 .set_route_timeout_seconds(30);
1714 assert_eq!(request.timeout_seconds(), 30);
1715 assert_eq!(
1717 AsyncHttpRequest::from_value(&request.to_value()).timeout_seconds(),
1718 30
1719 );
1720 }
1721}