wasi_http_client/
request.rs

1use crate::Response;
2use anyhow::{anyhow, Error, Result};
3use serde::Serialize;
4use std::time::Duration;
5use url::Url;
6use wasi::http::{
7    outgoing_handler,
8    types::{
9        FieldKey, FieldValue, Headers, Method, OutgoingBody, OutgoingRequest, RequestOptions,
10        Scheme,
11    },
12};
13
14pub struct RequestBuilder {
15    // all errors generated while building the request will be deferred and returned when `send` the request.
16    request: Result<Request>,
17}
18
19impl RequestBuilder {
20    pub(crate) fn new(method: Method, url: &str) -> Self {
21        Self {
22            request: Url::parse(url)
23                .map_or_else(|e| Err(Error::new(e)), |url| Ok(Request::new(method, url))),
24        }
25    }
26
27    /// Add a header to the Request.
28    ///
29    /// ```
30    /// # use anyhow::Result;
31    /// # use wasi_http_client::Client;
32    /// # fn run() -> Result<()> {
33    /// let resp = Client::new().get("https://httpbin.org/get")
34    ///     .header("Content-Type", "application/json")
35    ///     .send()?;
36    /// # Ok(())
37    /// # }
38    /// ```
39    pub fn header<K, V>(mut self, key: K, value: V) -> Self
40    where
41        K: Into<FieldKey>,
42        V: Into<FieldValue>,
43    {
44        let mut err = None;
45        if let Ok(ref mut req) = self.request {
46            if let Err(e) = req.headers.set(&key.into(), &[value.into()]) {
47                err = Some(e);
48            }
49        }
50        if let Some(e) = err {
51            self.request = Err(e.into());
52        }
53        self
54    }
55
56    /// Add a set of headers to the Request.
57    ///
58    /// Existing headers will be overwritten.
59    ///
60    /// ```
61    /// # use anyhow::Result;
62    /// # use wasi_http_client::Client;
63    /// # fn run() -> Result<()> {
64    /// let resp = Client::new().get("https://httpbin.org/get")
65    ///     .headers([("Content-Type", "application/json"), ("Accept", "*/*")])
66    ///     .send()?;
67    /// # Ok(())
68    /// # }
69    /// ```
70    pub fn headers<K, V, I>(mut self, headers: I) -> Self
71    where
72        K: Into<FieldKey>,
73        V: Into<FieldValue>,
74        I: IntoIterator<Item = (K, V)>,
75    {
76        let mut err = None;
77        if let Ok(ref mut req) = self.request {
78            let entries: Vec<(FieldKey, FieldValue)> = headers
79                .into_iter()
80                .map(|(k, v)| (k.into(), v.into()))
81                .collect();
82            match Headers::from_list(&entries) {
83                Ok(fields) => req.headers = fields,
84                Err(e) => err = Some(e),
85            }
86        }
87        if let Some(e) = err {
88            self.request = Err(e.into());
89        }
90        self
91    }
92
93    /// Modify the query string of the Request URL.
94    ///
95    /// ```
96    /// # use anyhow::Result;
97    /// # use wasi_http_client::Client;
98    /// # fn run() -> Result<()> {
99    /// let resp = Client::new().get("https://httpbin.org/get")
100    ///     .query(&[("a", "b"), ("c", "d")])
101    ///     .send()?;
102    /// # Ok(())
103    /// # }
104    /// ```
105    pub fn query<T: Serialize + ?Sized>(mut self, query: &T) -> Self {
106        let mut err = None;
107        if let Ok(ref mut req) = self.request {
108            let mut pairs = req.url.query_pairs_mut();
109            let serializer = serde_urlencoded::Serializer::new(&mut pairs);
110            if let Err(e) = query.serialize(serializer) {
111                err = Some(e);
112            }
113        }
114        if let Some(e) = err {
115            self.request = Err(e.into());
116        }
117        self
118    }
119
120    /// Set the request body.
121    ///
122    /// ```
123    /// # use anyhow::Result;
124    /// # use wasi_http_client::Client;
125    /// # fn run() -> Result<()> {
126    /// let resp = Client::new().post("https://httpbin.org/post")
127    ///     .body("hello".as_bytes())
128    ///     .send()?;
129    /// # Ok(())
130    /// # }
131    /// ```
132    pub fn body(mut self, body: &[u8]) -> Self {
133        if let Ok(ref mut req) = self.request {
134            req.body = Some(body.into());
135        }
136        self
137    }
138
139    /// Send a JSON body.
140    ///
141    /// # Optional
142    ///
143    /// This requires the `json` feature enabled.
144    ///
145    /// ```
146    /// # use anyhow::Result;
147    /// # use std::collections::HashMap;
148    /// # use wasi_http_client::Client;
149    /// # fn run() -> Result<()> {
150    /// let resp = Client::new().post("https://httpbin.org/post")
151    ///     .json(&HashMap::from([("data", "hello")]))
152    ///     .send()?;
153    /// # Ok(())
154    /// # }
155    /// ```
156    #[cfg(feature = "json")]
157    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
158    pub fn json<T: Serialize + ?Sized>(mut self, json: &T) -> Self {
159        let mut err = None;
160        if let Ok(ref mut req) = self.request {
161            if let Err(e) = req
162                .headers
163                .set(&"Content-Type".to_string(), &["application/json".into()])
164            {
165                err = Some(e.into());
166            }
167            match serde_json::to_vec(json) {
168                Ok(data) => req.body = Some(data),
169                Err(e) => err = Some(e.into()),
170            }
171        }
172        if let Some(e) = err {
173            self.request = Err(e);
174        }
175        self
176    }
177
178    /// Send a form body.
179    ///
180    /// ```
181    /// # use anyhow::Result;
182    /// # use wasi_http_client::Client;
183    /// # fn run() -> Result<()> {
184    /// let resp = Client::new().post("https://httpbin.org/post")
185    ///     .form(&[("a", "b"), ("c", "d")])
186    ///     .send()?;
187    /// # Ok(())
188    /// # }
189    /// ```
190    pub fn form<T: Serialize + ?Sized>(mut self, form: &T) -> Self {
191        let mut err = None;
192        if let Ok(ref mut req) = self.request {
193            if let Err(e) = req.headers.set(
194                &"Content-Type".to_string(),
195                &["application/x-www-form-urlencoded".into()],
196            ) {
197                err = Some(e.into());
198            }
199            match serde_urlencoded::to_string(form) {
200                Ok(data) => req.body = Some(data.into()),
201                Err(e) => err = Some(e.into()),
202            }
203        }
204        if let Some(e) = err {
205            self.request = Err(e);
206        }
207        self
208    }
209
210    /// Set the timeout for the initial connect to the HTTP Server.
211    ///
212    /// ```
213    /// # use anyhow::Result;
214    /// # use std::time::Duration;
215    /// # use wasi_http_client::Client;
216    /// # fn run() -> Result<()> {
217    /// let resp = Client::new().post("https://httpbin.org/post")
218    ///     .connect_timeout(Duration::from_secs(5))
219    ///     .send()?;
220    /// # Ok(())
221    /// # }
222    /// ```
223    pub fn connect_timeout(mut self, timeout: Duration) -> Self {
224        if let Ok(ref mut req) = self.request {
225            req.connect_timeout = Some(timeout.as_nanos() as u64);
226        }
227        self
228    }
229
230    /// Send the Request, returning a [`Response`].
231    pub fn send(self) -> Result<Response> {
232        match self.request {
233            Ok(req) => req.send(),
234            Err(e) => Err(e),
235        }
236    }
237}
238
239struct Request {
240    method: Method,
241    url: Url,
242    headers: Headers,
243    body: Option<Vec<u8>>,
244    connect_timeout: Option<u64>,
245}
246
247impl Request {
248    fn new(method: Method, url: Url) -> Self {
249        Self {
250            method,
251            url,
252            headers: Headers::new(),
253            body: None,
254            connect_timeout: None,
255        }
256    }
257
258    fn send(self) -> Result<Response> {
259        let req = OutgoingRequest::new(self.headers);
260        req.set_method(&self.method)
261            .map_err(|()| anyhow!("failed to set method"))?;
262
263        let scheme = match self.url.scheme() {
264            "http" => Scheme::Http,
265            "https" => Scheme::Https,
266            other => Scheme::Other(other.to_string()),
267        };
268        req.set_scheme(Some(&scheme))
269            .map_err(|()| anyhow!("failed to set scheme"))?;
270
271        req.set_authority(Some(self.url.authority()))
272            .map_err(|()| anyhow!("failed to set authority"))?;
273
274        let path = match self.url.query() {
275            Some(query) => format!("{}?{query}", self.url.path()),
276            None => self.url.path().to_string(),
277        };
278        req.set_path_with_query(Some(&path))
279            .map_err(|()| anyhow!("failed to set path_with_query"))?;
280
281        let outgoing_body = req
282            .body()
283            .map_err(|_| anyhow!("outgoing request write failed"))?;
284        if let Some(body) = self.body {
285            let request_body = outgoing_body
286                .write()
287                .map_err(|_| anyhow!("outgoing request write failed"))?;
288            request_body.blocking_write_and_flush(&body)?;
289        }
290        OutgoingBody::finish(outgoing_body, None)?;
291
292        let options = RequestOptions::new();
293        options
294            .set_connect_timeout(self.connect_timeout)
295            .map_err(|()| anyhow!("failed to set connect_timeout"))?;
296
297        let future_response = outgoing_handler::handle(req, Some(options))?;
298        let incoming_response = match future_response.get() {
299            Some(result) => result.map_err(|()| anyhow!("response already taken"))?,
300            None => {
301                let pollable = future_response.subscribe();
302                pollable.block();
303
304                future_response
305                    .get()
306                    .expect("incoming response available")
307                    .map_err(|()| anyhow!("response already taken"))?
308            }
309        }?;
310        drop(future_response);
311
312        Response::new(incoming_response)
313    }
314}