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