This repository has been archived by the owner on Jun 7, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from wacker-dev/init
Initial version of wasi-http-client
- Loading branch information
Showing
7 changed files
with
266 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
name: CI | ||
on: | ||
push: | ||
branches: | ||
- 'main' | ||
- 'release-**' | ||
pull_request: | ||
jobs: | ||
ci: | ||
name: Lint and test | ||
runs-on: ubuntu-latest | ||
timeout-minutes: 30 | ||
steps: | ||
- uses: actions/checkout@v4 | ||
- name: Install Rust | ||
uses: dtolnay/rust-toolchain@1.78.0 | ||
with: | ||
components: clippy, rustfmt | ||
- name: cargo fmt | ||
run: cargo fmt --all -- --check | ||
- name: cargo clippy | ||
run: cargo clippy --all-targets --all-features -- -D warnings |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
name: release | ||
on: | ||
push: | ||
tags: | ||
- "v*" | ||
permissions: | ||
contents: write | ||
jobs: | ||
publish: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v4 | ||
with: | ||
fetch-depth: 1 | ||
- name: Install Rust | ||
uses: dtolnay/rust-toolchain@1.78.0 | ||
- name: cargo publish | ||
run: cargo publish | ||
env: | ||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
[package] | ||
name = "wasi-http-client" | ||
description = "HTTP client library for WASI" | ||
readme = "README.md" | ||
version = "0.1.0" | ||
edition = "2021" | ||
authors = ["Xinzhao Xu"] | ||
categories = ["wasm"] | ||
keywords = ["webassembly", "wasm", "wasi"] | ||
repository = "https://github.com/wacker-dev/wasi-http-client" | ||
license = "Apache-2.0" | ||
|
||
[dependencies] | ||
anyhow = "1.0.83" | ||
wasi = "0.13.0" | ||
url = "2.5.0" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
use crate::RequestBuilder; | ||
use wasi::http::types::Method; | ||
|
||
#[derive(Default)] | ||
pub struct Client {} | ||
|
||
impl Client { | ||
pub fn new() -> Self { | ||
Default::default() | ||
} | ||
|
||
pub fn get(&self, url: &str) -> RequestBuilder { | ||
self.request(Method::Get, url) | ||
} | ||
|
||
pub fn post(&self, url: &str) -> RequestBuilder { | ||
self.request(Method::Post, url) | ||
} | ||
|
||
pub fn put(&self, url: &str) -> RequestBuilder { | ||
self.request(Method::Put, url) | ||
} | ||
|
||
pub fn patch(&self, url: &str) -> RequestBuilder { | ||
self.request(Method::Patch, url) | ||
} | ||
|
||
pub fn delete(&self, url: &str) -> RequestBuilder { | ||
self.request(Method::Delete, url) | ||
} | ||
|
||
pub fn head(&self, url: &str) -> RequestBuilder { | ||
self.request(Method::Head, url) | ||
} | ||
|
||
pub fn request(&self, method: Method, url: &str) -> RequestBuilder { | ||
RequestBuilder::new(method, url) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
mod client; | ||
mod request; | ||
mod response; | ||
|
||
pub use self::client::Client; | ||
pub use self::request::RequestBuilder; | ||
pub use self::response::Response; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
use crate::Response; | ||
use anyhow::{anyhow, Result}; | ||
use std::time::Duration; | ||
use url::Url; | ||
use wasi::http::{ | ||
outgoing_handler, | ||
types::{FieldValue, Headers, Method, OutgoingBody, OutgoingRequest, RequestOptions, Scheme}, | ||
}; | ||
|
||
pub struct RequestBuilder { | ||
method: Method, | ||
url: String, | ||
headers: Headers, | ||
body: Vec<u8>, | ||
connect_timeout: Option<u64>, | ||
} | ||
|
||
impl RequestBuilder { | ||
pub fn new(method: Method, url: &str) -> Self { | ||
Self { | ||
method, | ||
url: url.to_string(), | ||
headers: Headers::new(), | ||
body: vec![], | ||
connect_timeout: None, | ||
} | ||
} | ||
|
||
pub fn header(self, key: &str, value: &str) -> Result<Self> { | ||
self.headers | ||
.set(&key.to_string(), &[FieldValue::from(value)])?; | ||
Ok(self) | ||
} | ||
|
||
pub fn body(mut self, body: &[u8]) -> Self { | ||
self.body = Vec::from(body); | ||
self | ||
} | ||
|
||
pub fn connect_timeout(mut self, timeout: Duration) -> Self { | ||
self.connect_timeout = Some(timeout.as_nanos() as u64); | ||
self | ||
} | ||
|
||
pub fn send(self) -> Result<Response> { | ||
let req = OutgoingRequest::new(self.headers); | ||
req.set_method(&self.method) | ||
.map_err(|()| anyhow!("failed to set method"))?; | ||
|
||
let url = Url::parse(self.url.as_str())?; | ||
let scheme = match url.scheme() { | ||
"http" => Scheme::Http, | ||
"https" => Scheme::Https, | ||
other => Scheme::Other(other.to_string()), | ||
}; | ||
req.set_scheme(Some(&scheme)) | ||
.map_err(|()| anyhow!("failed to set scheme"))?; | ||
|
||
req.set_authority(Some(url.authority())) | ||
.map_err(|()| anyhow!("failed to set authority"))?; | ||
|
||
let path = match url.query() { | ||
Some(query) => format!("{}?{query}", url.path()), | ||
None => url.path().to_string(), | ||
}; | ||
req.set_path_with_query(Some(&path)) | ||
.map_err(|()| anyhow!("failed to set path_with_query"))?; | ||
|
||
let outgoing_body = req | ||
.body() | ||
.map_err(|_| anyhow!("outgoing request write failed"))?; | ||
if !self.body.is_empty() { | ||
let request_body = outgoing_body | ||
.write() | ||
.map_err(|_| anyhow!("outgoing request write failed"))?; | ||
request_body.blocking_write_and_flush(&self.body)?; | ||
} | ||
OutgoingBody::finish(outgoing_body, None)?; | ||
|
||
let options = RequestOptions::new(); | ||
options | ||
.set_connect_timeout(self.connect_timeout) | ||
.map_err(|()| anyhow!("failed to set connect_timeout"))?; | ||
|
||
let future_response = outgoing_handler::handle(req, Some(options))?; | ||
let incoming_response = match future_response.get() { | ||
Some(result) => result.map_err(|()| anyhow!("response already taken"))?, | ||
None => { | ||
let pollable = future_response.subscribe(); | ||
pollable.block(); | ||
|
||
future_response | ||
.get() | ||
.expect("incoming response available") | ||
.map_err(|()| anyhow!("response already taken"))? | ||
} | ||
}?; | ||
drop(future_response); | ||
|
||
Response::new(incoming_response) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
use anyhow::{anyhow, Result}; | ||
use std::collections::HashMap; | ||
use wasi::http::types::{IncomingResponse, StatusCode}; | ||
use wasi::io::streams::StreamError; | ||
|
||
pub struct Response { | ||
status: StatusCode, | ||
headers: HashMap<String, String>, | ||
body: Vec<u8>, | ||
} | ||
|
||
impl Response { | ||
pub fn new(incoming_response: IncomingResponse) -> Result<Self> { | ||
let status = incoming_response.status(); | ||
|
||
let mut headers: HashMap<String, String> = HashMap::new(); | ||
let headers_handle = incoming_response.headers(); | ||
for (key, value) in headers_handle.entries() { | ||
headers.insert(key, String::from_utf8(value)?); | ||
} | ||
drop(headers_handle); | ||
|
||
let incoming_body = incoming_response | ||
.consume() | ||
.map_err(|()| anyhow!("incoming response has no body stream"))?; | ||
drop(incoming_response); | ||
|
||
let input_stream = incoming_body.stream().unwrap(); | ||
let mut body = vec![]; | ||
loop { | ||
let mut body_chunk = match input_stream.read(1024 * 1024) { | ||
Ok(c) => c, | ||
Err(StreamError::Closed) => break, | ||
Err(e) => Err(anyhow!("input_stream read failed: {e:?}"))?, | ||
}; | ||
|
||
if !body_chunk.is_empty() { | ||
body.append(&mut body_chunk); | ||
} | ||
} | ||
|
||
Ok(Self { | ||
status, | ||
headers, | ||
body, | ||
}) | ||
} | ||
|
||
pub fn status(&self) -> &StatusCode { | ||
&self.status | ||
} | ||
|
||
pub fn headers(&self) -> &HashMap<String, String> { | ||
&self.headers | ||
} | ||
|
||
pub fn body(&self) -> &Vec<u8> { | ||
&self.body | ||
} | ||
} |