use std::collections::HashMap;
use std::sync::{Arc, OnceLock};
use std::time::Duration;
use http_body_util::{BodyExt, Full};
use hyper::body::Bytes;
use rmpv::Value;
use tokio_rustls::rustls;
use tokio_rustls::rustls::pki_types::ServerName;
use tokio_rustls::TlsConnector;
use crate::envelope::EventEnvelope;
use crate::event_stream;
use crate::function::AppError;
use crate::platform::Platform;
use crate::post_office::PostOffice;
use crate::util::app_config_reader::AppConfigReader;
use crate::util::w3c_trace;
pub const ASYNC_HTTP_REQUEST: &str = "async.http.request";
pub const STREAM_RELAY: &str = "stream";
const USER_AGENT_NAME: &str = "async-http-client";
const DEFAULT_TTL_SECONDS: u64 = 30;
const HEADERS_TO_IGNORE: &[&str] = &[
"content-length",
"user-agent",
"x-stream-id",
"content-encoding",
"transfer-encoding",
"host",
"connection",
"upgrade-insecure-requests",
"accept-encoding",
"sec-fetch-mode",
"sec-fetch-site",
"sec-fetch-user",
"x-event-api",
];
#[derive(Clone, Debug)]
pub struct AsyncHttpRequest {
method: Option<String>,
url: Option<String>,
target_host: Option<String>,
headers: Vec<(String, String)>,
body: Option<Value>,
query_parameters: Vec<(String, Value)>,
path_parameters: Vec<(String, String)>,
cookies: Vec<(String, String)>,
session: Vec<(String, String)>,
trust_all_cert: Option<bool>,
ip: Option<String>,
https: Option<bool>,
timeout: Option<u64>,
query_string: Option<String>,
}
impl<'de> serde::Deserialize<'de> for AsyncHttpRequest {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = Value::deserialize(deserializer)?;
Ok(AsyncHttpRequest::from_value(&value))
}
}
impl serde::Serialize for AsyncHttpRequest {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.to_value().serialize(serializer)
}
}
impl Default for AsyncHttpRequest {
fn default() -> Self {
Self::new()
}
}
impl AsyncHttpRequest {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
AsyncHttpRequest {
method: None,
url: None,
target_host: None,
headers: Vec::new(),
body: None,
query_parameters: Vec::new(),
path_parameters: Vec::new(),
cookies: Vec::new(),
session: Vec::new(),
trust_all_cert: None,
ip: None,
https: None,
timeout: None,
query_string: None,
}
}
pub fn from_value(value: &Value) -> Self {
let mut request = AsyncHttpRequest::new();
let Value::Map(entries) = value else {
return request;
};
let get = |key: &str| -> Option<&Value> {
entries
.iter()
.find(|(k, _)| k.as_str() == Some(key))
.map(|(_, v)| v)
};
request.method = get("method").and_then(|v| v.as_str()).map(str::to_string);
request.url = get("url").and_then(|v| v.as_str()).map(str::to_string);
request.target_host = get("host").and_then(|v| v.as_str()).map(str::to_string);
request.trust_all_cert = get("trust_all_cert").and_then(|v| v.as_bool());
request.body = get("body").cloned();
request.ip = get("ip").and_then(|v| v.as_str()).map(str::to_string);
request.https = get("https").and_then(|v| v.as_bool());
request.timeout = get("timeout").and_then(|v| v.as_u64());
request.query_string = get("query").and_then(|v| v.as_str()).map(str::to_string);
if let Some(Value::Map(headers)) = get("headers") {
for (k, v) in headers {
if let Some(key) = k.as_str() {
request.headers.push((key.to_string(), display_text(v)));
}
}
}
if let Some(Value::Map(cookies)) = get("cookies") {
for (k, v) in cookies {
if let Some(key) = k.as_str() {
request.cookies.push((key.to_string(), display_text(v)));
}
}
}
if let Some(Value::Map(session)) = get("session") {
for (k, v) in session {
if let Some(key) = k.as_str() {
request.session.push((key.to_string(), display_text(v)));
}
}
}
if let Some(Value::Map(parameters)) = get("parameters") {
for (k, v) in parameters {
match (k.as_str(), v) {
(Some("query"), Value::Map(query)) => {
for (qk, qv) in query {
if let Some(key) = qk.as_str() {
request.query_parameters.push((key.to_string(), qv.clone()));
}
}
}
(Some("path"), Value::Map(path)) => {
for (pk, pv) in path {
if let Some(key) = pk.as_str() {
request
.path_parameters
.push((key.to_string(), display_text(pv)));
}
}
}
_ => {}
}
}
}
request
}
pub fn to_value(&self) -> Value {
let mut map: Vec<(Value, Value)> = Vec::new();
if let Some(method) = &self.method {
map.push((Value::from("method"), Value::from(method.as_str())));
}
if let Some(url) = &self.url {
map.push((Value::from("url"), Value::from(url.as_str())));
}
if let Some(host) = &self.target_host {
map.push((Value::from("host"), Value::from(host.as_str())));
}
if let Some(trust_all_cert) = self.trust_all_cert {
map.push((Value::from("trust_all_cert"), Value::from(trust_all_cert)));
}
if !self.headers.is_empty() {
map.push((Value::from("headers"), string_pairs(&self.headers)));
}
if let Some(body) = &self.body {
map.push((Value::from("body"), body.clone()));
}
if let Some(ip) = &self.ip {
map.push((Value::from("ip"), Value::from(ip.as_str())));
}
if let Some(https) = self.https {
map.push((Value::from("https"), Value::from(https)));
}
if let Some(timeout) = self.timeout {
map.push((Value::from("timeout"), Value::from(timeout)));
}
if let Some(query) = &self.query_string {
map.push((Value::from("query"), Value::from(query.as_str())));
}
if !self.cookies.is_empty() {
map.push((Value::from("cookies"), string_pairs(&self.cookies)));
}
if !self.session.is_empty() {
map.push((Value::from("session"), string_pairs(&self.session)));
}
let query: Vec<(Value, Value)> = self
.query_parameters
.iter()
.map(|(k, v)| (Value::from(k.as_str()), v.clone()))
.collect();
let path: Vec<(Value, Value)> = self
.path_parameters
.iter()
.map(|(k, v)| (Value::from(k.as_str()), Value::from(v.as_str())))
.collect();
map.push((
Value::from("parameters"),
Value::Map(vec![
(Value::from("query"), Value::Map(query)),
(Value::from("path"), Value::Map(path)),
]),
));
Value::Map(map)
}
pub fn set_method(mut self, method: &str) -> Self {
self.method = Some(method.to_string());
self
}
pub fn set_url(mut self, url: &str) -> Self {
self.url = Some(url.to_string());
self
}
pub fn set_target_host(mut self, host: &str) -> Self {
self.target_host = Some(host.to_string());
self
}
pub fn set_trust_all_cert(mut self, trust_all_cert: bool) -> Self {
self.trust_all_cert = Some(trust_all_cert);
self
}
pub fn set_header(mut self, key: &str, value: &str) -> Self {
self.headers.retain(|(k, _)| !k.eq_ignore_ascii_case(key));
self.headers.push((key.to_string(), value.to_string()));
self
}
pub fn set_body(mut self, body: Value) -> Self {
self.body = Some(body);
self
}
pub fn set_query_parameter(mut self, key: &str, value: &str) -> Self {
self.query_parameters.retain(|(k, _)| k != key);
self.query_parameters
.push((key.to_string(), Value::from(value)));
self
}
pub fn set_query_parameter_values(mut self, key: &str, values: &[&str]) -> Self {
self.query_parameters.retain(|(k, _)| k != key);
self.query_parameters.push((
key.to_string(),
Value::Array(values.iter().map(|v| Value::from(*v)).collect()),
));
self
}
pub fn set_path_parameter(mut self, key: &str, value: &str) -> Self {
self.path_parameters
.push((key.to_string(), value.to_string()));
self
}
pub fn set_cookie(mut self, key: &str, value: &str) -> Self {
self.cookies.retain(|(k, _)| k != key);
self.cookies.push((key.to_string(), value.to_string()));
self
}
pub fn set_session_info(mut self, key: &str, value: &str) -> Self {
self.session.retain(|(k, _)| k != key);
self.session.push((key.to_string(), value.to_string()));
self
}
pub fn set_remote_ip(mut self, ip: &str) -> Self {
self.ip = Some(ip.to_string());
self
}
pub fn set_secure(mut self, https: bool) -> Self {
self.https = Some(https);
self
}
pub fn set_query_string(mut self, query: &str) -> Self {
self.query_string = Some(query.to_string());
self
}
pub fn set_route_timeout_seconds(mut self, seconds: u64) -> Self {
self.timeout = Some(seconds);
self
}
pub fn set_timeout_seconds(self, timeout_seconds: u64) -> Self {
let ms = timeout_seconds.max(1) * 1000;
self.set_header("x-ttl", &ms.to_string())
}
pub fn method(&self) -> &str {
self.method.as_deref().unwrap_or("GET")
}
pub fn url(&self) -> &str {
self.url.as_deref().unwrap_or("/")
}
pub fn target_host(&self) -> Option<&str> {
self.target_host.as_deref()
}
pub fn trust_all_cert(&self) -> bool {
self.trust_all_cert.unwrap_or(false)
}
pub fn headers(&self) -> &[(String, String)] {
&self.headers
}
pub fn session(&self) -> &[(String, String)] {
&self.session
}
pub fn header(&self, key: &str) -> Option<&str> {
self.headers
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case(key))
.map(|(_, v)| v.as_str())
}
pub fn body(&self) -> &Value {
static NIL: Value = Value::Nil;
self.body.as_ref().unwrap_or(&NIL)
}
pub fn timeout_seconds(&self) -> u64 {
self.header("x-ttl")
.and_then(|v| v.parse::<u64>().ok())
.map(|ms| ms.max(1).div_ceil(1000))
.or(self.timeout)
.unwrap_or(DEFAULT_TTL_SECONDS)
}
pub fn body_as<T: serde::de::DeserializeOwned>(&self) -> Result<T, AppError> {
rmpv::ext::from_value(self.body().clone())
.map_err(|e| AppError::new(500, format!("unable to deserialize body: {e}")))
}
pub fn remote_ip(&self) -> Option<&str> {
self.ip.as_deref()
}
pub fn is_secure(&self) -> bool {
self.https.unwrap_or(false)
}
pub fn query_string(&self) -> Option<&str> {
self.query_string.as_deref()
}
pub fn path_parameter(&self, key: &str) -> Option<&str> {
self.path_parameters
.iter()
.find(|(k, _)| k == key)
.map(|(_, v)| v.as_str())
}
pub fn path_parameters(&self) -> &[(String, String)] {
&self.path_parameters
}
pub fn query_parameter(&self, key: &str) -> Option<String> {
self.query_parameters
.iter()
.find(|(k, _)| k == key)
.map(|(_, v)| match v {
Value::Array(values) => values.first().map(display_text).unwrap_or_default(),
other => display_text(other),
})
}
pub fn query_parameters(&self, key: &str) -> Vec<String> {
self.query_parameters
.iter()
.find(|(k, _)| k == key)
.map(|(_, v)| match v {
Value::Array(values) => values.iter().map(display_text).collect(),
other => vec![display_text(other)],
})
.unwrap_or_default()
}
pub fn cookie(&self, key: &str) -> Option<&str> {
self.cookies
.iter()
.find(|(k, _)| k == key)
.map(|(_, v)| v.as_str())
}
pub fn cookies(&self) -> &[(String, String)] {
&self.cookies
}
pub fn session_info(&self, key: &str) -> Option<&str> {
self.session
.iter()
.find(|(k, _)| k == key)
.map(|(_, v)| v.as_str())
}
pub fn finalized_url(&self) -> String {
let uri = self.url.as_deref().unwrap_or("/");
let (without_hash, hash) = match uri.rfind('#') {
Some(mark) => (&uri[..mark], Some(&uri[mark + 1..])),
None => (uri, None),
};
let (mut raw_uri, mut query_string) = match without_hash.rfind('?') {
Some(mark) => (
without_hash[..mark].to_string(),
Some(without_hash[mark + 1..].to_string()),
),
None => (without_hash.to_string(), None),
};
let qs = self.query_parameters_to_string();
if let Some(qs) = qs {
query_string = Some(match query_string {
Some(existing) => format!("{existing}&{qs}"),
None => qs,
});
}
for (key, value) in &self.path_parameters {
let token = format!("{{{key}}}");
if raw_uri.contains(&token) {
raw_uri = raw_uri.replace(&token, value);
}
}
let mut out = raw_uri;
if let Some(qs) = query_string {
out.push('?');
out.push_str(&qs);
}
if let Some(hash) = hash {
out.push('#');
out.push_str(hash);
}
out.replace(' ', "%20")
}
fn query_parameters_to_string(&self) -> Option<String> {
if self.query_parameters.is_empty() {
return None;
}
let mut parts: Vec<String> = Vec::new();
for (key, value) in &self.query_parameters {
match value {
Value::String(_) => parts.push(format!("{key}={}", display_text(value))),
Value::Array(items) => {
for item in items {
if matches!(item, Value::String(_)) {
parts.push(format!("{key}={}", display_text(item)));
}
}
}
_ => {}
}
}
if parts.is_empty() {
None
} else {
Some(parts.join("&"))
}
}
}
fn string_pairs(pairs: &[(String, String)]) -> Value {
Value::Map(
pairs
.iter()
.map(|(k, v)| (Value::from(k.as_str()), Value::from(v.as_str())))
.collect(),
)
}
fn display_text(value: &Value) -> String {
match value {
Value::String(s) => s.as_str().unwrap_or_default().to_string(),
Value::Nil => String::new(),
other => other.to_string(),
}
}
struct StreamTarget {
reply_to: String,
cid: String,
envelope_mode: bool,
trace: Option<RelayTrace>,
}
#[derive(Clone)]
struct RelayTrace {
trace_id: String,
trace_path: String,
span_id: Option<String>,
}
impl RelayTrace {
fn capture(po: &PostOffice) -> Option<Self> {
Some(RelayTrace {
trace_id: po.my_trace_id()?,
trace_path: po.my_trace_path()?,
span_id: po.my_span_id(),
})
}
fn stamp(trace: &Option<RelayTrace>, segment: EventEnvelope) -> EventEnvelope {
match trace {
Some(t) => {
let segment = segment.set_trace(&t.trace_id, &t.trace_path);
match &t.span_id {
Some(span) => segment.set_span_id(span),
None => segment,
}
}
None => segment,
}
}
}
pub(crate) async fn handle(
platform: &Platform,
_headers: HashMap<String, String>,
event: EventEnvelope,
) -> Result<EventEnvelope, AppError> {
let po = PostOffice::new(platform);
let Some(reply_to) = event.reply_to().map(str::to_string) else {
if let Err(e) = process_request(platform, &po, &_headers, &event, None).await {
log::error!("Unhandled exception (no reply-to) - {}", e.message());
}
return EventEnvelope::new().set_body("ignored");
};
let cid = event.correlation_id().unwrap_or_default().to_string();
let envelope_mode = event.headers().iter().any(|(name, value)| {
name.eq_ignore_ascii_case(super::event_api::X_EVENT_API) && value == STREAM_RELAY
});
let stream_target = Some(StreamTarget {
reply_to: reply_to.clone(),
cid: cid.clone(),
envelope_mode,
trace: RelayTrace::capture(&po),
});
let response = match process_request(platform, &po, &_headers, &event, stream_target).await {
Ok(None) => return EventEnvelope::new().set_body("ignored"),
Ok(Some(response)) => response,
Err(e) => EventEnvelope::new()
.set_status(e.status())
.set_raw_body(Value::from(e.message())),
};
let _ = po
.send(response.set_to(&reply_to).set_correlation_id(&cid))
.await;
EventEnvelope::new().set_body("ignored")
}
fn accepts_event_stream(request: &AsyncHttpRequest) -> bool {
request.headers().iter().any(|(name, value)| {
name.eq_ignore_ascii_case("accept") && value.contains("text/event-stream")
})
}
async fn process_request(
platform: &Platform,
po: &PostOffice,
invocation_headers: &HashMap<String, String>,
event: &EventEnvelope,
stream_target: Option<StreamTarget>,
) -> Result<Option<EventEnvelope>, AppError> {
let request = AsyncHttpRequest::from_value(event.body());
let (secure, host, port) = validate_url(&request)?;
let uri = request.finalized_url();
po.annotate_trace(
"destination",
format!(
"{}{}",
request.target_host().unwrap_or_default(),
raw_url(&uri)
),
);
let method = request.method().to_string();
if !matches!(
method.as_str(),
"GET" | "HEAD" | "PUT" | "POST" | "PATCH" | "DELETE" | "OPTIONS"
) {
return Err(AppError::new(405, "Method not allowed"));
}
let connect_ms = connect_timeout_ms();
let stream = tokio::time::timeout(
Duration::from_millis(connect_ms),
tokio::net::TcpStream::connect((host.as_str(), port)),
)
.await
.map_err(|_| AppError::new(408, format!("Connection timeout for {host}:{port}")))?
.map_err(|e| AppError::new(500, format!("Unable to connect to {host}:{port} - {e}")))?;
let mut sender = if secure {
let connector = tls_connector(request.trust_all_cert())?;
let server_name = ServerName::try_from(host.clone())
.map_err(|e| AppError::new(400, format!("Invalid TLS server name {host} - {e}")))?;
let tls_stream = connector
.connect(server_name, stream)
.await
.map_err(|e| AppError::new(500, format!("TLS handshake failed for {host} - {e}")))?;
let io = hyper_util::rt::TokioIo::new(tls_stream);
let (sender, connection) = hyper::client::conn::http1::handshake(io)
.await
.map_err(|e| AppError::new(500, format!("HTTP handshake failed - {e}")))?;
tokio::spawn(async move {
let _ = connection.await;
});
sender
} else {
let io = hyper_util::rt::TokioIo::new(stream);
let (sender, connection) = hyper::client::conn::http1::handshake(io)
.await
.map_err(|e| AppError::new(500, format!("HTTP handshake failed - {e}")))?;
tokio::spawn(async move {
let _ = connection.await;
});
sender
};
let mut builder = hyper::Request::builder()
.method(method.as_str())
.uri(if uri.is_empty() { "/" } else { &uri });
builder = apply_headers(
builder,
invocation_headers,
&request,
event,
&host,
port,
secure,
);
let body_bytes = request_body_bytes(&request, &method)?;
let http_request = builder
.body(Full::new(Bytes::from(body_bytes)))
.map_err(|e| AppError::new(400, format!("Invalid HTTP request - {e}")))?;
let ttl = Duration::from_secs(request.timeout_seconds() + 1);
let http_response = tokio::time::timeout(ttl, sender.send_request(http_request))
.await
.map_err(|_| AppError::new(408, format!("Timeout for {} ms", ttl.as_millis())))?
.map_err(|e| AppError::new(500, format!("HTTP request failed - {e}")))?;
let mut response = EventEnvelope::new().set_status(http_response.status().as_u16() as i32);
let mut content_type: Option<String> = None;
let mut has_content_length = false;
for (name, value) in http_response.headers() {
let key = name.as_str();
let text = value.to_str().unwrap_or_default();
if key.eq_ignore_ascii_case("content-type") {
content_type = Some(text.to_lowercase());
}
if key.eq_ignore_ascii_case("content-length") {
has_content_length = true;
}
response = response.set_header(key, text);
}
let envelope_mode = stream_target.as_ref().is_some_and(|t| t.envelope_mode);
if let Some(target) = stream_target {
let sse = content_type
.as_deref()
.is_some_and(|ct| ct.starts_with("text/event-stream"));
if sse && accepts_event_stream(&request) {
let status = http_response.status().as_u16() as i32;
let relay_platform = platform.clone();
if target.envelope_mode {
let idle = Duration::from_secs(request.timeout_seconds().max(1) + 1);
tokio::spawn(relay_envelope_sse(
relay_platform,
http_response.into_body(),
target.reply_to,
target.cid,
idle,
target.trace,
));
} else {
let idle = Duration::from_secs(request.timeout_seconds().max(1));
tokio::spawn(relay_sse(
relay_platform,
http_response.into_body(),
status,
target.reply_to,
target.cid,
idle,
target.trace,
));
}
return Ok(None);
}
}
let bytes = http_response
.into_body()
.collect()
.await
.map_err(|e| AppError::new(500, format!("Unable to read HTTP response - {e}")))?
.to_bytes();
if envelope_mode {
return Ok(Some(decode_relay_reply(&bytes, response.status())));
}
if !has_content_length {
response = response.set_header("x-content-length", &bytes.len().to_string());
}
Ok(Some(response.set_raw_body(decode_response_body(
&bytes,
content_type.as_deref(),
))))
}
fn decode_relay_reply(bytes: &[u8], http_status: i32) -> EventEnvelope {
if bytes.is_empty() {
return EventEnvelope::new().set_status(http_status);
}
match EventEnvelope::from_bytes(bytes) {
Ok(envelope) => envelope.clear_reply_to(),
Err(e) => rest_error_reply(bytes, http_status).unwrap_or_else(|| {
EventEnvelope::new()
.set_status(400)
.set_raw_body(Value::from(format!(
"Did you configure rest.yaml correctly? Invalid result set - {}",
e.message()
)))
}),
}
}
fn rest_error_reply(bytes: &[u8], http_status: i32) -> Option<EventEnvelope> {
if http_status < 400 {
return None;
}
let data = serde_json::from_slice::<serde_json::Value>(bytes).ok()?;
if data.get("type").and_then(|v| v.as_str()) != Some("error") {
return None;
}
let message = data.get("message").and_then(|v| v.as_str())?;
Some(
EventEnvelope::new()
.set_status(http_status)
.set_raw_body(Value::from(message)),
)
}
#[derive(Default)]
struct SseParser {
pending: Vec<u8>,
data_lines: Vec<String>,
event_name: Option<String>,
}
impl SseParser {
fn feed(&mut self, chunk: &[u8]) -> Vec<(Option<String>, String)> {
self.pending.extend_from_slice(chunk);
let mut events = Vec::new();
let mut start = 0;
let buffer = std::mem::take(&mut self.pending);
for i in 0..buffer.len() {
if buffer[i] == b'\n' {
let end = if i > start && buffer[i - 1] == b'\r' {
i - 1
} else {
i
};
let line = String::from_utf8_lossy(&buffer[start..end]).to_string();
start = i + 1;
if line.is_empty() {
if !self.data_lines.is_empty() {
events.push((self.event_name.take(), self.data_lines.join("\n")));
}
self.data_lines.clear();
self.event_name = None;
} else if !line.starts_with(':') {
let (field, value) = match line.find(':') {
Some(colon) => (&line[..colon], &line[colon + 1..]),
None => (line.as_str(), ""),
};
let value = value.strip_prefix(' ').unwrap_or(value);
match field {
"data" => self.data_lines.push(value.to_string()),
"event" => self.event_name = Some(value.to_string()),
_ => { }
}
}
}
}
self.pending = buffer[start..].to_vec();
events
}
}
#[allow(clippy::too_many_arguments)]
async fn relay_sse(
platform: Platform,
mut body: hyper::body::Incoming,
status: i32,
reply_to: String,
cid: String,
idle: Duration,
trace: Option<RelayTrace>,
) {
let po = PostOffice::new(&platform);
let mut parser = SseParser::default();
let mut head_sent = false;
loop {
match tokio::time::timeout(idle, body.frame()).await {
Ok(Some(Ok(frame))) => {
if let Some(data) = frame.data_ref() {
for (name, text) in parser.feed(data) {
let mut segment = EventEnvelope::new()
.set_header(event_stream::X_EVENT_STREAM, event_stream::DATA);
if let Some(name) = name.filter(|n| !n.is_empty()) {
segment = segment.set_header(event_stream::X_EVENT_NAME, &name);
}
segment = match segment.set_body(text) {
Ok(seg) => seg,
Err(_) => continue,
};
if !head_sent {
head_sent = true;
segment = RelayTrace::stamp(
&trace,
segment
.set_status(status)
.set_header("content-type", "text/event-stream"),
);
}
if send_segment(&po, segment, &reply_to, &cid).await.is_err() {
return;
}
}
}
}
Ok(None) => {
let mut eof = EventEnvelope::new()
.set_header(event_stream::X_EVENT_STREAM, event_stream::EOF);
if !head_sent {
eof = eof
.set_status(status)
.set_header("content-type", "text/event-stream");
}
let _ = send_segment(&po, RelayTrace::stamp(&trace, eof), &reply_to, &cid).await;
return;
}
Ok(Some(Err(e))) => {
fail_in_band(&po, &reply_to, &cid, 500, &e.to_string(), head_sent, &trace).await;
return;
}
Err(_) => {
let message = format!("Timeout for {} seconds", idle.as_secs());
fail_in_band(&po, &reply_to, &cid, 408, &message, head_sent, &trace).await;
return;
}
}
}
}
async fn relay_envelope_sse(
platform: Platform,
mut body: hyper::body::Incoming,
reply_to: String,
cid: String,
idle: Duration,
trace: Option<RelayTrace>,
) {
let po = PostOffice::new(&platform);
let mut parser = SseParser::default();
let mut head_seen = false;
loop {
match tokio::time::timeout(idle, body.frame()).await {
Ok(Some(Ok(frame))) => {
if let Some(data) = frame.data_ref() {
for (name, text) in parser.feed(data) {
match relay_envelope_event(
&po,
name,
text,
&reply_to,
&cid,
&mut head_seen,
&trace,
)
.await
{
RelayFlow::Next => {}
RelayFlow::End => return,
}
}
}
}
Ok(None) => {
fail_in_band(
&po,
&reply_to,
&cid,
500,
"Event stream ended without eof",
head_seen,
&trace,
)
.await;
return;
}
Ok(Some(Err(e))) => {
fail_in_band(&po, &reply_to, &cid, 500, &e.to_string(), head_seen, &trace).await;
return;
}
Err(_) => {
let message = format!("Timeout for {} seconds", idle.as_secs());
fail_in_band(&po, &reply_to, &cid, 408, &message, head_seen, &trace).await;
return;
}
}
}
}
enum RelayFlow {
Next,
End,
}
async fn relay_envelope_event(
po: &PostOffice,
name: Option<String>,
text: String,
reply_to: &str,
cid: &str,
head_seen: &mut bool,
trace: &Option<RelayTrace>,
) -> RelayFlow {
use base64::Engine as _;
if name.as_deref() == Some(event_stream::ENVELOPE) {
let decoded = base64::engine::general_purpose::STANDARD
.decode(&text)
.ok()
.and_then(|bytes| EventEnvelope::from_bytes(&bytes).ok());
let Some(decoded) = decoded else {
fail_in_band(
po,
reply_to,
cid,
500,
"Invalid event stream - malformed envelope frame",
*head_seen,
trace,
)
.await;
return RelayFlow::End;
};
*head_seen = true;
let terminal = decoded.headers().iter().any(|(key, value)| {
key.eq_ignore_ascii_case(event_stream::X_EVENT_STREAM)
&& (value.eq_ignore_ascii_case(event_stream::EOF)
|| value.eq_ignore_ascii_case(event_stream::EXCEPTION))
});
let _ = send_segment(po, decoded.clear_reply_to(), reply_to, cid).await;
if terminal {
RelayFlow::End
} else {
RelayFlow::Next
}
} else if !*head_seen {
fail_in_band(
po,
reply_to,
cid,
500,
"Invalid event stream - missing envelope head",
false,
trace,
)
.await;
RelayFlow::End
} else {
let mut segment =
EventEnvelope::new().set_header(event_stream::X_EVENT_STREAM, event_stream::DATA);
if let Some(name) = name.filter(|n| !n.is_empty()) {
segment = segment.set_header(event_stream::X_EVENT_NAME, &name);
}
if let Ok(segment) = segment.set_body(text) {
let _ = send_segment(po, segment, reply_to, cid).await;
}
RelayFlow::Next
}
}
async fn fail_in_band(
po: &PostOffice,
reply_to: &str,
cid: &str,
status: i32,
message: &str,
head_sent: bool,
trace: &Option<RelayTrace>,
) {
let body = serde_json::json!({"type": "error", "status": status, "message": message});
let Ok(mut error) = EventEnvelope::new()
.set_header(event_stream::X_EVENT_STREAM, event_stream::EXCEPTION)
.set_status(status)
.set_body(body)
else {
return;
};
if !head_sent {
error = error.set_header("content-type", "text/event-stream");
}
let _ = send_segment(po, RelayTrace::stamp(trace, error), reply_to, cid).await;
}
async fn send_segment(
po: &PostOffice,
segment: EventEnvelope,
reply_to: &str,
cid: &str,
) -> Result<(), AppError> {
po.send(segment.set_to(reply_to).set_correlation_id(cid))
.await
}
fn raw_url(uri: &str) -> &str {
match uri.rfind('?') {
Some(mark) => &uri[..mark],
None => uri,
}
}
fn connect_timeout_ms() -> u64 {
let config = AppConfigReader::get_instance();
config
.get_property_or("http.client.connection.timeout", "5000")
.parse::<u64>()
.unwrap_or(5000)
.max(2000)
}
fn validate_url(request: &AsyncHttpRequest) -> Result<(bool, String, u16), AppError> {
let Some(target) = request.target_host() else {
return Err(AppError::new(
400,
"Missing target host. e.g. https://hostname",
));
};
let (secure, rest) = if let Some(rest) = target.strip_prefix("http://") {
(false, rest)
} else if let Some(rest) = target.strip_prefix("https://") {
(true, rest)
} else {
return Err(AppError::new(400, "Protocol must be http or https"));
};
let authority = rest.trim_end_matches('/');
if authority.contains('/') {
return Err(AppError::new(400, "Target host must not contain URI path"));
}
let default_port = if secure { 443 } else { 80 };
let (host, port) = match authority.rsplit_once(':') {
Some((h, p)) => (
h.to_string(),
p.parse::<u16>()
.map_err(|_| AppError::new(400, "Invalid port number in target host"))?,
),
None => (authority.to_string(), default_port),
};
if host.trim().is_empty() {
return Err(AppError::new(
400,
"Unable to resolve target host as domain or IP address",
));
}
Ok((secure, host, port))
}
fn tls_connector(trust_all_cert: bool) -> Result<TlsConnector, AppError> {
static STRICT: OnceLock<Result<Arc<rustls::ClientConfig>, String>> = OnceLock::new();
static TRUST_ALL: OnceLock<Arc<rustls::ClientConfig>> = OnceLock::new();
let config = if trust_all_cert {
TRUST_ALL
.get_or_init(|| {
let config = rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(Arc::new(TrustAllVerifier))
.with_no_client_auth();
Arc::new(config)
})
.clone()
} else {
STRICT
.get_or_init(|| {
let loaded = rustls_native_certs::load_native_certs();
let mut roots = rustls::RootCertStore::empty();
for cert in loaded.certs {
let _ = roots.add(cert);
}
if roots.is_empty() {
return Err(format!(
"No usable certificates in the OS trust store - {:?}",
loaded.errors
));
}
Ok(Arc::new(
rustls::ClientConfig::builder()
.with_root_certificates(roots)
.with_no_client_auth(),
))
})
.clone()
.map_err(|e| AppError::new(500, e))?
};
Ok(TlsConnector::from(config))
}
#[derive(Debug)]
struct TrustAllVerifier;
impl rustls::client::danger::ServerCertVerifier for TrustAllVerifier {
fn verify_server_cert(
&self,
_end_entity: &rustls::pki_types::CertificateDer<'_>,
_intermediates: &[rustls::pki_types::CertificateDer<'_>],
_server_name: &ServerName<'_>,
_ocsp_response: &[u8],
_now: rustls::pki_types::UnixTime,
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
Ok(rustls::client::danger::ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
message: &[u8],
cert: &rustls::pki_types::CertificateDer<'_>,
dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
rustls::crypto::verify_tls12_signature(
message,
cert,
dss,
&rustls::crypto::ring::default_provider().signature_verification_algorithms,
)
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &rustls::pki_types::CertificateDer<'_>,
dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
rustls::crypto::verify_tls13_signature(
message,
cert,
dss,
&rustls::crypto::ring::default_provider().signature_verification_algorithms,
)
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
rustls::crypto::ring::default_provider()
.signature_verification_algorithms
.supported_schemes()
}
}
#[allow(clippy::too_many_arguments)]
fn apply_headers(
mut builder: hyper::http::request::Builder,
invocation_headers: &HashMap<String, String>,
request: &AsyncHttpRequest,
event: &EventEnvelope,
host: &str,
port: u16,
secure: bool,
) -> hyper::http::request::Builder {
let default_port = if secure { 443 } else { 80 };
let authority = if port == default_port {
host.to_string()
} else {
format!("{host}:{port}")
};
builder = builder.header("host", authority);
builder = builder.header("user-agent", USER_AGENT_NAME);
let mut merged: Vec<(String, String)> = request.headers.clone();
merged.extend(request.session.iter().cloned());
for (key, value) in &merged {
if permitted_http_header(key) {
builder = builder.header(key.as_str(), value.trim());
}
}
if !merged.iter().any(|(k, _)| k.eq_ignore_ascii_case("accept")) {
builder = builder.header("accept", "*/*");
}
let config = AppConfigReader::get_instance();
if let Some(trace_id) = event.trace_id() {
let trace_header = config.get_property_or("http.trace.id.header", "X-Trace-Id");
stamp_header(&mut builder, &trace_header, trace_id);
if let Some(traceparent) = w3c_trace::format(trace_id, event.span_id().unwrap_or_default())
{
stamp_header(&mut builder, w3c_trace::TRACEPARENT, &traceparent);
let custom_traceparent =
config.get_property_or("http.traceparent.header", w3c_trace::TRACEPARENT);
if !custom_traceparent.eq_ignore_ascii_case(w3c_trace::TRACEPARENT) {
stamp_header(&mut builder, &custom_traceparent, &traceparent);
}
}
}
if request.header(super::event_api::X_EVENT_API).is_none() {
if let Some(business_cid) = invocation_headers.get(crate::automation::MY_CORRELATION_ID) {
let cid_header =
config.get_property_or("http.correlation.id.header", "X-Correlation-Id");
if request.header(&cid_header).is_none() {
stamp_header(&mut builder, &cid_header, business_cid.as_str());
}
}
}
if !request.cookies.is_empty() {
let cookie = request
.cookies
.iter()
.map(|(k, v)| format!("{k}={}", url_encode(v)))
.collect::<Vec<_>>()
.join("; ");
builder = builder.header("cookie", cookie.as_str());
}
builder
}
fn permitted_http_header(header: &str) -> bool {
!HEADERS_TO_IGNORE
.iter()
.any(|ignored| header.eq_ignore_ascii_case(ignored))
}
fn stamp_header(builder: &mut hyper::http::request::Builder, name: &str, value: &str) {
if let Some(headers) = builder.headers_mut() {
if let (Ok(name), Ok(value)) = (
hyper::header::HeaderName::from_bytes(name.as_bytes()),
hyper::header::HeaderValue::from_str(value),
) {
headers.insert(name, value);
}
}
}
fn url_encode(text: &str) -> String {
let mut out = String::with_capacity(text.len());
for byte in text.bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' | b'*' => {
out.push(byte as char)
}
b' ' => out.push('+'),
other => out.push_str(&format!("%{other:02X}")),
}
}
out
}
fn request_body_bytes(request: &AsyncHttpRequest, method: &str) -> Result<Vec<u8>, AppError> {
if !matches!(method, "POST" | "PUT" | "PATCH") {
return Ok(Vec::new());
}
match request.body() {
Value::Nil => Ok(Vec::new()),
Value::Binary(bytes) => Ok(bytes.clone()),
Value::String(text) => Ok(text.as_str().unwrap_or_default().as_bytes().to_vec()),
value @ (Value::Map(_) | Value::Array(_)) => {
let json = serde_json::to_value(crate::serializer::strip_nulls(value))
.map_err(|e| AppError::new(400, format!("Invalid HTTP request body - {e}")))?;
serde_json::to_vec(&json)
.map_err(|e| AppError::new(400, format!("Invalid HTTP request body - {e}")))
}
_ => Err(AppError::new(400, "Invalid HTTP request body")),
}
}
fn decode_response_body(bytes: &[u8], content_type: Option<&str>) -> Value {
let Some(content_type) = content_type else {
return Value::from(bytes.to_vec());
};
if content_type.starts_with("application/json") {
let text = String::from_utf8_lossy(bytes).trim().to_string();
if text.is_empty() {
return Value::Map(vec![]);
}
if (text.starts_with('{') && text.ends_with('}'))
|| (text.starts_with('[') && text.ends_with(']'))
{
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&text) {
if let Ok(value) = rmpv::ext::to_value(&json) {
return value;
}
}
}
return Value::from(text);
}
if content_type.starts_with("text/")
|| content_type.starts_with("application/javascript")
|| content_type.starts_with("application/xml")
{
return Value::from(String::from_utf8_lossy(bytes).to_string());
}
Value::from(bytes.to_vec())
}
pub struct AsyncHttpClientService {
platform: Platform,
}
impl AsyncHttpClientService {
pub fn new(platform: &Platform) -> Self {
AsyncHttpClientService {
platform: platform.clone(),
}
}
}
#[async_trait::async_trait]
impl crate::function::ComposableFunction for AsyncHttpClientService {
async fn handle_event(
&self,
headers: HashMap<String, String>,
input: EventEnvelope,
_instance: usize,
) -> Result<EventEnvelope, AppError> {
handle(&self.platform, headers, input).await
}
}
#[cfg(test)]
mod tests {
use super::*;
fn server_dataset_request() -> AsyncHttpRequest {
AsyncHttpRequest::new()
.set_method("POST")
.set_url("/api/typed/alice")
.set_remote_ip("127.0.0.1")
.set_secure(true)
.set_target_host("localhost:8085")
.set_header("accept", "application/json")
.set_header("x-api-key", "open-sesame")
.set_header("x-ttl", "5000")
.set_path_parameter("user", "alice")
.set_query_parameter_values("q", &["a", "b"])
.set_query_parameter("single", "1")
.set_query_string("q=a&q=b&single=1")
.set_route_timeout_seconds(30)
.set_cookie("first", "alpha")
.set_session_info("user_id", "u-1")
.set_body(Value::Map(vec![(
Value::from("note"),
Value::from("hello"),
)]))
}
#[test]
fn server_dataset_round_trips_through_the_typed_contract() {
let request = server_dataset_request();
assert_eq!(request.method(), "POST");
assert_eq!(request.url(), "/api/typed/alice");
assert_eq!(request.remote_ip(), Some("127.0.0.1"));
assert!(request.is_secure());
assert_eq!(request.target_host(), Some("localhost:8085"));
assert_eq!(request.path_parameter("user"), Some("alice"));
assert_eq!(request.query_parameter("single").as_deref(), Some("1"));
assert_eq!(request.query_parameter("q").as_deref(), Some("a"));
assert_eq!(request.query_parameters("q"), vec!["a", "b"]);
assert_eq!(request.query_parameters("single"), vec!["1"]);
assert_eq!(request.query_string(), Some("q=a&q=b&single=1"));
assert_eq!(
request.header("Accept"),
Some("application/json"),
"case-insensitive"
);
assert_eq!(request.cookie("first"), Some("alpha"));
assert_eq!(request.session_info("user_id"), Some("u-1"));
assert_eq!(request.timeout_seconds(), 5);
#[derive(serde::Deserialize)]
struct Note {
note: String,
}
assert_eq!(request.body_as::<Note>().expect("typed body").note, "hello");
let round = AsyncHttpRequest::from_value(&request.to_value());
assert_eq!(round.to_value(), request.to_value());
let deserialized: AsyncHttpRequest =
rmpv::ext::from_value(request.to_value()).expect("serde deserialize");
assert_eq!(deserialized.to_value(), request.to_value());
let serialized = rmpv::ext::to_value(&request).expect("serde serialize");
assert_eq!(
AsyncHttpRequest::from_value(&serialized).to_value(),
request.to_value()
);
}
#[test]
fn route_timeout_is_the_fallback_without_x_ttl() {
let request = AsyncHttpRequest::new()
.set_method("GET")
.set_url("/x")
.set_route_timeout_seconds(30);
assert_eq!(request.timeout_seconds(), 30);
assert_eq!(
AsyncHttpRequest::from_value(&request.to_value()).timeout_seconds(),
30
);
}
}