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
37impl EspHttpClient {
38    /// Creates a new client from a network stack and TLS seed.
39    pub fn new(stack: Stack<'static>, tls_seed: u64) -> Self {
40        Self { stack, tls_seed }
41    }
42
43    /// Sends an HTTP request to `url`, following up to 5 redirects automatically.
44    ///
45    /// # Errors
46    /// Returns [`Error::Codec`] if the redirect limit is exceeded or a redirect
47    /// response is missing the `Location` header.
48    pub async fn request(
49        &self,
50        method: reqwless::request::Method,
51        url: &str,
52        body: Option<&[u8]>,
53    ) -> Result<http::Response, Error> {
54        const MAX_REDIRECTS: u8 = 5;
55        let mut current_url = alloc::string::String::from(url);
56
57        for _ in 0..=MAX_REDIRECTS {
58            let url_this_iter = current_url.clone();
59
60            let dns = DnsSocket::new(self.stack);
61            let tcp_state = alloc::boxed::Box::new(TcpClientState::<1, 4096, 4096>::new());
62            let tcp = TcpClient::new(self.stack, &*tcp_state);
63
64            let mut rx_buffer = alloc::vec![0u8; 16640].into_boxed_slice();
65            let mut tx_buffer = alloc::vec![0u8; 16640].into_boxed_slice();
66            let mut response_buffer = alloc::vec![0u8; 524288].into_boxed_slice();
67
68            let tls = TlsConfig::new(
69                self.tls_seed,
70                &mut rx_buffer,
71                &mut tx_buffer,
72                reqwless::client::TlsVerify::None,
73            );
74
75            let mut client = HttpClient::new_with_tls(&tcp, &dns, tls);
76            info!("HTTP {} {}", method as u8, url_this_iter.as_str());
77            let mut request = client
78                .request(method, url_this_iter.as_str())
79                .await?
80                .body(body);
81
82            let response = request.send(&mut response_buffer).await?;
83            let status = response.status.0;
84            info!("Response status: {}", status);
85
86            // check code and return, otherwise follow redirect
87            if !(300u16..400).contains(&status) {
88                if !(200u16..300).contains(&status) {
89                    warn!("Request failed with status {}", status);
90                }
91                let body_bytes = response.body().read_to_end().await?;
92                info!("Response complete: {} bytes", body_bytes.len());
93                return Ok(http::Response {
94                    status,
95                    bytes: body_bytes.to_vec(),
96                    content_length: Some(u64::try_from(body_bytes.len()).unwrap_or(0)),
97                });
98            }
99
100            // Follow redirect
101            let location = response
102                .headers()
103                .find(|(name, _)| name.eq_ignore_ascii_case("location"))
104                .and_then(|(_, value)| core::str::from_utf8(value).ok())
105                .map(alloc::string::String::from);
106
107            match location {
108                Some(new_url) => {
109                    info!("Following redirect {} -> {}", status, new_url.as_str());
110                    current_url = new_url;
111                }
112                None => {
113                    warn!("Redirect response missing Location header");
114                    return Err(Error::Codec);
115                }
116            }
117        }
118
119        warn!("Max redirects ({}) reached", MAX_REDIRECTS);
120        Err(Error::Codec)
121    }
122}