use super::*;
pub(super) fn http_request(method: &str, url: &str, body: Option<&str>) -> Value {
use std::time::Duration;
let agent: ureq::Agent = ureq::Agent::config_builder()
.timeout_connect(Some(Duration::from_secs(10)))
.timeout_recv_body(Some(Duration::from_secs(30)))
.timeout_send_body(Some(Duration::from_secs(10)))
.http_status_as_error(false)
.build()
.into();
let resp = match (method, body) {
("GET", _) => agent.get(url).call(),
("POST", Some(b)) => agent.post(url).send(b),
("POST", None) => agent.post(url).send(""),
(m, _) => return err_value(format!("unsupported method: {m}")),
};
match resp {
Ok(mut r) => {
let status = r.status().as_u16();
let body = r.body_mut().read_to_string().unwrap_or_default();
if (200..300).contains(&status) {
Value::Variant { name: "Ok".into(), args: vec![Value::Str(body.into())] }
} else {
err_value(format!("status {status}: {body}"))
}
}
Err(e) => err_value(format!("transport: {e}")),
}
}
pub(super) fn http_stream_agent() -> ureq::Agent {
use std::time::Duration;
ureq::Agent::config_builder()
.timeout_global(Some(Duration::from_secs(600)))
.http_status_as_error(false)
.build()
.into()
}
pub(super) fn http_agent(timeout_ms: Option<u64>) -> ureq::Agent {
use std::time::Duration;
match timeout_ms {
Some(ms) => ureq::Agent::config_builder()
.timeout_global(Some(Duration::from_millis(ms)))
.http_status_as_error(false)
.build()
.into(),
None => ureq::Agent::config_builder()
.timeout_connect(Some(Duration::from_secs(10)))
.timeout_recv_body(Some(Duration::from_secs(30)))
.timeout_send_body(Some(Duration::from_secs(10)))
.http_status_as_error(false)
.build()
.into(),
}
}
pub(super) fn http_error_value(e: ureq::Error) -> Value {
let (ctor, payload): (&str, Option<String>) = match &e {
ureq::Error::Timeout(_) => ("TimeoutError", None),
ureq::Error::Tls(s) => ("TlsError", Some((*s).into())),
ureq::Error::Pem(p) => ("TlsError", Some(format!("{p}"))),
ureq::Error::Rustls(r) => ("TlsError", Some(format!("{r}"))),
_ => ("NetworkError", Some(format!("{e}"))),
};
let args = match payload { Some(s) => vec![Value::Str(s.into())], None => vec![] };
let inner = Value::Variant { name: ctor.into(), args };
Value::Variant { name: "Err".into(), args: vec![inner] }
}
pub(super) fn http_decode_err(msg: String) -> Value {
let inner = Value::Variant {
name: "DecodeError".into(),
args: vec![Value::Str(msg.into())],
};
Value::Variant { name: "Err".into(), args: vec![inner] }
}
pub(super) fn http_send_simple(
method: &str,
url: &str,
body: Option<Vec<u8>>,
content_type: &str,
timeout_ms: Option<u64>,
) -> Value {
http_send_full(method, url, body, content_type, &[], timeout_ms)
}
pub(super) fn http_send_full(
method: &str,
url: &str,
body: Option<Vec<u8>>,
content_type: &str,
headers: &[(String, String)],
timeout_ms: Option<u64>,
) -> Value {
let agent = http_agent(timeout_ms);
let method_upper = method.to_ascii_uppercase();
let body_bytes: Vec<u8> = body.unwrap_or_default();
let resp = match method_upper.as_str() {
"GET" => {
let mut req = agent.get(url);
if !content_type.is_empty() { req = req.header("content-type", content_type); }
for (k, v) in headers { req = req.header(k.as_str(), v.as_str()); }
req.call()
}
"HEAD" => {
let mut req = agent.head(url);
for (k, v) in headers { req = req.header(k.as_str(), v.as_str()); }
req.call()
}
"DELETE" => {
let mut req = agent.delete(url);
for (k, v) in headers { req = req.header(k.as_str(), v.as_str()); }
req.call()
}
"POST" => {
let mut req = agent.post(url);
if !content_type.is_empty() { req = req.header("content-type", content_type); }
for (k, v) in headers { req = req.header(k.as_str(), v.as_str()); }
req.send(&body_bytes[..])
}
"PUT" => {
let mut req = agent.put(url);
if !content_type.is_empty() { req = req.header("content-type", content_type); }
for (k, v) in headers { req = req.header(k.as_str(), v.as_str()); }
req.send(&body_bytes[..])
}
"PATCH" => {
let mut req = agent.patch(url);
if !content_type.is_empty() { req = req.header("content-type", content_type); }
for (k, v) in headers { req = req.header(k.as_str(), v.as_str()); }
req.send(&body_bytes[..])
}
m => {
return http_decode_err(format!("unsupported method: {m}"));
}
};
match resp {
Ok(mut r) => {
let status = r.status().as_u16() as i64;
let headers_map = collect_response_headers(r.headers());
let body_bytes = match r.body_mut().with_config().limit(10 * 1024 * 1024).read_to_vec() {
Ok(b) => b,
Err(e) => return http_decode_err(format!("body read: {e}")),
};
let mut rec = indexmap::IndexMap::new();
rec.insert("status".into(), Value::Int(status));
rec.insert("headers".into(), Value::Map(headers_map));
rec.insert("body".into(), Value::Bytes(body_bytes));
Value::Variant { name: "Ok".into(), args: vec![Value::record_dynamic(rec)] }
}
Err(e) => http_error_value(e),
}
}
pub(super) fn collect_response_headers(
headers: &ureq::http::HeaderMap,
) -> std::collections::BTreeMap<lex_bytecode::MapKey, Value> {
let mut out = std::collections::BTreeMap::new();
for (name, value) in headers.iter() {
let v = value.to_str().unwrap_or("").to_string();
out.insert(lex_bytecode::MapKey::Str(name.as_str().to_string()), Value::Str(v.into()));
}
out
}
pub(super) fn http_send_record(handler: &DefaultHandler, req: &indexmap::IndexMap<smol_str::SmolStr, Value>) -> Value {
let method = match req.get("method") {
Some(Value::Str(s)) => s.to_string(),
_ => return http_decode_err("HttpRequest.method must be Str".into()),
};
let url = match req.get("url") {
Some(Value::Str(s)) => s.to_string(),
_ => return http_decode_err("HttpRequest.url must be Str".into()),
};
if let Err(e) = handler.ensure_host_allowed(&url) {
return http_decode_err(e);
}
let body = match req.get("body") {
Some(Value::Variant { name, args }) if name == "None" => None,
Some(Value::Variant { name, args }) if name == "Some" => match args.as_slice() {
[Value::Bytes(b)] => Some(b.clone()),
_ => return http_decode_err("HttpRequest.body Some payload must be Bytes".into()),
},
_ => return http_decode_err("HttpRequest.body must be Option[Bytes]".into()),
};
let timeout_ms = match req.get("timeout_ms") {
Some(Value::Variant { name, .. }) if name == "None" => None,
Some(Value::Variant { name, args }) if name == "Some" => match args.as_slice() {
[Value::Int(n)] if *n >= 0 => Some(*n as u64),
_ => return http_decode_err(
"HttpRequest.timeout_ms Some payload must be a non-negative Int".into()),
},
_ => return http_decode_err("HttpRequest.timeout_ms must be Option[Int]".into()),
};
let headers: Vec<(String, String)> = match req.get("headers") {
Some(Value::Map(m)) => m.iter().filter_map(|(k, v)| {
let kk = match k { lex_bytecode::MapKey::Str(s) => s.clone(), _ => return None };
let vv = match v { Value::Str(s) => s.to_string(), _ => return None };
Some((kk, vv))
}).collect(),
_ => return http_decode_err("HttpRequest.headers must be Map[Str, Str]".into()),
};
http_send_full(&method, &url, body, "", &headers, timeout_ms)
}
pub(super) fn http_stream_lines_impl(handler: &DefaultHandler, url: &str, headers_val: &Value, body: &str) -> Value {
let body_bytes = body.as_bytes().to_vec();
let agent = http_stream_agent();
let mut req = agent.post(url);
if let Value::Map(headers) = headers_val {
for (k, v) in headers {
let key_str = match k {
lex_bytecode::MapKey::Str(s) => s.as_str(),
_ => continue,
};
if let Value::Str(val) = v {
req = req.header(key_str, val.as_str());
}
}
}
match req.send(&body_bytes[..]) {
Ok(resp) => {
use std::io::BufRead;
let reader = std::io::BufReader::new(resp.into_body().into_reader());
let lines = reader
.lines()
.map_while(Result::ok)
.map(|l| decode_unicode_escapes(&l));
let handle = handler.register_stream(lines);
ok(stream_handle_value(handle))
}
Err(e) => err(Value::Str(format!("http.stream_lines: {e}").into())),
}
}