reqwless/
request.rs

1/// Low level API for encoding requests and decoding responses.
2use crate::headers::ContentType;
3use crate::Error;
4use core::fmt::Write as _;
5use embedded_io::Error as _;
6use embedded_io_async::Write;
7use heapless::String;
8
9/// A read only HTTP request type
10pub struct Request<'req, B>
11where
12    B: RequestBody,
13{
14    pub(crate) method: Method,
15    pub(crate) base_path: Option<&'req str>,
16    pub(crate) path: &'req str,
17    pub(crate) auth: Option<Auth<'req>>,
18    pub(crate) host: Option<&'req str>,
19    pub(crate) body: Option<B>,
20    pub(crate) content_type: Option<ContentType>,
21    pub(crate) extra_headers: Option<&'req [(&'req str, &'req str)]>,
22}
23
24impl Default for Request<'_, ()> {
25    fn default() -> Self {
26        Self {
27            method: Method::GET,
28            base_path: None,
29            path: "/",
30            auth: None,
31            host: None,
32            body: None,
33            content_type: None,
34            extra_headers: None,
35        }
36    }
37}
38
39/// A HTTP request builder.
40pub trait RequestBuilder<'req, B>
41where
42    B: RequestBody,
43{
44    type WithBody<T: RequestBody>: RequestBuilder<'req, T>;
45
46    /// Set optional headers on the request.
47    fn headers(self, headers: &'req [(&'req str, &'req str)]) -> Self;
48    /// Set the path of the HTTP request.
49    fn path(self, path: &'req str) -> Self;
50    /// Set the data to send in the HTTP request body.
51    fn body<T: RequestBody>(self, body: T) -> Self::WithBody<T>;
52    /// Set the host header.
53    fn host(self, host: &'req str) -> Self;
54    /// Set the content type header for the request.
55    fn content_type(self, content_type: ContentType) -> Self;
56    /// Set the basic authentication header for the request.
57    fn basic_auth(self, username: &'req str, password: &'req str) -> Self;
58    /// Return an immutable request.
59    fn build(self) -> Request<'req, B>;
60}
61
62/// Request authentication scheme.
63pub enum Auth<'a> {
64    Basic { username: &'a str, password: &'a str },
65}
66
67impl<'req> Request<'req, ()> {
68    /// Create a new http request.
69    #[allow(clippy::new_ret_no_self)]
70    pub fn new(method: Method, path: &'req str) -> DefaultRequestBuilder<'req, ()> {
71        DefaultRequestBuilder(Request {
72            method,
73            path,
74            ..Default::default()
75        })
76    }
77
78    /// Create a new GET http request.
79    pub fn get(path: &'req str) -> DefaultRequestBuilder<'req, ()> {
80        Self::new(Method::GET, path)
81    }
82
83    /// Create a new POST http request.
84    pub fn post(path: &'req str) -> DefaultRequestBuilder<'req, ()> {
85        Self::new(Method::POST, path)
86    }
87
88    /// Create a new PUT http request.
89    pub fn put(path: &'req str) -> DefaultRequestBuilder<'req, ()> {
90        Self::new(Method::PUT, path)
91    }
92
93    /// Create a new DELETE http request.
94    pub fn delete(path: &'req str) -> DefaultRequestBuilder<'req, ()> {
95        Self::new(Method::DELETE, path)
96    }
97
98    /// Create a new HEAD http request.
99    pub fn head(path: &'req str) -> DefaultRequestBuilder<'req, ()> {
100        Self::new(Method::HEAD, path)
101    }
102}
103
104impl<'req, B> Request<'req, B>
105where
106    B: RequestBody,
107{
108    /// Write request header to the I/O stream
109    pub async fn write_header<C>(&self, c: &mut C) -> Result<(), Error>
110    where
111        C: Write,
112    {
113        write_str(c, self.method.as_str()).await?;
114        write_str(c, " ").await?;
115        if let Some(base_path) = self.base_path {
116            write_str(c, base_path.trim_end_matches('/')).await?;
117            if !self.path.starts_with('/') {
118                write_str(c, "/").await?;
119            }
120        }
121        write_str(c, self.path).await?;
122        write_str(c, " HTTP/1.1\r\n").await?;
123
124        if let Some(auth) = &self.auth {
125            match auth {
126                Auth::Basic { username, password } => {
127                    use base64::engine::{general_purpose, Engine as _};
128
129                    let mut combined: String<128> = String::new();
130                    write!(combined, "{}:{}", username, password).map_err(|_| Error::Codec)?;
131                    let mut authz = [0; 256];
132                    let authz_len = general_purpose::STANDARD
133                        .encode_slice(combined.as_bytes(), &mut authz)
134                        .map_err(|_| Error::Codec)?;
135                    write_str(c, "Authorization: Basic ").await?;
136                    write_str(c, unsafe { core::str::from_utf8_unchecked(&authz[..authz_len]) }).await?;
137                    write_str(c, "\r\n").await?;
138                }
139            }
140        }
141        if let Some(host) = &self.host {
142            write_header(c, "Host", host).await?;
143        }
144        if let Some(content_type) = &self.content_type {
145            write_header(c, "Content-Type", content_type.as_str()).await?;
146        }
147        if let Some(body) = self.body.as_ref() {
148            if let Some(len) = body.len() {
149                let mut s: String<32> = String::new();
150                write!(s, "{}", len).map_err(|_| Error::Codec)?;
151                write_header(c, "Content-Length", s.as_str()).await?;
152            } else {
153                write_header(c, "Transfer-Encoding", "chunked").await?;
154            }
155        }
156        if let Some(extra_headers) = self.extra_headers {
157            for (header, value) in extra_headers.iter() {
158                write_header(c, header, value).await?;
159            }
160        }
161        write_str(c, "\r\n").await?;
162        trace!("Header written");
163        Ok(())
164    }
165}
166
167pub struct DefaultRequestBuilder<'req, B>(Request<'req, B>)
168where
169    B: RequestBody;
170
171impl<'req, B> RequestBuilder<'req, B> for DefaultRequestBuilder<'req, B>
172where
173    B: RequestBody,
174{
175    type WithBody<T: RequestBody> = DefaultRequestBuilder<'req, T>;
176
177    fn headers(mut self, headers: &'req [(&'req str, &'req str)]) -> Self {
178        self.0.extra_headers.replace(headers);
179        self
180    }
181
182    fn path(mut self, path: &'req str) -> Self {
183        self.0.path = path;
184        self
185    }
186
187    fn body<T: RequestBody>(self, body: T) -> Self::WithBody<T> {
188        DefaultRequestBuilder(Request {
189            method: self.0.method,
190            base_path: self.0.base_path,
191            path: self.0.path,
192            auth: self.0.auth,
193            host: self.0.host,
194            body: Some(body),
195            content_type: self.0.content_type,
196            extra_headers: self.0.extra_headers,
197        })
198    }
199
200    fn host(mut self, host: &'req str) -> Self {
201        self.0.host.replace(host);
202        self
203    }
204
205    fn content_type(mut self, content_type: ContentType) -> Self {
206        self.0.content_type.replace(content_type);
207        self
208    }
209
210    fn basic_auth(mut self, username: &'req str, password: &'req str) -> Self {
211        self.0.auth.replace(Auth::Basic { username, password });
212        self
213    }
214
215    fn build(self) -> Request<'req, B> {
216        self.0
217    }
218}
219
220#[derive(Clone, Copy, Debug, PartialEq)]
221#[cfg_attr(feature = "defmt", derive(defmt::Format))]
222/// HTTP request methods
223pub enum Method {
224    /// GET
225    GET,
226    /// PUT
227    PUT,
228    /// POST
229    POST,
230    /// DELETE
231    DELETE,
232    /// HEAD
233    HEAD,
234}
235
236impl Method {
237    /// str representation of method
238    pub fn as_str(&self) -> &str {
239        match self {
240            Method::POST => "POST",
241            Method::PUT => "PUT",
242            Method::GET => "GET",
243            Method::DELETE => "DELETE",
244            Method::HEAD => "HEAD",
245        }
246    }
247}
248
249async fn write_str<C: Write>(c: &mut C, data: &str) -> Result<(), Error> {
250    c.write_all(data.as_bytes()).await.map_err(|e| e.kind())?;
251    Ok(())
252}
253
254async fn write_header<C: Write>(c: &mut C, key: &str, value: &str) -> Result<(), Error> {
255    write_str(c, key).await?;
256    write_str(c, ": ").await?;
257    write_str(c, value).await?;
258    write_str(c, "\r\n").await?;
259    Ok(())
260}
261
262/// The request body
263#[allow(clippy::len_without_is_empty)]
264pub trait RequestBody {
265    /// Get the length of the body if known
266    ///
267    /// If the length is known, then it will be written in the `Content-Length` header,
268    /// chunked encoding will be used otherwise.
269    fn len(&self) -> Option<usize> {
270        None
271    }
272
273    /// Write the body to the provided writer
274    async fn write<W: Write>(&self, writer: &mut W) -> Result<(), W::Error>;
275}
276
277impl RequestBody for () {
278    fn len(&self) -> Option<usize> {
279        None
280    }
281
282    async fn write<W: Write>(&self, _writer: &mut W) -> Result<(), W::Error> {
283        Ok(())
284    }
285}
286
287impl RequestBody for &[u8] {
288    fn len(&self) -> Option<usize> {
289        Some(<[u8]>::len(self))
290    }
291
292    async fn write<W: Write>(&self, writer: &mut W) -> Result<(), W::Error> {
293        writer.write_all(self).await
294    }
295}
296
297impl<T> RequestBody for Option<T>
298where
299    T: RequestBody,
300{
301    fn len(&self) -> Option<usize> {
302        self.as_ref().map(|inner| inner.len()).unwrap_or_default()
303    }
304
305    async fn write<W: Write>(&self, writer: &mut W) -> Result<(), W::Error> {
306        if let Some(inner) = self.as_ref() {
307            inner.write(writer).await
308        } else {
309            Ok(())
310        }
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317
318    #[tokio::test]
319    async fn basic_auth() {
320        let mut buffer: Vec<u8> = Vec::new();
321        Request::new(Method::GET, "/")
322            .basic_auth("username", "password")
323            .build()
324            .write_header(&mut buffer)
325            .await
326            .unwrap();
327
328        assert_eq!(
329            b"GET / HTTP/1.1\r\nAuthorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=\r\n\r\n",
330            buffer.as_slice()
331        );
332    }
333
334    #[tokio::test]
335    async fn with_empty_body() {
336        let mut buffer = Vec::new();
337        Request::new(Method::POST, "/")
338            .body([].as_slice())
339            .build()
340            .write_header(&mut buffer)
341            .await
342            .unwrap();
343
344        assert_eq!(b"POST / HTTP/1.1\r\nContent-Length: 0\r\n\r\n", buffer.as_slice());
345    }
346
347    #[tokio::test]
348    async fn with_known_body_adds_content_length_header() {
349        let mut buffer = Vec::new();
350        Request::new(Method::POST, "/")
351            .body(b"BODY".as_slice())
352            .build()
353            .write_header(&mut buffer)
354            .await
355            .unwrap();
356
357        assert_eq!(b"POST / HTTP/1.1\r\nContent-Length: 4\r\n\r\n", buffer.as_slice());
358    }
359
360    struct ChunkedBody;
361
362    impl RequestBody for ChunkedBody {
363        fn len(&self) -> Option<usize> {
364            None // Unknown length: triggers chunked body
365        }
366
367        async fn write<W: Write>(&self, _writer: &mut W) -> Result<(), W::Error> {
368            unreachable!()
369        }
370    }
371
372    #[tokio::test]
373    async fn with_unknown_body_adds_transfer_encoding_header() {
374        let mut buffer = Vec::new();
375
376        Request::new(Method::POST, "/")
377            .body(ChunkedBody)
378            .build()
379            .write_header(&mut buffer)
380            .await
381            .unwrap();
382
383        assert_eq!(
384            b"POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n",
385            buffer.as_slice()
386        );
387    }
388}