ESPHome 2026.1.4
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",
31 this->buffer_size_rx_, this->buffer_size_tx_);
32}
33
34esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) {
35 UserData *user_data = (UserData *) evt->user_data;
36
37 switch (evt->event_id) {
38 case HTTP_EVENT_ON_HEADER: {
39 const std::string header_name = str_lower_case(evt->header_key);
40 if (user_data->collect_headers.count(header_name)) {
41 const std::string header_value = evt->header_value;
42 ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str());
43 user_data->response_headers[header_name].push_back(header_value);
44 }
45 break;
46 }
47 default: {
48 break;
49 }
50 }
51 return ESP_OK;
52}
53
54std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, const std::string &method,
55 const std::string &body,
56 const std::list<Header> &request_headers,
57 const std::set<std::string> &collect_headers) {
58 if (!network::is_connected()) {
59 this->status_momentary_error("failed", 1000);
60 ESP_LOGE(TAG, "HTTP Request failed; Not connected to network");
61 return nullptr;
62 }
63
64 esp_http_client_method_t method_idf;
65 if (method == "GET") {
66 method_idf = HTTP_METHOD_GET;
67 } else if (method == "POST") {
68 method_idf = HTTP_METHOD_POST;
69 } else if (method == "PUT") {
70 method_idf = HTTP_METHOD_PUT;
71 } else if (method == "DELETE") {
72 method_idf = HTTP_METHOD_DELETE;
73 } else if (method == "PATCH") {
74 method_idf = HTTP_METHOD_PATCH;
75 } else {
76 this->status_momentary_error("failed", 1000);
77 ESP_LOGE(TAG, "HTTP Request failed; Unsupported method");
78 return nullptr;
79 }
80
81 bool secure = url.find("https:") != std::string::npos;
82
83 esp_http_client_config_t config = {};
84
85 config.url = url.c_str();
86 config.method = method_idf;
87 config.timeout_ms = this->timeout_;
88 config.disable_auto_redirect = !this->follow_redirects_;
89 config.max_redirection_count = this->redirect_limit_;
90 config.auth_type = HTTP_AUTH_TYPE_BASIC;
91#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
92 if (secure && this->verify_ssl_) {
93 config.crt_bundle_attach = esp_crt_bundle_attach;
94 }
95#endif
96
97 if (this->useragent_ != nullptr) {
98 config.user_agent = this->useragent_;
99 }
100
101 config.buffer_size = this->buffer_size_rx_;
102 config.buffer_size_tx = this->buffer_size_tx_;
103
104 const uint32_t start = millis();
106
107 config.event_handler = http_event_handler;
108 auto user_data = UserData{collect_headers, {}};
109 config.user_data = static_cast<void *>(&user_data);
110
111 esp_http_client_handle_t client = esp_http_client_init(&config);
112
113 std::shared_ptr<HttpContainerIDF> container = std::make_shared<HttpContainerIDF>(client);
114 container->set_parent(this);
115
116 container->set_secure(secure);
117
118 for (const auto &header : request_headers) {
119 esp_http_client_set_header(client, header.name.c_str(), header.value.c_str());
120 }
121
122 const int body_len = body.length();
123
124 esp_err_t err = esp_http_client_open(client, body_len);
125 if (err != ESP_OK) {
126 this->status_momentary_error("failed", 1000);
127 ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(err));
128 esp_http_client_cleanup(client);
129 return nullptr;
130 }
131
132 if (body_len > 0) {
133 int write_left = body_len;
134 int write_index = 0;
135 const char *buf = body.c_str();
136 while (write_left > 0) {
137 int written = esp_http_client_write(client, buf + write_index, write_left);
138 if (written < 0) {
139 err = ESP_FAIL;
140 break;
141 }
142 write_left -= written;
143 write_index += written;
144 }
145 }
146
147 if (err != ESP_OK) {
148 this->status_momentary_error("failed", 1000);
149 ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(err));
150 esp_http_client_cleanup(client);
151 return nullptr;
152 }
153
154 container->feed_wdt();
155 // esp_http_client_fetch_headers() returns 0 for chunked transfer encoding (no Content-Length header).
156 // The read() method handles content_length == 0 specially to support chunked responses.
157 container->content_length = esp_http_client_fetch_headers(client);
158 container->set_chunked(esp_http_client_is_chunked_response(client));
159 container->feed_wdt();
160 container->status_code = esp_http_client_get_status_code(client);
161 container->feed_wdt();
162 container->set_response_headers(user_data.response_headers);
163 container->duration_ms = millis() - start;
164 if (is_success(container->status_code)) {
165 return container;
166 }
167
168 if (this->follow_redirects_) {
169 auto num_redirects = this->redirect_limit_;
170 while (is_redirect(container->status_code) && num_redirects > 0) {
171 err = esp_http_client_set_redirection(client);
172 if (err != ESP_OK) {
173 ESP_LOGE(TAG, "esp_http_client_set_redirection failed: %s", esp_err_to_name(err));
174 this->status_momentary_error("failed", 1000);
175 esp_http_client_cleanup(client);
176 return nullptr;
177 }
178#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
179 char redirect_url[256]{};
180 if (esp_http_client_get_url(client, redirect_url, sizeof(redirect_url) - 1) == ESP_OK) {
181 ESP_LOGV(TAG, "redirecting to url: %s", redirect_url);
182 }
183#endif
184 err = esp_http_client_open(client, 0);
185 if (err != ESP_OK) {
186 ESP_LOGE(TAG, "esp_http_client_open failed: %s", esp_err_to_name(err));
187 this->status_momentary_error("failed", 1000);
188 esp_http_client_cleanup(client);
189 return nullptr;
190 }
191
192 container->feed_wdt();
193 container->content_length = esp_http_client_fetch_headers(client);
194 container->set_chunked(esp_http_client_is_chunked_response(client));
195 container->feed_wdt();
196 container->status_code = esp_http_client_get_status_code(client);
197 container->feed_wdt();
198 container->duration_ms = millis() - start;
199 if (is_success(container->status_code)) {
200 return container;
201 }
202
203 num_redirects--;
204 }
205
206 if (num_redirects == 0) {
207 ESP_LOGW(TAG, "Reach redirect limit count=%d", this->redirect_limit_);
208 }
209 }
210
211 ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), container->status_code);
212 this->status_momentary_error("failed", 1000);
213 return container;
214}
215
216// ESP-IDF HTTP read implementation (blocking mode)
217//
218// WARNING: Return values differ from BSD sockets! See http_request.h for full documentation.
219//
220// esp_http_client_read() in blocking mode returns:
221// > 0: bytes read
222// 0: connection closed (end of stream)
223// < 0: error
224//
225// We normalize to HttpContainer::read() contract:
226// > 0: bytes read
227// 0: all content read (only returned when content_length is known and fully read)
228// < 0: error/connection closed
229//
230// Note on chunked transfer encoding:
231// esp_http_client_fetch_headers() returns 0 for chunked responses (no Content-Length header).
232// We handle this by skipping the content_length check when content_length is 0,
233// allowing esp_http_client_read() to handle chunked decoding internally and signal EOF
234// by returning 0.
235int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
236 const uint32_t start = millis();
237 watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
238
239 // Check if we've already read all expected content (non-chunked only)
240 // For chunked responses (content_length == 0), esp_http_client_read() handles EOF
241 if (this->is_read_complete()) {
242 return 0; // All content read successfully
243 }
244
245 this->feed_wdt();
246 int read_len_or_error = esp_http_client_read(this->client_, (char *) buf, max_len);
247 this->feed_wdt();
248
249 this->duration_ms += (millis() - start);
250
251 if (read_len_or_error > 0) {
252 this->bytes_read_ += read_len_or_error;
253 return read_len_or_error;
254 }
255
256 // esp_http_client_read() returns 0 in two cases:
257 // 1. Known content_length: connection closed before all data received (error)
258 // 2. Chunked encoding (content_length == 0): end of stream reached (EOF)
259 // For case 1, returning HTTP_ERROR_CONNECTION_CLOSED is correct.
260 // For case 2, 0 indicates that all chunked data has already been delivered
261 // in previous successful read() calls, so treating this as a closed
262 // connection does not cause any loss of response data.
263 if (read_len_or_error == 0) {
264 return HTTP_ERROR_CONNECTION_CLOSED;
265 }
266
267 // Negative value - error, return the actual error code for debugging
268 return read_len_or_error;
269}
270
272 if (this->client_ == nullptr) {
273 return; // Already cleaned up
274 }
275 watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
276
277 esp_http_client_close(this->client_);
278 esp_http_client_cleanup(this->client_);
279 this->client_ = nullptr;
280}
281
283 // Tests to see if the executing task has a watchdog timer attached
284 if (esp_task_wdt_status(nullptr) == ESP_OK) {
285 App.feed_wdt();
286 }
287}
288
289} // namespace esphome::http_request
290
291#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.
bool is_read_complete() const
Check if all expected content has been read For chunked responses, returns false (completion detected...
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:193
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:25
Application App
Global storage of Application pointer - only one Application can exist.