#![doc = include_str!("../../examples/daemon_http.rs")]
use alloc::string::String;
use alloc::vec::Vec;
use crate::error::{Error, Result};
pub const DEFAULT_TIMEOUT_MS: u32 = 30_000;
pub const DEFAULT_MAX_BODY: usize = 4 * 1024 * 1024;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Request {
pub method: String,
pub url: String,
pub headers: Vec<(String, String)>,
pub body: Vec<u8>,
pub timeout_ms: u32,
pub max_body: usize,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Response {
pub status: u16,
pub headers: Vec<(String, String)>,
pub body: Vec<u8>,
}
impl Response {
pub fn new(status: u16) -> Self {
Self {
status,
headers: Vec::new(),
body: Vec::new(),
}
}
pub fn with_header(mut self, name: &str, value: &str) -> Self {
self.headers
.push((name.to_ascii_lowercase(), String::from(value)));
self
}
pub fn with_body(mut self, body: impl Into<Vec<u8>>) -> Self {
self.body = body.into();
self
}
pub fn with_json<T: serde::Serialize + ?Sized>(self, value: &T) -> Result<Self> {
let body = serde_json::to_vec(value).map_err(Error::json)?;
Ok(self
.with_header("content-type", "application/json")
.with_body(body))
}
pub fn header(&self, name: &str) -> Option<&str> {
self.headers
.iter()
.find(|(key, _)| key.eq_ignore_ascii_case(name))
.map(|(_, value)| value.as_str())
}
pub fn ok(&self) -> bool {
(200..300).contains(&self.status)
}
pub fn text(&self) -> Result<&str> {
core::str::from_utf8(&self.body).map_err(|error| Error::Invalid(alloc::format!("{error}")))
}
pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T> {
serde_json::from_slice(&self.body).map_err(Error::json)
}
}
impl Request {
pub fn new(method: &str, url: &str) -> Self {
Self {
method: String::from(method),
url: String::from(url),
headers: Vec::new(),
body: Vec::new(),
timeout_ms: DEFAULT_TIMEOUT_MS,
max_body: DEFAULT_MAX_BODY,
}
}
pub fn get(url: &str) -> Self {
Self::new("GET", url)
}
pub fn post(url: &str) -> Self {
Self::new("POST", url)
}
pub fn header(mut self, name: &str, value: &str) -> Self {
self.headers.push((String::from(name), String::from(value)));
self
}
pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
self.body = body.into();
self
}
pub fn json<T: serde::Serialize + ?Sized>(self, value: &T) -> Result<Self> {
let body = serde_json::to_vec(value).map_err(Error::json)?;
Ok(self.header("content-type", "application/json").body(body))
}
pub fn timeout_ms(mut self, timeout_ms: u32) -> Self {
self.timeout_ms = timeout_ms.max(1);
self
}
pub fn max_body(mut self, bytes: usize) -> Self {
self.max_body = bytes;
self
}
pub async fn send(self) -> Result<Response> {
let target = split_url(&self.url)?;
#[cfg(target_arch = "wasm32")]
let answer = super::http_wasi::send(&self, &target).await;
#[cfg(not(target_arch = "wasm32"))]
let answer = crate::testing::http_send(&self, &target);
answer
}
}
pub async fn get(url: &str) -> Result<Response> {
Request::get(url).send().await
}
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
pub(crate) fn timeout() -> Error {
Error::Unavailable("timeout".into())
}
pub(crate) fn too_long(max: usize) -> Error {
Error::Invalid(alloc::format!("the response body is over {max} bytes"))
}
pub(crate) struct Target<'a> {
pub scheme: &'static str,
pub authority: &'a str,
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
pub path: &'a str,
}
impl Target<'_> {
pub fn host(&self) -> String {
let host = match self.authority.rsplit_once(':') {
Some((host, port)) if port.bytes().all(|byte| byte.is_ascii_digit()) => host,
_ => self.authority,
};
host.to_ascii_lowercase()
}
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
pub fn port(&self) -> Option<u16> {
self.authority
.rsplit_once(':')
.and_then(|(_, port)| port.parse().ok())
}
pub fn denied(&self) -> Error {
Error::GrantDenied {
grant: alloc::format!("fetch:{}", self.host()),
}
}
}
pub(crate) fn split_url(url: &str) -> Result<Target<'_>> {
let (scheme, rest) = if let Some(rest) = url.strip_prefix("https://") {
("https", rest)
} else if let Some(rest) = url.strip_prefix("http://") {
("http", rest)
} else {
return Err(Error::Invalid(alloc::format!("not an http(s) url: {url}")));
};
let (authority, path) = match rest.find(['/', '?']) {
Some(index) if rest.as_bytes()[index] == b'/' => (&rest[..index], &rest[index..]),
Some(index) => (&rest[..index], &rest[index..]),
None => (rest, "/"),
};
if authority.is_empty() {
return Err(Error::Invalid(alloc::format!("no host in {url}")));
}
Ok(Target {
scheme,
authority,
path,
})
}