Skip to content
This repository was archived by the owner on Jun 7, 2024. It is now read-only.

Commit 88582e4

Browse files
authored
Merge pull request #1 from wacker-dev/init
Initial version of wasi-http-client
2 parents 929b2d1 + db2b734 commit 88582e4

File tree

7 files changed

+266
-0
lines changed

7 files changed

+266
-0
lines changed

.github/workflows/ci.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: CI
2+
on:
3+
push:
4+
branches:
5+
- 'main'
6+
- 'release-**'
7+
pull_request:
8+
jobs:
9+
ci:
10+
name: Lint and test
11+
runs-on: ubuntu-latest
12+
timeout-minutes: 30
13+
steps:
14+
- uses: actions/checkout@v4
15+
- name: Install Rust
16+
uses: dtolnay/rust-toolchain@1.78.0
17+
with:
18+
components: clippy, rustfmt
19+
- name: cargo fmt
20+
run: cargo fmt --all -- --check
21+
- name: cargo clippy
22+
run: cargo clippy --all-targets --all-features -- -D warnings

.github/workflows/release.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: release
2+
on:
3+
push:
4+
tags:
5+
- "v*"
6+
permissions:
7+
contents: write
8+
jobs:
9+
publish:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
with:
14+
fetch-depth: 1
15+
- name: Install Rust
16+
uses: dtolnay/rust-toolchain@1.78.0
17+
- name: cargo publish
18+
run: cargo publish
19+
env:
20+
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}

Cargo.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[package]
2+
name = "wasi-http-client"
3+
description = "HTTP client library for WASI"
4+
readme = "README.md"
5+
version = "0.1.0"
6+
edition = "2021"
7+
authors = ["Xinzhao Xu"]
8+
categories = ["wasm"]
9+
keywords = ["webassembly", "wasm", "wasi"]
10+
repository = "https://github.com/wacker-dev/wasi-http-client"
11+
license = "Apache-2.0"
12+
13+
[dependencies]
14+
anyhow = "1.0.83"
15+
wasi = "0.13.0"
16+
url = "2.5.0"

src/client.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
use crate::RequestBuilder;
2+
use wasi::http::types::Method;
3+
4+
#[derive(Default)]
5+
pub struct Client {}
6+
7+
impl Client {
8+
pub fn new() -> Self {
9+
Default::default()
10+
}
11+
12+
pub fn get(&self, url: &str) -> RequestBuilder {
13+
self.request(Method::Get, url)
14+
}
15+
16+
pub fn post(&self, url: &str) -> RequestBuilder {
17+
self.request(Method::Post, url)
18+
}
19+
20+
pub fn put(&self, url: &str) -> RequestBuilder {
21+
self.request(Method::Put, url)
22+
}
23+
24+
pub fn patch(&self, url: &str) -> RequestBuilder {
25+
self.request(Method::Patch, url)
26+
}
27+
28+
pub fn delete(&self, url: &str) -> RequestBuilder {
29+
self.request(Method::Delete, url)
30+
}
31+
32+
pub fn head(&self, url: &str) -> RequestBuilder {
33+
self.request(Method::Head, url)
34+
}
35+
36+
pub fn request(&self, method: Method, url: &str) -> RequestBuilder {
37+
RequestBuilder::new(method, url)
38+
}
39+
}

src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
mod client;
2+
mod request;
3+
mod response;
4+
5+
pub use self::client::Client;
6+
pub use self::request::RequestBuilder;
7+
pub use self::response::Response;

src/request.rs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
use crate::Response;
2+
use anyhow::{anyhow, Result};
3+
use std::time::Duration;
4+
use url::Url;
5+
use wasi::http::{
6+
outgoing_handler,
7+
types::{FieldValue, Headers, Method, OutgoingBody, OutgoingRequest, RequestOptions, Scheme},
8+
};
9+
10+
pub struct RequestBuilder {
11+
method: Method,
12+
url: String,
13+
headers: Headers,
14+
body: Vec<u8>,
15+
connect_timeout: Option<u64>,
16+
}
17+
18+
impl RequestBuilder {
19+
pub fn new(method: Method, url: &str) -> Self {
20+
Self {
21+
method,
22+
url: url.to_string(),
23+
headers: Headers::new(),
24+
body: vec![],
25+
connect_timeout: None,
26+
}
27+
}
28+
29+
pub fn header(self, key: &str, value: &str) -> Result<Self> {
30+
self.headers
31+
.set(&key.to_string(), &[FieldValue::from(value)])?;
32+
Ok(self)
33+
}
34+
35+
pub fn body(mut self, body: &[u8]) -> Self {
36+
self.body = Vec::from(body);
37+
self
38+
}
39+
40+
pub fn connect_timeout(mut self, timeout: Duration) -> Self {
41+
self.connect_timeout = Some(timeout.as_nanos() as u64);
42+
self
43+
}
44+
45+
pub fn send(self) -> Result<Response> {
46+
let req = OutgoingRequest::new(self.headers);
47+
req.set_method(&self.method)
48+
.map_err(|()| anyhow!("failed to set method"))?;
49+
50+
let url = Url::parse(self.url.as_str())?;
51+
let scheme = match url.scheme() {
52+
"http" => Scheme::Http,
53+
"https" => Scheme::Https,
54+
other => Scheme::Other(other.to_string()),
55+
};
56+
req.set_scheme(Some(&scheme))
57+
.map_err(|()| anyhow!("failed to set scheme"))?;
58+
59+
req.set_authority(Some(url.authority()))
60+
.map_err(|()| anyhow!("failed to set authority"))?;
61+
62+
let path = match url.query() {
63+
Some(query) => format!("{}?{query}", url.path()),
64+
None => url.path().to_string(),
65+
};
66+
req.set_path_with_query(Some(&path))
67+
.map_err(|()| anyhow!("failed to set path_with_query"))?;
68+
69+
let outgoing_body = req
70+
.body()
71+
.map_err(|_| anyhow!("outgoing request write failed"))?;
72+
if !self.body.is_empty() {
73+
let request_body = outgoing_body
74+
.write()
75+
.map_err(|_| anyhow!("outgoing request write failed"))?;
76+
request_body.blocking_write_and_flush(&self.body)?;
77+
}
78+
OutgoingBody::finish(outgoing_body, None)?;
79+
80+
let options = RequestOptions::new();
81+
options
82+
.set_connect_timeout(self.connect_timeout)
83+
.map_err(|()| anyhow!("failed to set connect_timeout"))?;
84+
85+
let future_response = outgoing_handler::handle(req, Some(options))?;
86+
let incoming_response = match future_response.get() {
87+
Some(result) => result.map_err(|()| anyhow!("response already taken"))?,
88+
None => {
89+
let pollable = future_response.subscribe();
90+
pollable.block();
91+
92+
future_response
93+
.get()
94+
.expect("incoming response available")
95+
.map_err(|()| anyhow!("response already taken"))?
96+
}
97+
}?;
98+
drop(future_response);
99+
100+
Response::new(incoming_response)
101+
}
102+
}

src/response.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
use anyhow::{anyhow, Result};
2+
use std::collections::HashMap;
3+
use wasi::http::types::{IncomingResponse, StatusCode};
4+
use wasi::io::streams::StreamError;
5+
6+
pub struct Response {
7+
status: StatusCode,
8+
headers: HashMap<String, String>,
9+
body: Vec<u8>,
10+
}
11+
12+
impl Response {
13+
pub fn new(incoming_response: IncomingResponse) -> Result<Self> {
14+
let status = incoming_response.status();
15+
16+
let mut headers: HashMap<String, String> = HashMap::new();
17+
let headers_handle = incoming_response.headers();
18+
for (key, value) in headers_handle.entries() {
19+
headers.insert(key, String::from_utf8(value)?);
20+
}
21+
drop(headers_handle);
22+
23+
let incoming_body = incoming_response
24+
.consume()
25+
.map_err(|()| anyhow!("incoming response has no body stream"))?;
26+
drop(incoming_response);
27+
28+
let input_stream = incoming_body.stream().unwrap();
29+
let mut body = vec![];
30+
loop {
31+
let mut body_chunk = match input_stream.read(1024 * 1024) {
32+
Ok(c) => c,
33+
Err(StreamError::Closed) => break,
34+
Err(e) => Err(anyhow!("input_stream read failed: {e:?}"))?,
35+
};
36+
37+
if !body_chunk.is_empty() {
38+
body.append(&mut body_chunk);
39+
}
40+
}
41+
42+
Ok(Self {
43+
status,
44+
headers,
45+
body,
46+
})
47+
}
48+
49+
pub fn status(&self) -> &StatusCode {
50+
&self.status
51+
}
52+
53+
pub fn headers(&self) -> &HashMap<String, String> {
54+
&self.headers
55+
}
56+
57+
pub fn body(&self) -> &Vec<u8> {
58+
&self.body
59+
}
60+
}

0 commit comments

Comments
 (0)