mod bindings {
wit_bindgen::generate!({
path: "wit",
world: "http-client",
generate_all,
});
}
use bindings::wasi::http::outgoing_handler;
use bindings::wasi::http::types::{
Fields, Method, OutgoingBody, OutgoingRequest, RequestOptions, Scheme,
};
use bindings::wasi::io::streams::StreamError;
#[derive(Debug, Clone)]
pub struct Request {
pub method: String,
pub url: String,
pub headers: Vec<(String, String)>,
pub body: Vec<u8>,
pub timeout: Option<core::time::Duration>,
}
impl Request {
pub fn get(url: impl Into<String>) -> Self {
Self {
method: "GET".into(),
url: url.into(),
headers: Vec::new(),
body: Vec::new(),
timeout: None,
}
}
pub fn post(url: impl Into<String>, body: impl Into<Vec<u8>>) -> Self {
Self {
method: "POST".into(),
url: url.into(),
headers: Vec::new(),
body: body.into(),
timeout: None,
}
}
pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.push((name.into(), value.into()));
self
}
pub fn timeout(mut self, dur: core::time::Duration) -> Self {
self.timeout = Some(dur);
self
}
}
#[derive(Debug, Clone)]
pub struct Response {
pub status: u16,
pub headers: Vec<(String, String)>,
pub body: Vec<u8>,
}
impl Response {
pub fn is_success(&self) -> bool {
(200..300).contains(&self.status)
}
pub fn text(&self) -> String {
String::from_utf8_lossy(&self.body).into_owned()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HttpError {
pub message: String,
}
impl HttpError {
fn new(msg: impl Into<String>) -> Self {
Self {
message: msg.into(),
}
}
}
impl core::fmt::Display for HttpError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "http: {}", self.message)
}
}
impl std::error::Error for HttpError {}
pub fn get(url: &str) -> Result<Response, HttpError> {
send(Request::get(url))
}
pub fn post(url: &str, body: &[u8]) -> Result<Response, HttpError> {
send(Request::post(url, body.to_vec()))
}
pub fn send(req: Request) -> Result<Response, HttpError> {
let (scheme, rest) = if let Some(r) = req.url.strip_prefix("http://") {
(Scheme::Http, r)
} else if let Some(r) = req.url.strip_prefix("https://") {
(Scheme::Https, r)
} else {
return Err(HttpError::new(format!(
"unsupported url scheme: {}",
req.url
)));
};
let (authority, path) = match rest.find('/') {
Some(i) => (&rest[..i], &rest[i..]),
None => (rest, "/"),
};
let method = match req.method.as_str() {
"GET" => Method::Get,
"POST" => Method::Post,
"PUT" => Method::Put,
"DELETE" => Method::Delete,
"HEAD" => Method::Head,
"PATCH" => Method::Patch,
other => Method::Other(other.to_string()),
};
let fields = Fields::new();
for (name, value) in &req.headers {
fields
.append(name, &value.clone().into_bytes())
.map_err(|e| HttpError::new(format!("header {name}: {e:?}")))?;
}
let outgoing = OutgoingRequest::new(fields);
outgoing
.set_method(&method)
.map_err(|_| HttpError::new("set_method"))?;
outgoing
.set_scheme(Some(&scheme))
.map_err(|_| HttpError::new("set_scheme"))?;
outgoing
.set_authority(Some(authority))
.map_err(|_| HttpError::new("set_authority"))?;
outgoing
.set_path_with_query(Some(path))
.map_err(|_| HttpError::new("set_path"))?;
let body = outgoing.body().map_err(|_| HttpError::new("body"))?;
if !req.body.is_empty() {
let stream = body.write().map_err(|_| HttpError::new("body.write"))?;
for chunk in req.body.chunks(4096) {
stream
.blocking_write_and_flush(chunk)
.map_err(|e| HttpError::new(format!("write: {e:?}")))?;
}
drop(stream);
}
OutgoingBody::finish(body, None).map_err(|e| HttpError::new(format!("finish: {e:?}")))?;
let options = req.timeout.map(|d| {
let ns = d.as_nanos().min(u64::MAX as u128) as u64;
let opts = RequestOptions::new();
let _ = opts.set_connect_timeout(Some(ns));
let _ = opts.set_first_byte_timeout(Some(ns));
let _ = opts.set_between_bytes_timeout(Some(ns));
opts
});
let fut = outgoing_handler::handle(outgoing, options)
.map_err(|e| HttpError::new(format!("dispatch denied or failed: {e:?}")))?;
fut.subscribe().block();
let resp = fut
.get()
.ok_or_else(|| HttpError::new("no response"))?
.map_err(|_| HttpError::new("response already taken"))?
.map_err(|e| HttpError::new(format!("response error: {e:?}")))?;
let status = resp.status();
let headers = resp
.headers()
.entries()
.into_iter()
.map(|(k, v)| (k, String::from_utf8_lossy(&v).into_owned()))
.collect();
let incoming = resp.consume().map_err(|_| HttpError::new("consume"))?;
let stream = incoming.stream().map_err(|_| HttpError::new("stream"))?;
let mut buf = Vec::new();
loop {
match stream.blocking_read(8192) {
Ok(chunk) if chunk.is_empty() => continue,
Ok(chunk) => buf.extend_from_slice(&chunk),
Err(StreamError::Closed) => break,
Err(e) => return Err(HttpError::new(format!("read: {e:?}"))),
}
}
Ok(Response {
status,
headers,
body: buf,
})
}