Skip to main content

esp_idf_svc/http/
client.rs

1//! HTTP client
2//!
3//! This provides a set of APIs for making HTTP(S) requests.
4//!
5//! You can find a usage example at
6//! [`examples/http_request.rs`](https://github.com/esp-rs/esp-idf-svc/blob/master/examples/http_request.rs).
7
8use core::cell::UnsafeCell;
9use core::fmt::Write as _;
10
11extern crate alloc;
12use alloc::boxed::Box;
13use alloc::collections::BTreeMap;
14use alloc::string::String;
15use alloc::string::ToString;
16
17use ::log::*;
18
19use embedded_svc::io::{ErrorType, Read, Write};
20
21use crate::sys::*;
22
23use uncased::{Uncased, UncasedStr};
24
25use crate::handle::RawHandle;
26use crate::io::EspIOError;
27use crate::private::common::Newtype;
28use crate::private::cstr::*;
29use crate::tls::X509;
30
31pub use embedded_svc::http::client::{Connection, Request, Response};
32
33pub use super::*;
34
35impl From<Method> for Newtype<(esp_http_client_method_t, ())> {
36    fn from(method: Method) -> Self {
37        Self((
38            match method {
39                Method::Get => esp_http_client_method_t_HTTP_METHOD_GET,
40                Method::Post => esp_http_client_method_t_HTTP_METHOD_POST,
41                Method::Delete => esp_http_client_method_t_HTTP_METHOD_DELETE,
42                Method::Head => esp_http_client_method_t_HTTP_METHOD_HEAD,
43                Method::Put => esp_http_client_method_t_HTTP_METHOD_PUT,
44                Method::Options => esp_http_client_method_t_HTTP_METHOD_OPTIONS,
45                Method::Copy => esp_http_client_method_t_HTTP_METHOD_COPY,
46                Method::Lock => esp_http_client_method_t_HTTP_METHOD_LOCK,
47                Method::MkCol => esp_http_client_method_t_HTTP_METHOD_MKCOL,
48                Method::Move => esp_http_client_method_t_HTTP_METHOD_MOVE,
49                Method::Propfind => esp_http_client_method_t_HTTP_METHOD_PROPFIND,
50                Method::Proppatch => esp_http_client_method_t_HTTP_METHOD_PROPPATCH,
51                Method::Unlock => esp_http_client_method_t_HTTP_METHOD_UNLOCK,
52                Method::Notify => esp_http_client_method_t_HTTP_METHOD_NOTIFY,
53                Method::Subscribe => esp_http_client_method_t_HTTP_METHOD_SUBSCRIBE,
54                Method::Unsubscribe => esp_http_client_method_t_HTTP_METHOD_UNSUBSCRIBE,
55                Method::Patch => esp_http_client_method_t_HTTP_METHOD_PATCH,
56                method => panic!("Method {method:?} is not supported"),
57            },
58            (),
59        ))
60    }
61}
62
63#[derive(Default, Copy, Clone, Debug, Eq, PartialEq, Hash)]
64pub enum FollowRedirectsPolicy {
65    FollowNone,
66    #[default]
67    FollowGetHead,
68    FollowAll,
69}
70
71#[derive(Copy, Clone, Debug, Default)]
72pub struct Configuration {
73    pub buffer_size: Option<usize>,
74    pub buffer_size_tx: Option<usize>,
75    pub timeout: Option<core::time::Duration>,
76    pub follow_redirects_policy: FollowRedirectsPolicy,
77    pub client_certificate: Option<X509<'static>>,
78    pub server_certificate: Option<X509<'static>>,
79    pub private_key: Option<X509<'static>>,
80    pub use_global_ca_store: bool,
81    pub crt_bundle_attach: Option<unsafe extern "C" fn(conf: *mut core::ffi::c_void) -> esp_err_t>,
82    pub raw_request_body: bool,
83    pub keep_alive_enable: bool,
84    pub keep_alive_idle: Option<usize>,
85    pub keep_alive_interval: Option<usize>,
86    pub keep_alive_count: Option<usize>,
87}
88
89#[derive(Debug, Copy, Clone, Eq, PartialEq)]
90enum State {
91    New,
92    Request,
93    Response,
94}
95
96#[allow(clippy::type_complexity)]
97pub struct EspHttpConnection {
98    raw_client: esp_http_client_handle_t,
99    follow_redirects_policy: FollowRedirectsPolicy,
100    raw_request_body: bool,
101    event_handler: Box<Option<Box<dyn Fn(&esp_http_client_event_t) -> esp_err_t>>>,
102    state: State,
103    request_content_len: i64,
104    follow_redirects: bool,
105    headers: BTreeMap<Uncased<'static>, String>,
106    content_len_header: UnsafeCell<Option<Option<String>>>,
107}
108
109impl EspHttpConnection {
110    pub fn new(configuration: &Configuration) -> Result<Self, EspError> {
111        let event_handler = Box::new(None);
112
113        #[allow(clippy::manual_c_str_literals)]
114        let mut native_config = esp_http_client_config_t {
115            // The ESP-IDF HTTP client is really picky on being initialized with a valid URL
116            // So we set something here, which will be changed later anyway, in the request() method
117            url: b"http://127.0.0.1\0".as_ptr() as *const _,
118            event_handler: Some(Self::on_events),
119            user_data: &*event_handler as *const _ as *mut core::ffi::c_void,
120
121            use_global_ca_store: configuration.use_global_ca_store,
122            crt_bundle_attach: configuration.crt_bundle_attach,
123            keep_alive_enable: configuration.keep_alive_enable,
124
125            ..Default::default()
126        };
127
128        if let Some(buffer_size) = configuration.buffer_size {
129            native_config.buffer_size = buffer_size as _;
130        };
131
132        if let Some(buffer_size_tx) = configuration.buffer_size_tx {
133            native_config.buffer_size_tx = buffer_size_tx as _;
134        }
135
136        if let Some(timeout) = configuration.timeout {
137            native_config.timeout_ms = timeout.as_millis() as _;
138        }
139
140        if let Some(cert) = configuration.server_certificate {
141            #[cfg(esp_idf_version_at_least_5_5_0)]
142            {
143                native_config.__bindgen_anon_1.cert_pem = cert.as_esp_idf_raw_ptr() as _;
144            }
145            #[cfg(not(esp_idf_version_at_least_5_5_0))]
146            {
147                native_config.cert_pem = cert.as_esp_idf_raw_ptr() as _;
148            }
149            native_config.cert_len = cert.as_esp_idf_raw_len();
150        }
151
152        if let (Some(cert), Some(private_key)) =
153            (configuration.client_certificate, configuration.private_key)
154        {
155            #[cfg(esp_idf_version_at_least_5_5_0)]
156            {
157                native_config.__bindgen_anon_2.client_cert_pem = cert.as_esp_idf_raw_ptr() as _;
158            }
159
160            #[cfg(not(esp_idf_version_at_least_5_5_0))]
161            {
162                native_config.client_cert_pem = cert.as_esp_idf_raw_ptr() as _;
163            }
164
165            native_config.client_cert_len = cert.as_esp_idf_raw_len();
166
167            native_config.client_key_pem = private_key.as_esp_idf_raw_ptr() as _;
168            native_config.client_key_len = private_key.as_esp_idf_raw_len();
169        }
170
171        if configuration.keep_alive_enable {
172            if let Some(keep_alive_idle) = configuration.keep_alive_idle {
173                native_config.keep_alive_idle = keep_alive_idle as _;
174            }
175            if let Some(keep_alive_interval) = configuration.keep_alive_interval {
176                native_config.keep_alive_interval = keep_alive_interval as _;
177            }
178            if let Some(keep_alive_count) = configuration.keep_alive_count {
179                native_config.keep_alive_count = keep_alive_count as _;
180            }
181        }
182
183        let raw_client = unsafe { esp_http_client_init(&native_config) };
184        if raw_client.is_null() {
185            Err(EspError::from_infallible::<ESP_FAIL>())
186        } else {
187            Ok(Self {
188                raw_client,
189                follow_redirects_policy: configuration.follow_redirects_policy,
190                raw_request_body: configuration.raw_request_body,
191                event_handler,
192                state: State::New,
193                request_content_len: -1,
194                follow_redirects: false,
195                headers: BTreeMap::new(),
196                content_len_header: UnsafeCell::new(None),
197            })
198        }
199    }
200
201    pub fn status(&self) -> u16 {
202        self.assert_response();
203        unsafe { esp_http_client_get_status_code(self.raw_client) as _ }
204    }
205
206    pub fn status_message(&self) -> Option<&str> {
207        self.assert_response();
208        None
209    }
210
211    pub fn header(&self, name: &str) -> Option<&str> {
212        self.assert_response();
213
214        if name.eq_ignore_ascii_case("Content-Length") {
215            if let Some(content_len_opt) =
216                unsafe { self.content_len_header.get().as_mut().unwrap() }.as_ref()
217            {
218                content_len_opt.as_ref().map(|s| s.as_str())
219            } else {
220                let content_len = unsafe { esp_http_client_get_content_length(self.raw_client) };
221                *unsafe { self.content_len_header.get().as_mut().unwrap() } = if content_len >= 0 {
222                    Some(Some(content_len.to_string()))
223                } else {
224                    None
225                };
226
227                unsafe { self.content_len_header.get().as_mut().unwrap() }
228                    .as_ref()
229                    .and_then(|s| s.as_ref().map(|s| s.as_ref()))
230            }
231        } else {
232            self.headers.get(UncasedStr::new(name)).map(|s| s.as_str())
233        }
234    }
235
236    pub fn initiate_request<'a>(
237        &'a mut self,
238        method: Method,
239        uri: &'a str,
240        headers: &'a [(&'a str, &'a str)],
241    ) -> Result<(), EspError> {
242        // If response data from the previous request remains, subsequent requests may fail
243        if self.is_response_initiated() {
244            self.flush_response()?;
245        }
246
247        self.assert_initial();
248
249        let c_uri = to_cstring_arg(uri)?;
250
251        esp!(unsafe { esp_http_client_set_url(self.raw_client, c_uri.as_ptr() as _) })?;
252        esp!(unsafe {
253            esp_http_client_set_method(
254                self.raw_client,
255                Newtype::<(esp_http_client_method_t, ())>::from(method).0 .0,
256            )
257        })?;
258
259        let mut content_len = None;
260
261        for (name, value) in headers {
262            if name.eq_ignore_ascii_case("Content-Length") {
263                if let Ok(len) = value.parse::<i64>() {
264                    content_len = Some(len);
265                }
266            } else {
267                let c_name = to_cstring_arg(name)?;
268
269                // TODO: Replace with a proper conversion from UTF8 to ISO-8859-1
270                let c_value = to_cstring_arg(value)?;
271
272                esp!(unsafe {
273                    esp_http_client_set_header(
274                        self.raw_client,
275                        c_name.as_ptr() as _,
276                        c_value.as_ptr() as _,
277                    )
278                })?;
279            }
280        }
281
282        self.follow_redirects = match self.follow_redirects_policy {
283            FollowRedirectsPolicy::FollowAll => true,
284            FollowRedirectsPolicy::FollowGetHead => method == Method::Get || method == Method::Head,
285            _ => false,
286        };
287
288        // No Content-Length for POST requests means chunked encoding
289        // This is indicated to the ESP IDF client by setting the
290        // content length param of `esp_http_client_open` to -1
291        self.request_content_len =
292            content_len.unwrap_or(if method == Method::Post { -1 } else { 0 });
293
294        esp!(unsafe { esp_http_client_open(self.raw_client, self.request_content_len as i32) })?;
295
296        self.state = State::Request;
297
298        Ok(())
299    }
300
301    pub fn is_request_initiated(&self) -> bool {
302        self.state == State::Request
303    }
304
305    pub fn initiate_response(&mut self) -> Result<(), EspError> {
306        self.assert_request();
307
308        self.flush()?;
309        self.fetch_headers()?;
310
311        self.state = State::Response;
312
313        Ok(())
314    }
315
316    pub fn is_response_initiated(&self) -> bool {
317        self.state == State::Response
318    }
319
320    pub fn split(&mut self) -> (&EspHttpConnection, &mut Self) {
321        self.assert_response();
322
323        let headers_ptr: *const EspHttpConnection = self as *const _;
324
325        let headers = unsafe { headers_ptr.as_ref().unwrap() };
326
327        (headers, self)
328    }
329
330    pub fn read(&mut self, buf: &mut [u8]) -> Result<usize, EspError> {
331        self.assert_response();
332
333        let result = Self::check(unsafe {
334            esp_http_client_read(self.raw_client, buf.as_mut_ptr() as _, buf.len() as _)
335        });
336
337        // workaround since esp_http_client_read does not yet return EAGAIN error in ESP-IDF v4.
338        // in ESP-IDF v5 esp_http_client_read will return EAGAIN and this should not be needed.
339        #[cfg(esp_idf_version_major = "4")]
340        {
341            match result {
342                Ok(0) if unsafe { !esp_http_client_is_complete_data_received(self.raw_client) } => {
343                    // no error but read 0 bytes and body is not yet complete, probably caused by EAGAIN
344                    Err(EspError::from_infallible::<ESP_ERR_HTTP_EAGAIN>())
345                }
346                other => other,
347            }
348        }
349        #[cfg(not(esp_idf_version_major = "4"))]
350        {
351            result
352        }
353    }
354
355    pub fn write(&mut self, buf: &[u8]) -> Result<usize, EspError> {
356        if buf.is_empty() {
357            Ok(0)
358        } else if !self.raw_request_body && self.request_content_len == -1 {
359            // Assume chunked encoding and send the buffer as the next chunk
360            let mut len_buf = heapless::String::<8>::new();
361            write!(&mut len_buf, "{:x}", buf.len()).unwrap();
362
363            self.raw_write_all(len_buf.as_bytes())?;
364            self.raw_write_all(b"\r\n")?;
365
366            self.raw_write_all(buf)?;
367            self.raw_write_all(b"\r\n")?;
368
369            Ok(buf.len())
370        } else {
371            self.raw_write(buf)
372        }
373    }
374
375    pub fn write_all(&mut self, data: &[u8]) -> Result<(), EspError> {
376        let mut offset = 0;
377
378        while offset < data.len() {
379            offset += self.write(&data[offset..])?;
380        }
381
382        Ok(())
383    }
384
385    fn flush(&mut self) -> Result<(), EspError> {
386        if !self.raw_request_body && self.request_content_len == -1 {
387            // Finish the chunked-encoded stream
388            self.raw_write_all(b"0\r\n\r\n")
389        } else {
390            Ok(())
391        }
392    }
393
394    fn flush_response(&mut self) -> Result<(), EspError> {
395        let mut len = 0_i32;
396        esp!(unsafe { esp_http_client_flush_response(self.raw_client, &mut len) })?;
397
398        Ok(())
399    }
400
401    fn raw_write(&mut self, buf: &[u8]) -> Result<usize, EspError> {
402        self.assert_request();
403
404        Self::check(unsafe {
405            esp_http_client_write(self.raw_client, buf.as_ptr() as _, buf.len() as _)
406        })
407    }
408
409    fn raw_write_all(&mut self, data: &[u8]) -> Result<(), EspError> {
410        let mut offset = 0;
411
412        while offset < data.len() {
413            offset += self.raw_write(&data[offset..])?;
414        }
415
416        Ok(())
417    }
418
419    fn check(result: i32) -> Result<usize, EspError> {
420        match EspError::from(result) {
421            Some(err) if result < 0 => Err(err),
422            _ => Ok(result as _),
423        }
424    }
425
426    extern "C" fn on_events(event: *mut esp_http_client_event_t) -> esp_err_t {
427        match unsafe { event.as_mut() } {
428            Some(event) => {
429                let handler = event.user_data
430                    as *const Option<Box<dyn Fn(&esp_http_client_event_t) -> esp_err_t>>;
431                if let Some(handler) = unsafe { handler.as_ref() } {
432                    if let Some(handler) = handler.as_ref() {
433                        return handler(event);
434                    }
435                }
436
437                ESP_OK as _
438            }
439            None => ESP_FAIL as _,
440        }
441    }
442
443    fn fetch_headers(&mut self) -> Result<(), EspError> {
444        self.headers.clear();
445        *self.content_len_header.get_mut() = None;
446
447        loop {
448            // TODO: Implement a mechanism where the client can declare in which header it is interested
449            let headers_ptr = &mut self.headers as *mut BTreeMap<Uncased, String>;
450
451            let handler = move |event: &esp_http_client_event_t| {
452                if event.event_id == esp_http_client_event_id_t_HTTP_EVENT_ON_HEADER {
453                    unsafe {
454                        // TODO: Replace with a proper conversion from ISO-8859-1 to UTF8
455
456                        headers_ptr.as_mut().unwrap().insert(
457                            Uncased::from(from_cstr_ptr(event.header_key).to_string()),
458                            from_cstr_ptr(event.header_value).to_string(),
459                        );
460                    }
461                }
462
463                ESP_OK as esp_err_t
464            };
465
466            self.register_handler(handler);
467
468            let result = unsafe { esp_http_client_fetch_headers(self.raw_client) };
469
470            self.deregister_handler();
471
472            Self::check(result as _)?;
473
474            trace!("Fetched headers: {:?}", self.headers);
475
476            if self.follow_redirects {
477                let status = unsafe { esp_http_client_get_status_code(self.raw_client) as u16 };
478
479                if status::REDIRECT.contains(&status) && status != 304 {
480                    info!("Got response {status}, about to follow redirect");
481
482                    let mut len = 0_i32;
483                    esp!(unsafe { esp_http_client_flush_response(self.raw_client, &mut len) })?;
484                    esp!(unsafe {
485                        esp_http_client_set_method(
486                            self.raw_client,
487                            esp_http_client_method_t_HTTP_METHOD_GET,
488                        )
489                    })?;
490                    esp!(unsafe { esp_http_client_set_redirection(self.raw_client) })?;
491                    esp!(unsafe {
492                        esp_http_client_open(self.raw_client, self.request_content_len as i32)
493                    })?;
494
495                    self.headers.clear();
496
497                    continue;
498                }
499            }
500
501            break;
502        }
503
504        Ok(())
505    }
506
507    fn register_handler(
508        &mut self,
509        handler: impl Fn(&esp_http_client_event_t) -> esp_err_t + 'static,
510    ) {
511        *self.event_handler = Some(Box::new(handler));
512    }
513
514    fn deregister_handler(&mut self) {
515        *self.event_handler = None;
516    }
517
518    fn assert_initial(&self) {
519        if self.state != State::New && self.state != State::Response {
520            panic!("connection is not in initial phase");
521        }
522    }
523
524    fn assert_request(&self) {
525        if self.state != State::Request {
526            panic!("connection is not in request phase");
527        }
528    }
529
530    fn assert_response(&self) {
531        if self.state != State::Response {
532            panic!("connection is not in response phase");
533        }
534    }
535}
536
537impl Drop for EspHttpConnection {
538    fn drop(&mut self) {
539        esp!(unsafe { esp_http_client_cleanup(self.raw_client) })
540            .expect("Unable to stop the client cleanly");
541    }
542}
543
544impl RawHandle for EspHttpConnection {
545    type Handle = esp_http_client_handle_t;
546
547    fn handle(&self) -> Self::Handle {
548        self.raw_client
549    }
550}
551
552impl embedded_svc::http::Status for EspHttpConnection {
553    fn status(&self) -> u16 {
554        EspHttpConnection::status(self)
555    }
556
557    fn status_message(&self) -> Option<&str> {
558        EspHttpConnection::status_message(self)
559    }
560}
561
562impl embedded_svc::http::Headers for EspHttpConnection {
563    fn header(&self, name: &str) -> Option<&str> {
564        EspHttpConnection::header(self, name)
565    }
566}
567
568impl ErrorType for EspHttpConnection {
569    type Error = EspIOError;
570}
571
572impl Read for EspHttpConnection {
573    fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
574        let size = EspHttpConnection::read(self, buf)?;
575
576        Ok(size)
577    }
578}
579
580impl Write for EspHttpConnection {
581    fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
582        let size = EspHttpConnection::write(self, buf)?;
583
584        Ok(size)
585    }
586
587    fn flush(&mut self) -> Result<(), Self::Error> {
588        self.assert_request();
589
590        Ok(())
591    }
592}
593
594impl embedded_svc::http::client::Connection for EspHttpConnection {
595    type Headers = Self;
596
597    type Read = Self;
598
599    type RawConnectionError = EspIOError;
600
601    type RawConnection = Self;
602
603    fn initiate_request<'a>(
604        &'a mut self,
605        method: Method,
606        uri: &'a str,
607        headers: &'a [(&'a str, &'a str)],
608    ) -> Result<(), Self::Error> {
609        EspHttpConnection::initiate_request(self, method, uri, headers).map_err(EspIOError)
610    }
611
612    fn is_request_initiated(&self) -> bool {
613        EspHttpConnection::is_request_initiated(self)
614    }
615
616    fn initiate_response(&mut self) -> Result<(), Self::Error> {
617        EspHttpConnection::initiate_response(self).map_err(EspIOError)
618    }
619
620    fn is_response_initiated(&self) -> bool {
621        EspHttpConnection::is_response_initiated(self)
622    }
623
624    fn split(&mut self) -> (&Self::Headers, &mut Self::Read) {
625        EspHttpConnection::split(self)
626    }
627
628    fn raw_connection(&mut self) -> Result<&mut Self::RawConnection, Self::Error> {
629        Err(EspError::from_infallible::<ESP_FAIL>().into())
630    }
631}