ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
http_request_idf.cpp
Go to the documentation of this file.
1#include "http_request_idf.h"
2
3#ifdef USE_ESP32
4
5#include <algorithm>
6#include <cerrno>
7#include <cstring>
8
9#include <fcntl.h>
10#include <sys/select.h>
11
14
16#include "esphome/core/log.h"
18
19#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
20#include "esp_crt_bundle.h"
21#endif
22
23#include "esp_task_wdt.h"
24
25namespace esphome::http_request {
26
27ESPHOME_LOG_TAG(TAG, "http_request");
28
29// Request body sent per esp_http_client_write() call, and the time one poll() spends sending it
30static constexpr size_t SEND_CHUNK_SIZE = 1024;
31static constexpr uint32_t SEND_BUDGET_MS = 10;
32static constexpr uint32_t ERROR_DURATION_MS = 1000;
33
36 ESP_LOGCONFIG(TAG,
37 " Buffer Size RX: %u\n"
38 " Buffer Size TX: %u\n"
39 " Custom CA Certificate: %s",
40 this->buffer_size_rx_, this->buffer_size_tx_, YESNO(this->ca_certificate_ != nullptr));
41}
42
43esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) {
44 auto *container = (HttpContainerIDF *) evt->user_data;
45
46 switch (evt->event_id) {
47 case HTTP_EVENT_ON_HEADER: {
48 const std::string header_name = str_lower_case(evt->header_key); // NOLINT
49 if (should_collect_header(container->collect_headers_, header_name)) {
50 const std::string header_value = evt->header_value;
51 ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str());
52 container->response_headers_.push_back({header_name, header_value});
53 }
54 break;
55 }
56 default: {
57 break;
58 }
59 }
60 return ESP_OK;
61}
62
63std::shared_ptr<HttpContainerIDF> HttpRequestIDF::create_container_(
64 const char *url, const char *method, const std::vector<Header> &request_headers,
65 const std::vector<std::string> &lower_case_collect_headers, uint32_t timeout_ms, [[maybe_unused]] bool async) {
66 if (!network::is_connected()) {
67 this->status_momentary_error(ERROR_DURATION_MS);
68 ESP_LOGE(TAG, "HTTP Request failed; Not connected to network");
69 return nullptr;
70 }
71
72 esp_http_client_method_t method_idf;
73 if (strcmp(method, "GET") == 0) {
74 method_idf = HTTP_METHOD_GET;
75 } else if (strcmp(method, "POST") == 0) {
76 method_idf = HTTP_METHOD_POST;
77 } else if (strcmp(method, "PUT") == 0) {
78 method_idf = HTTP_METHOD_PUT;
79 } else if (strcmp(method, "DELETE") == 0) {
80 method_idf = HTTP_METHOD_DELETE;
81 } else if (strcmp(method, "PATCH") == 0) {
82 method_idf = HTTP_METHOD_PATCH;
83 } else {
84 this->status_momentary_error(ERROR_DURATION_MS);
85 ESP_LOGE(TAG, "HTTP Request failed; Unsupported method");
86 return nullptr;
87 }
88
89 // Only the scheme counts; "https:" later in the URL (a query string) does not make it secure
90 bool secure = ESPHOME_strncasecmp_P(url, ESPHOME_PSTR("https:"), 6) == 0;
91
92 esp_http_client_config_t config = {};
93
94 config.url = url;
95 config.method = method_idf;
96 config.timeout_ms = timeout_ms;
97 config.disable_auto_redirect = !this->follow_redirects_;
98 config.max_redirection_count = this->redirect_limit_;
99 config.auth_type = HTTP_AUTH_TYPE_BASIC;
100 if (secure && this->verify_ssl_) {
101 if (this->ca_certificate_ != nullptr) {
102 config.cert_pem = this->ca_certificate_;
103#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
104 } else {
105 config.crt_bundle_attach = esp_crt_bundle_attach;
106#endif
107 }
108 }
109
110 if (this->useragent_ != nullptr) {
111 config.user_agent = this->useragent_;
112 }
113
114 config.buffer_size = this->buffer_size_rx_;
115 config.buffer_size_tx = this->buffer_size_tx_;
116
117 config.event_handler = http_event_handler;
118#ifdef USE_HTTP_REQUEST_IDF_SOCKET_POLL
119 // Only TLS has a non-blocking connect worth having: the handshake. A plain connection waits for
120 // the TCP connection either way.
121 config.is_async = async && secure;
122#endif
123
124 esp_http_client_handle_t client = esp_http_client_init(&config);
125 if (client == nullptr) {
126 this->status_momentary_error(ERROR_DURATION_MS);
127 ESP_LOGE(TAG, "HTTP Request failed; client could not be initialized");
128 return nullptr;
129 }
130
131 std::shared_ptr<HttpContainerIDF> container = std::make_shared<HttpContainerIDF>(client);
132 container->set_parent(this);
133 container->set_watchdog_timeout(this->get_watchdog_timeout(timeout_ms));
134 container->timeout_ms_ = timeout_ms;
135 container->async_ = config.is_async;
136
137 container->set_secure(secure);
138
139 container->collect_headers_ = lower_case_collect_headers;
140 esp_http_client_set_user_data(client, static_cast<void *>(container.get()));
141
142 for (const auto &header : request_headers) {
143 esp_http_client_set_header(client, header.name.c_str(), header.value.c_str());
144 }
145
146 return container;
147}
148
149std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const char *url, const char *method, const std::string &body,
150 const std::vector<Header> &request_headers,
151 const std::vector<std::string> &lower_case_collect_headers,
152 uint32_t timeout_ms) {
153 std::shared_ptr<HttpContainerIDF> container = this->start_container_(url, method, std::string(body), request_headers,
154 lower_case_collect_headers, timeout_ms, false);
155 if (container == nullptr) {
156 return nullptr;
157 }
158 // The same stages as begin(), run here until the response headers are in
159 watchdog::WatchdogManager wdm(container->get_watchdog_timeout());
160 HttpRequestProgress progress;
161 while ((progress = container->poll()) == HttpRequestProgress::HTTP_REQUEST_PROGRESS_PENDING) {
162 container->feed_wdt();
163 delay(1);
164 }
166 container->end();
167 return nullptr;
168 }
169 return container;
170}
171
172std::shared_ptr<HttpContainer> HttpRequestIDF::begin(const char *url, const char *method, std::string &&body,
173 const std::vector<Header> &request_headers,
174 const std::vector<std::string> &lower_case_collect_headers,
175 uint32_t timeout_ms) {
176 return this->start_container_(url, method, std::move(body), request_headers, lower_case_collect_headers, timeout_ms,
177 true);
178}
179
180std::shared_ptr<HttpContainerIDF> HttpRequestIDF::start_container_(
181 const char *url, const char *method, std::string &&body, const std::vector<Header> &request_headers,
182 const std::vector<std::string> &lower_case_collect_headers, uint32_t timeout_ms, bool async) {
183 std::shared_ptr<HttpContainerIDF> container =
184 this->create_container_(url, method, request_headers, lower_case_collect_headers, timeout_ms, async);
185 if (container == nullptr) {
186 return nullptr;
187 }
188 container->body_ = std::move(body);
189 container->start_time_ = millis();
190 container->progress_time_ = container->start_time_;
191 container->redirects_left_ = this->follow_redirects_ ? this->redirect_limit_ : 0;
192 container->stage_ = HttpContainerIDF::Stage::STAGE_CONNECT;
193 return container;
194}
195
197
199 ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(err));
200 this->parent_->status_momentary_error(ERROR_DURATION_MS);
203}
204
206 // millis(), not the loop time: progress_time_ is set after calls that block within this pass
207 if (millis() - this->progress_time_ >= this->timeout_ms_) {
208 return this->fail_(ESP_ERR_TIMEOUT);
209 }
211}
212
214#ifdef USE_HTTP_REQUEST_IDF_SOCKET_POLL
215 const int fd = esp_http_client_get_socket(this->client_);
216 if (fd < 0) {
217 return true;
218 }
219 fd_set fds;
220 FD_ZERO(&fds);
221 FD_SET(fd, &fds);
222 timeval no_wait{};
223 // An error also counts as ready, so that the call that follows reports it
224 // Qualified: esphome::select is a namespace
225 return ::select(fd + 1, write ? nullptr : &fds, write ? &fds : nullptr, nullptr, &no_wait) != 0;
226#else
227 return true;
228#endif
229}
230
231// Each stage either finishes, or returns PENDING after a call that did not wait. The waits left are
232// the name lookup and TCP connection in the first esp_http_client_open() of each hop: a non-blocking
233// esp-tls connect that times out once never completes, as select() clears the sets it keeps.
235 while (true) {
236 switch (this->stage_) {
239
242
244 // Only the first call of a hop waits
246 esp_http_client_set_timeout_ms(this->client_, this->connecting_ ? 1 : this->timeout_ms_);
247 this->feed_wdt();
248 esp_err_t err = esp_http_client_open(this->client_, this->body_.size());
249 this->feed_wdt();
250 if (err == ESP_ERR_HTTP_EAGAIN) {
251 this->connecting_ = true;
252 return this->wait_();
253 }
254 this->connecting_ = false;
255 if (err != ESP_OK) {
256 return this->fail_(err);
257 }
258#ifdef USE_HTTP_REQUEST_IDF_SOCKET_POLL
259 if (this->async_) {
260 // Back to a blocking socket, so that mbedtls waits for the rest of a record instead of
261 // failing; reads only start once the socket is readable
262 const int fd = esp_http_client_get_socket(this->client_);
263 const int flags = fd < 0 ? -1 : fcntl(fd, F_GETFL, 0);
264 if (flags < 0 || fcntl(fd, F_SETFL, flags & ~O_NONBLOCK) < 0) {
265 ESP_LOGE(TAG, "Could not make the socket blocking: fd %d, errno %d", fd, errno);
266 return this->fail_(ESP_FAIL);
267 }
268 }
269#endif
270 esp_http_client_set_timeout_ms(this->client_, this->timeout_ms_);
271 this->progress_time_ = millis();
272 this->stage_ = this->body_sent_ < this->body_.size() ? Stage::STAGE_SEND_BODY : Stage::STAGE_HEADERS;
273 break;
274 }
275
277 // esp_http_client_write() returns only once all it was given is sent, so give it a piece at a time
279 const uint32_t start = millis();
280 while (this->body_sent_ < this->body_.size()) {
281 if (!this->socket_ready_(true)) {
282 return this->wait_();
283 }
284 const size_t len = std::min(this->body_.size() - this->body_sent_, SEND_CHUNK_SIZE);
285 int written = esp_http_client_write(this->client_, this->body_.data() + this->body_sent_, len);
286 this->feed_wdt();
287 if (written < 0) {
288 return this->fail_(ESP_FAIL);
289 }
290 if (written == 0) {
291 return this->wait_();
292 }
293 this->body_sent_ += written;
294 this->progress_time_ = millis();
295 if (this->progress_time_ - start >= SEND_BUDGET_MS && this->body_sent_ < this->body_.size()) {
297 }
298 }
299 this->body_ = std::string();
301 break;
302 }
303
305 if (!this->socket_ready_(false)) {
308 }
309 // As perform(), where esp_http_client_fetch_headers() times out: no response is status -1
310 return this->response_failed_(-1);
311 }
312 // The response has begun to arrive, so this waits only for the rest of the headers, or over TLS
313 // the rest of the record that holds them
315 this->feed_wdt();
316 // esp_http_client_fetch_headers() returns 0 for chunked transfer encoding (no Content-Length header)
317 const int64_t length = esp_http_client_fetch_headers(this->client_);
318 this->feed_wdt();
319 this->duration_ms = millis() - this->start_time_;
320 if (length < 0) {
321 return this->response_failed_(-1);
322 }
323 this->content_length = length;
324 this->set_chunked(esp_http_client_is_chunked_response(this->client_));
325 this->status_code = esp_http_client_get_status_code(this->client_);
326 if (is_success(this->status_code)) {
328 break;
329 }
330 if (this->redirects_left_ > 0 && is_redirect(this->status_code)) {
331 esp_err_t err = esp_http_client_set_redirection(this->client_);
332 if (err != ESP_OK) {
333 ESP_LOGE(TAG, "esp_http_client_set_redirection failed: %s", esp_err_to_name(err));
334 return this->fail_(err);
335 }
336#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
337 char redirect_url[256]{};
338 if (esp_http_client_get_url(this->client_, redirect_url, sizeof(redirect_url) - 1) == ESP_OK) {
339 ESP_LOGV(TAG, "redirecting to url: %s", redirect_url);
340 }
341#endif
342 this->redirects_left_--;
343 // Collect only the final response's headers; redirects do not resend the body
344 this->response_headers_.clear();
345 this->body_ = std::string();
346 this->body_sent_ = 0;
347 this->progress_time_ = millis();
349 break;
350 }
351 if (is_redirect(this->status_code) && this->idf_parent_()->follow_redirects_) {
352 ESP_LOGW(TAG, "Reach redirect limit count=%d", this->idf_parent_()->redirect_limit_);
353 }
354 return this->response_failed_(this->status_code);
355 }
356 }
357 }
358}
359
361 this->status_code = status_code;
362 this->duration_ms = millis() - this->start_time_;
363 char url[256]{};
364 esp_http_client_get_url(this->client_, url, sizeof(url) - 1);
365 ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url, this->status_code);
366 this->parent_->status_momentary_error(ERROR_DURATION_MS);
369}
370
372 // Base class handles no-body status codes and non-chunked content_length completion
374 return true;
375 }
376 // For chunked responses, use the authoritative ESP-IDF completion check
377 return this->is_chunked_ && esp_http_client_is_complete_data_received(this->client_);
378}
379
380// ESP-IDF HTTP read implementation (blocking mode)
381//
382// WARNING: Return values differ from BSD sockets! See http_request.h for full documentation.
383//
384// esp_http_client_read() in blocking mode returns:
385// > 0: bytes read
386// 0: all chunked data received (is_chunk_complete true) or connection closed
387// -ESP_ERR_HTTP_EAGAIN: transport timeout, no data available yet
388// < 0: error
389//
390// We normalize to HttpContainer::read() contract:
391// > 0: bytes read
392// 0: all content read (for both content_length-based and chunked completion)
393// < 0: error/connection closed
394//
395// Note on chunked transfer encoding:
396// esp_http_client_fetch_headers() returns 0 for chunked responses (no Content-Length header).
397// When esp_http_client_read() returns 0 for a chunked response, is_read_complete() calls
398// esp_http_client_is_complete_data_received() to distinguish successful completion from
399// connection errors. Callers use http_read_loop_result() which checks is_read_complete()
400// to return COMPLETE for successful chunked EOF.
401//
402// Streaming chunked responses are not supported (see http_request.h for details).
403// When data stops arriving, esp_http_client_read() returns -ESP_ERR_HTTP_EAGAIN
404// after its internal transport timeout (configured via timeout_ms) expires.
405// This is passed through as a negative return value, which callers treat as an error.
406int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
408 esp_http_client_set_timeout_ms(this->client_, this->timeout_ms_);
409 return this->read_(buf, max_len);
410}
411
412int HttpContainerIDF::read_available(uint8_t *buf, size_t max_len) {
413 // A zero timeout only polls; TLS still waits for the rest of a record that has begun to arrive
415 esp_http_client_set_timeout_ms(this->client_, 0);
416 int read_len_or_error = this->read_(buf, max_len);
417 return read_len_or_error == -ESP_ERR_HTTP_EAGAIN ? 0 : read_len_or_error;
418}
419
420int HttpContainerIDF::read_(uint8_t *buf, size_t max_len) {
421 const uint32_t start = millis();
422
423 // Check if we've already read all expected content (non-chunked and no-body only).
424 // Use the base class check here, NOT the override: esp_http_client_is_complete_data_received()
425 // returns true as soon as all data arrives from the network, but data may still be in
426 // the client's internal buffer waiting to be consumed by esp_http_client_read().
428 return 0; // All content read successfully
429 }
430
431 this->feed_wdt();
432 int read_len_or_error = esp_http_client_read(this->client_, (char *) buf, max_len);
433 this->feed_wdt();
434
435 this->duration_ms += (millis() - start);
436
437 if (read_len_or_error > 0) {
438 this->bytes_read_ += read_len_or_error;
439 return read_len_or_error;
440 }
441
442 // esp_http_client_read() returns 0 when:
443 // - Known content_length: connection closed before all data received (error)
444 // - Chunked encoding: all chunks received (is_chunk_complete true, genuine EOF)
445 //
446 // Return 0 in both cases. Callers use http_read_loop_result() which calls
447 // is_read_complete() to distinguish these:
448 // - Chunked complete: is_read_complete() returns true (via
449 // esp_http_client_is_complete_data_received()), caller gets COMPLETE
450 // - Non-chunked incomplete: is_read_complete() returns false, caller
451 // eventually gets TIMEOUT (since no more data arrives)
452 if (read_len_or_error == 0) {
453 return 0;
454 }
455
456 // Negative value - error, return the actual error code for debugging
457 return read_len_or_error;
458}
459
461 if (this->client_ == nullptr) {
462 return; // Already cleaned up
463 }
465
466 esp_http_client_close(this->client_);
467 esp_http_client_cleanup(this->client_);
468 this->client_ = nullptr;
469}
470
472 // Tests to see if the executing task has a watchdog timer attached
473 if (esp_task_wdt_status(nullptr) == ESP_OK) {
474 App.feed_wdt();
475 }
476}
477
478} // namespace esphome::http_request
479
480#endif // USE_ESP32
void feed_wdt()
Feed the task watchdog.
void status_momentary_error(uint32_t length=5000)
Set error status flag and clear it after length ms.
virtual bool is_read_complete() const
Check if all expected content has been read.
std::vector< Header > response_headers_
int status_code
-1 indicates no response received yet
bool is_chunked_
True if response uses chunked transfer encoding.
bool socket_ready_(bool write)
True when the socket can be read or written without waiting; always true when it cannot be checked.
HttpRequestProgress response_failed_(int status_code)
Finish with an unsuccessful status, which is reported through on_response like a success.
HttpRequestProgress poll() override
int read(uint8_t *buf, size_t max_len) override
int read_(uint8_t *buf, size_t max_len)
HttpRequestProgress fail_(esp_err_t err)
void feed_wdt()
Feeds the watchdog timer if the executing task has one attached.
int read_available(uint8_t *buf, size_t max_len) override
std::shared_ptr< HttpContainerIDF > start_container_(const char *url, const char *method, std::string &&body, const std::vector< Header > &request_headers, const std::vector< std::string > &lower_case_collect_headers, uint32_t timeout_ms, bool async)
A container that poll() takes from the connect through to the response headers.
std::shared_ptr< HttpContainer > perform(const char *url, const char *method, const std::string &body, const std::vector< Header > &request_headers, const std::vector< std::string > &lower_case_collect_headers, uint32_t timeout_ms) override
std::shared_ptr< HttpContainer > begin(const char *url, const char *method, std::string &&body, const std::vector< Header > &request_headers, const std::vector< std::string > &lower_case_collect_headers, uint32_t timeout_ms) override
std::shared_ptr< HttpContainerIDF > create_container_(const char *url, const char *method, const std::vector< Header > &request_headers, const std::vector< std::string > &lower_case_collect_headers, uint32_t timeout_ms, bool async)
static esp_err_t http_event_handler(esp_http_client_event_t *evt)
Monitors the http client events to gather response headers.
uint16_t flags
bool should_collect_header(const std::vector< std::string > &lower_case_collect_headers, const std::string &lower_header_name)
Check if a header name should be collected (linear scan, fine for small lists)
HttpRequestProgress
Progress of a request started with HttpRequestComponent::begin()
@ HTTP_REQUEST_PROGRESS_FAILED
No response; the container must still be ended.
@ HTTP_REQUEST_PROGRESS_DONE
Response headers received; status_code is set and the body can be read.
@ HTTP_REQUEST_PROGRESS_PENDING
Still connecting, sending, or waiting for the response headers.
bool is_success(int const status)
Checks if the given HTTP status code indicates a successful request.
ESPHOME_LOG_TAG(TAG, "http_request")
bool is_redirect(int const status)
Returns true if the HTTP status code is a redirect.
ESPHOME_ALWAYS_INLINE bool is_connected()
Return whether the node is connected to the network (through wifi, eth, ...)
Definition util.h:36
std::string str_lower_case(const std::string &str)
Convert the string to lower case.
const void size_t len
Definition hal.h:68
void HOT delay(uint32_t ms)
Definition hal.cpp:85
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:25
int written
Definition helpers.h:1262
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t
uint16_t length
Definition tt21100.cpp:0