Skip to main content

embedded_wg_display/http_client/
mod.rs

1//! Async HTTPS client using `reqwless` and `embassy-net`.
2//!
3//! ## Buffer sizes
4//! - TLS RX/TX buffers: 16,640 bytes each (allocated in PSRAM)
5//! - Response buffer: 524,288 bytes ≈512 KB (PSRAM) — determines the maximum
6//!   downloadable file size, including widget WASM binaries. This also is the max size of a storeable widget with [`Storage`](crate::storage::Storage).
7//!
8//! ## Security
9//! TLS certificate verification is **disabled** (`TlsVerify::None`).
10//!
11//! ## Usage
12//!
13//! Call via
14//! [`http_sync::http_request_async`](crate::runtime::http_sync::http_request_async)
15//! from async contexts, or
16//! [`http_sync::http_request_sync`](crate::runtime::http_sync::http_request_sync)
17//! from sync contexts (e.g. widget WASM host functions). Do not construct
18//! [`EspHttpClient`] directly.
19use crate::runtime::widget::widget::http;
20use defmt::{info, warn};
21use embassy_net::{
22    Stack,
23    dns::DnsSocket,
24    tcp::client::{TcpClient, TcpClientState},
25};
26use reqwless::{
27    Error,
28    client::{HttpClient, TlsConfig},
29    request::RequestBuilder,
30};
31
32pub struct EspHttpClient {
33    stack: Stack<'static>,
34    tls_seed: u64,
35}
36
37const MAX_RESPONSE_SIZE: usize = 1024 * 1024; // 1 MiB
38const MAX_REDIRECTS: u8 = 5;
39
40impl EspHttpClient {
41    /// Creates a new client from a network stack and TLS seed.
42    pub fn new(stack: Stack<'static>, tls_seed: u64) -> Self {
43        Self { stack, tls_seed }
44    }
45
46    /// Sends an HTTP request to `url` with provided [`reqwless::request::Method`], following up to 5 redirects automatically (defined by [`MAX_REDIRECTS`]).
47    /// Max response body is defined by [`MAX_RESPONSE_SIZE`] (currently 1 MiB).
48    ///
49    /// # Errors
50    /// Returns [`Error::Codec`] if the redirect limit is exceeded or a redirect
51    /// response is missing the `Location` header.
52    pub async fn request(
53        &self,
54        method: reqwless::request::Method,
55        url: &str,
56        body: Option<&[u8]>,
57    ) -> Result<http::Response, Error> {
58        let mut current_url = alloc::string::String::from(url);
59
60        for _ in 0..=MAX_REDIRECTS {
61            let url_this_iter = current_url.clone();
62
63            let dns = DnsSocket::new(self.stack);
64            let tcp_state = alloc::boxed::Box::new(TcpClientState::<1, 4096, 4096>::new());
65            let tcp = TcpClient::new(self.stack, &*tcp_state);
66
67            let mut rx_buffer = alloc::vec![0u8; 16640].into_boxed_slice();
68            let mut tx_buffer = alloc::vec![0u8; 16640].into_boxed_slice();
69            let mut response_buffer = alloc::vec![0u8; MAX_RESPONSE_SIZE].into_boxed_slice();
70
71            let tls = TlsConfig::new(
72                self.tls_seed,
73                &mut rx_buffer,
74                &mut tx_buffer,
75                reqwless::client::TlsVerify::None,
76            );
77
78            let mut client = HttpClient::new_with_tls(&tcp, &dns, tls);
79            info!("HTTP {} {}", method as u8, url_this_iter.as_str());
80            let mut request = client
81                .request(method, url_this_iter.as_str())
82                .await?
83                .body(body);
84
85            let response = request.send(&mut response_buffer).await?;
86            let status = response.status.0;
87            info!("Response status: {}", status);
88
89            // check code and return, otherwise follow redirect
90            if !(300u16..400).contains(&status) {
91                if !(200u16..300).contains(&status) {
92                    warn!("Request failed with status {}", status);
93                }
94                let body_bytes = response.body().read_to_end().await?;
95                info!("Response complete: {} bytes", body_bytes.len());
96                return Ok(http::Response {
97                    status,
98                    bytes: body_bytes.to_vec(),
99                    content_length: Some(u64::try_from(body_bytes.len()).unwrap_or(0)),
100                });
101            }
102
103            // Follow redirect
104            let location = response
105                .headers()
106                .find(|(name, _)| name.eq_ignore_ascii_case("location"))
107                .and_then(|(_, value)| core::str::from_utf8(value).ok())
108                .map(alloc::string::String::from);
109
110            match location {
111                Some(new_url) => {
112                    info!("Following redirect {} -> {}", status, new_url.as_str());
113                    current_url = new_url;
114                }
115                None => {
116                    warn!("Redirect response missing Location header");
117                    return Err(Error::Codec);
118                }
119            }
120        }
121
122        warn!("Max redirects ({}) reached", MAX_REDIRECTS);
123        Err(Error::Codec)
124    }
125}