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

Commit

Permalink
Merge pull request #1 from wacker-dev/init
Browse files Browse the repository at this point in the history
Initial version of wasi-http-client
  • Loading branch information
iawia002 authored May 11, 2024
2 parents 929b2d1 + db2b734 commit 88582e4
Show file tree
Hide file tree
Showing 7 changed files with 266 additions and 0 deletions.
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
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
20 changes: 20 additions & 0 deletions .github/workflows/release.yml
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 }}
16 changes: 16 additions & 0 deletions Cargo.toml
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"
39 changes: 39 additions & 0 deletions src/client.rs
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)
}
}
7 changes: 7 additions & 0 deletions src/lib.rs
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;
102 changes: 102 additions & 0 deletions src/request.rs
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)
}
}
60 changes: 60 additions & 0 deletions src/response.rs
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
}
}

0 comments on commit 88582e4

Please sign in to comment.