ESPHome 2026.10.1
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 <cstring>
6
9
11#include "esphome/core/log.h"
12
13#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
14#include "esp_crt_bundle.h"
15#endif
16
17#include "esp_task_wdt.h"
18
19namespace esphome::http_request {
20
21static const char *const TAG = "http_request";
22static constexpr uint32_t ERROR_DURATION_MS = 1000;
23
26 ESP_LOGCONFIG(TAG,
27 " Buffer Size RX: %u\n"
28 " Buffer Size TX: %u\n"
29 " Custom CA Certificate: %s",
30 this->buffer_size_rx_, this->buffer_size_tx_, YESNO(this->ca_certificate_ != nullptr));
31}
32
33esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) {
34 auto *container = (HttpContainerIDF *) evt->user_data;
35
36 switch (evt->event_id) {
37 case HTTP_EVENT_ON_HEADER: {
38 const std::string header_name = str_lower_case(evt->header_key); // NOLINT
39 if (should_collect_header(container->collect_headers_, header_name)) {
40 const std::string header_value = evt->header_value;
41 ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str());
42 container->response_headers_.push_back({header_name, header_value});
43 }
44 break;
45 }
46 default: {
47 break;
48 }
49 }
50 return ESP_OK;
51}
52
53std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const char *url, const char *method, const std::string &body,
54 const std::vector<Header> &request_headers,
55 const std::vector<std::string> &lower_case_collect_headers) {
56 if (!network::is_connected()) {
57 this->status_momentary_error("failed", ERROR_DURATION_MS);
58 ESP_LOGE(TAG, "HTTP Request failed; Not connected to network");
59 return nullptr;
60 }
61
62 esp_http_client_method_t method_idf;
63 if (strcmp(method, "GET") == 0) {
64 method_idf = HTTP_METHOD_GET;
65 } else if (strcmp(method, "POST") == 0) {
66 method_idf = HTTP_METHOD_POST;
67 } else if (strcmp(method, "PUT") == 0) {
68 method_idf = HTTP_METHOD_PUT;
69 } else if (strcmp(method, "DELETE") == 0) {
70 method_idf = HTTP_METHOD_DELETE;
71 } else if (strcmp(method, "PATCH") == 0) {
72 method_idf = HTTP_METHOD_PATCH;
73 } else {
74 this->status_momentary_error("failed", ERROR_DURATION_MS);
75 ESP_LOGE(TAG, "HTTP Request failed; Unsupported method");
76 return nullptr;
77 }
78
79 bool secure = strstr(url, "https:") != nullptr;
80
81 esp_http_client_config_t config = {};
82
83 config.url = url;
84 config.method = method_idf;
85 config.timeout_ms = this->timeout_;
86 config.disable_auto_redirect = !this->follow_redirects_;
87 config.max_redirection_count = this->redirect_limit_;
88 config.auth_type = HTTP_AUTH_TYPE_BASIC;
89 if (secure && this->verify_ssl_) {
90 if (this->ca_certificate_ != nullptr) {
91 config.cert_pem = this->ca_certificate_;
92#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
93 } else {
94 config.crt_bundle_attach = esp_crt_bundle_attach;
95#endif
96 }
97 }
98
99 if (this->useragent_ != nullptr) {
100 config.user_agent = this->useragent_;
101 }
102
103 config.buffer_size = this->buffer_size_rx_;
104 config.buffer_size_tx = this->buffer_size_tx_;
105
106 const uint32_t start = millis();
108
109 config.event_handler = http_event_handler;
110
111 esp_http_client_handle_t client = esp_http_client_init(&config);
112 if (client == nullptr) {
113 this->status_momentary_error("failed", ERROR_DURATION_MS);
114 ESP_LOGE(TAG, "HTTP Request failed; client could not be initialized");
115 return nullptr;
116 }
117
118 std::shared_ptr<HttpContainerIDF> container = std::make_shared<HttpContainerIDF>(client);
119 container->set_parent(this);
120
121 container->set_secure(secure);
122
123 container->collect_headers_ = lower_case_collect_headers;
124 esp_http_client_set_user_data(client, static_cast<void *>(container.get()));
125
126 for (const auto &header : request_headers) {
127 esp_http_client_set_header(client, header.name.c_str(), header.value.c_str());
128 }
129
130 const int body_len = body.length();
131
132 esp_err_t err = esp_http_client_open(client, body_len);
133 if (err != ESP_OK) {
134 this->status_momentary_error("failed", ERROR_DURATION_MS);
135 ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(err));
136 esp_http_client_cleanup(client);
137 return nullptr;
138 }
139
140 if (body_len > 0) {
141 int write_left = body_len;
142 int write_index = 0;
143 const char *buf = body.c_str();
144 while (write_left > 0) {
145 int written = esp_http_client_write(client, buf + write_index, write_left);
146 if (written <= 0) {
147 err = ESP_FAIL;
148 break;
149 }
150 write_left -= written;
151 write_index += written;
152 container->feed_wdt();
153 }
154 }
155
156 if (err != ESP_OK) {
157 this->status_momentary_error("failed", ERROR_DURATION_MS);
158 ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(err));
159 esp_http_client_cleanup(client);
160 return nullptr;
161 }
162
163 container->feed_wdt();
164 // esp_http_client_fetch_headers() returns 0 for chunked transfer encoding (no Content-Length header).
165 // The read() method handles content_length == 0 specially to support chunked responses.
166 container->content_length = esp_http_client_fetch_headers(client);
167 container->set_chunked(esp_http_client_is_chunked_response(client));
168 container->feed_wdt();
169 container->status_code = esp_http_client_get_status_code(client);
170 container->feed_wdt();
171 container->duration_ms = millis() - start;
172 if (is_success(container->status_code)) {
173 return container;
174 }
175
176 if (this->follow_redirects_) {
177 auto num_redirects = this->redirect_limit_;
178 while (is_redirect(container->status_code) && num_redirects > 0) {
179 err = esp_http_client_set_redirection(client);
180 if (err != ESP_OK) {
181 ESP_LOGE(TAG, "esp_http_client_set_redirection failed: %s", esp_err_to_name(err));
182 this->status_momentary_error("failed", ERROR_DURATION_MS);
183 esp_http_client_cleanup(client);
184 return nullptr;
185 }
186#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
187 char redirect_url[256]{};
188 if (esp_http_client_get_url(client, redirect_url, sizeof(redirect_url) - 1) == ESP_OK) {
189 ESP_LOGV(TAG, "redirecting to url: %s", redirect_url);
190 }
191#endif
192 err = esp_http_client_open(client, 0);
193 if (err != ESP_OK) {
194 ESP_LOGE(TAG, "esp_http_client_open failed: %s", esp_err_to_name(err));
195 this->status_momentary_error("failed", ERROR_DURATION_MS);
196 esp_http_client_cleanup(client);
197 return nullptr;
198 }
199
200 container->feed_wdt();
201 // IDF is the only backend reusing the container across redirect hops;
202 // drop the previous hop's headers (Arduino/host collect only the final response)
203 container->response_headers_.clear();
204 container->content_length = esp_http_client_fetch_headers(client);
205 container->set_chunked(esp_http_client_is_chunked_response(client));
206 container->feed_wdt();
207 container->status_code = esp_http_client_get_status_code(client);
208 container->feed_wdt();
209 container->duration_ms = millis() - start;
210 if (is_success(container->status_code)) {
211 return container;
212 }
213
214 num_redirects--;
215 }
216
217 if (num_redirects == 0) {
218 ESP_LOGW(TAG, "Reach redirect limit count=%d", this->redirect_limit_);
219 }
220 }
221
222 ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url, container->status_code);
223 this->status_momentary_error("failed", ERROR_DURATION_MS);
224 return container;
225}
226
228 // Base class handles no-body status codes and non-chunked content_length completion
230 return true;
231 }
232 // For chunked responses, use the authoritative ESP-IDF completion check
233 return this->is_chunked_ && esp_http_client_is_complete_data_received(this->client_);
234}
235
236// ESP-IDF HTTP read implementation (blocking mode)
237//
238// WARNING: Return values differ from BSD sockets! See http_request.h for full documentation.
239//
240// esp_http_client_read() in blocking mode returns:
241// > 0: bytes read
242// 0: all chunked data received (is_chunk_complete true) or connection closed
243// -ESP_ERR_HTTP_EAGAIN: transport timeout, no data available yet
244// < 0: error
245//
246// We normalize to HttpContainer::read() contract:
247// > 0: bytes read
248// 0: all content read (for both content_length-based and chunked completion)
249// < 0: error/connection closed
250//
251// Note on chunked transfer encoding:
252// esp_http_client_fetch_headers() returns 0 for chunked responses (no Content-Length header).
253// When esp_http_client_read() returns 0 for a chunked response, is_read_complete() calls
254// esp_http_client_is_complete_data_received() to distinguish successful completion from
255// connection errors. Callers use http_read_loop_result() which checks is_read_complete()
256// to return COMPLETE for successful chunked EOF.
257//
258// Streaming chunked responses are not supported (see http_request.h for details).
259// When data stops arriving, esp_http_client_read() returns -ESP_ERR_HTTP_EAGAIN
260// after its internal transport timeout (configured via timeout_ms) expires.
261// This is passed through as a negative return value, which callers treat as an error.
262int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
263 const uint32_t start = millis();
264 watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
265
266 // Check if we've already read all expected content (non-chunked and no-body only).
267 // Use the base class check here, NOT the override: esp_http_client_is_complete_data_received()
268 // returns true as soon as all data arrives from the network, but data may still be in
269 // the client's internal buffer waiting to be consumed by esp_http_client_read().
271 return 0; // All content read successfully
272 }
273
274 this->feed_wdt();
275 int read_len_or_error = esp_http_client_read(this->client_, (char *) buf, max_len);
276 this->feed_wdt();
277
278 this->duration_ms += (millis() - start);
279
280 if (read_len_or_error > 0) {
281 this->bytes_read_ += read_len_or_error;
282 return read_len_or_error;
283 }
284
285 // esp_http_client_read() returns 0 when:
286 // - Known content_length: connection closed before all data received (error)
287 // - Chunked encoding: all chunks received (is_chunk_complete true, genuine EOF)
288 //
289 // Return 0 in both cases. Callers use http_read_loop_result() which calls
290 // is_read_complete() to distinguish these:
291 // - Chunked complete: is_read_complete() returns true (via
292 // esp_http_client_is_complete_data_received()), caller gets COMPLETE
293 // - Non-chunked incomplete: is_read_complete() returns false, caller
294 // eventually gets TIMEOUT (since no more data arrives)
295 if (read_len_or_error == 0) {
296 return 0;
297 }
298
299 // Negative value - error, return the actual error code for debugging
300 return read_len_or_error;
301}
302
304 if (this->client_ == nullptr) {
305 return; // Already cleaned up
306 }
307 watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
308
309 esp_http_client_close(this->client_);
310 esp_http_client_cleanup(this->client_);
311 this->client_ = nullptr;
312}
313
315 // Tests to see if the executing task has a watchdog timer attached
316 if (esp_task_wdt_status(nullptr) == ESP_OK) {
317 App.feed_wdt();
318 }
319}
320
321} // namespace esphome::http_request
322
323#endif // USE_ESP32
void feed_wdt()
Feed the task watchdog.
void status_momentary_error(const char *name, uint32_t length=5000)
Set error status flag and automatically clear it after a timeout.
virtual bool is_read_complete() const
Check if all expected content has been read.
bool is_chunked_
True if response uses chunked transfer encoding.
int read(uint8_t *buf, size_t max_len) override
void feed_wdt()
Feeds the watchdog timer if the executing task has one attached.
std::shared_ptr< HttpContainer > start(const char *url, const char *method, const std::string &body, const std::vector< Header > &request_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) override
static esp_err_t http_event_handler(esp_http_client_event_t *evt)
Monitors the http client events to gather response headers.
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)
bool is_success(int const status)
Checks if the given HTTP status code indicates a successful 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:28
std::string str_lower_case(const std::string &str)
Convert the string to lower case.
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:25
int written
Definition helpers.h:1193
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t