ESPHome 2026.3.0-dev
Loading...
Searching...
No Matches
http_request_arduino.cpp
Go to the documentation of this file.
2
3#if defined(USE_ARDUINO) && !defined(USE_ESP32)
4
7
10#include "esphome/core/log.h"
11
12// Include BearSSL error constants for TLS failure diagnostics
13#ifdef USE_ESP8266
14#include <bearssl/bearssl_ssl.h>
15#endif
16
17namespace esphome::http_request {
18
19static const char *const TAG = "http_request.arduino";
20#ifdef USE_ESP8266
21static constexpr int RX_BUFFER_SIZE = 512;
22static constexpr int TX_BUFFER_SIZE = 512;
23// ESP8266 Arduino core (WiFiClientSecureBearSSL.cpp) returns -1000 on OOM
24static constexpr int ESP8266_SSL_ERR_OOM = -1000;
25#endif
26
27std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &url, const std::string &method,
28 const std::string &body,
29 const std::vector<Header> &request_headers,
30 const std::vector<std::string> &lower_case_collect_headers) {
31 if (!network::is_connected()) {
32 this->status_momentary_error("failed", 1000);
33 ESP_LOGW(TAG, "HTTP Request failed; Not connected to network");
34 return nullptr;
35 }
36
37 std::shared_ptr<HttpContainerArduino> container = std::make_shared<HttpContainerArduino>();
38 container->set_parent(this);
39
40 const uint32_t start = millis();
41
42 bool secure = url.find("https:") != std::string::npos;
43 container->set_secure(secure);
44
46
47 if (this->follow_redirects_) {
48 container->client_.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS);
49 container->client_.setRedirectLimit(this->redirect_limit_);
50 } else {
51 container->client_.setFollowRedirects(HTTPC_DISABLE_FOLLOW_REDIRECTS);
52 }
53
54#if defined(USE_ESP8266)
55 std::unique_ptr<WiFiClient> stream_ptr;
56#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS
57 if (secure) {
58 ESP_LOGV(TAG, "ESP8266 HTTPS connection with WiFiClientSecure");
59 stream_ptr = std::make_unique<WiFiClientSecure>();
60 WiFiClientSecure *secure_client = static_cast<WiFiClientSecure *>(stream_ptr.get());
61 secure_client->setBufferSizes(RX_BUFFER_SIZE, TX_BUFFER_SIZE);
62 secure_client->setInsecure();
63 } else {
64 stream_ptr = std::make_unique<WiFiClient>();
65 }
66#else
67 ESP_LOGV(TAG, "ESP8266 HTTP connection with WiFiClient");
68 if (secure) {
69 ESP_LOGE(TAG, "Can't use HTTPS connection with esp8266_disable_ssl_support");
70 return nullptr;
71 }
72 stream_ptr = std::make_unique<WiFiClient>();
73#endif // USE_HTTP_REQUEST_ESP8266_HTTPS
74
75#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 1, 0) // && USE_ARDUINO_VERSION_CODE < VERSION_CODE(?, ?, ?)
76 if (!secure) {
77 ESP_LOGW(TAG, "Using HTTP on Arduino version >= 3.1 is **very** slow. Consider setting framework version to 3.0.2 "
78 "in your YAML, or use HTTPS");
79 }
80#endif // USE_ARDUINO_VERSION_CODE
81 bool status = container->client_.begin(*stream_ptr, url.c_str());
82
83#elif defined(USE_RP2040)
84 if (secure) {
85 container->client_.setInsecure();
86 }
87 bool status = container->client_.begin(url.c_str());
88#endif
89
90 App.feed_wdt();
91
92 if (!status) {
93 ESP_LOGW(TAG, "HTTP Request failed; URL: %s", url.c_str());
94 container->end();
95 this->status_momentary_error("failed", 1000);
96 return nullptr;
97 }
98
99 container->client_.setReuse(true);
100 container->client_.setTimeout(this->timeout_);
101
102 if (this->useragent_ != nullptr) {
103 container->client_.setUserAgent(this->useragent_);
104 }
105 for (const auto &header : request_headers) {
106 container->client_.addHeader(header.name.c_str(), header.value.c_str(), false, true);
107 }
108
109 // returned needed headers must be collected before the requests
110 const char *header_keys[lower_case_collect_headers.size()];
111 int index = 0;
112 for (auto const &header_name : lower_case_collect_headers) {
113 header_keys[index++] = header_name.c_str();
114 }
115 container->client_.collectHeaders(header_keys, index);
116
117 App.feed_wdt();
118 container->status_code = container->client_.sendRequest(method.c_str(), body.c_str());
119 App.feed_wdt();
120 if (container->status_code < 0) {
121#if defined(USE_ESP8266) && defined(USE_HTTP_REQUEST_ESP8266_HTTPS)
122 if (secure) {
123 WiFiClientSecure *secure_client = static_cast<WiFiClientSecure *>(stream_ptr.get());
124 int last_error = secure_client->getLastSSLError();
125
126 if (last_error != 0) {
127 const LogString *error_msg;
128 switch (last_error) {
129 case ESP8266_SSL_ERR_OOM:
130 error_msg = LOG_STR("Unable to allocate buffer memory");
131 break;
132 case BR_ERR_TOO_LARGE:
133 error_msg = LOG_STR("Incoming TLS record does not fit in receive buffer (BR_ERR_TOO_LARGE)");
134 break;
135 default:
136 error_msg = LOG_STR("Unknown SSL error");
137 break;
138 }
139 ESP_LOGW(TAG, "SSL failure: %s (Code: %d)", LOG_STR_ARG(error_msg), last_error);
140 if (last_error == ESP8266_SSL_ERR_OOM) {
141 ESP_LOGW(TAG, "Heap free: %u bytes, configured buffer sizes: %u bytes", ESP.getFreeHeap(),
142 static_cast<unsigned int>(RX_BUFFER_SIZE + TX_BUFFER_SIZE));
143 }
144 } else {
145 ESP_LOGW(TAG, "Connection failure with no error code");
146 }
147 }
148#endif
149
150 ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s", url.c_str(),
151 HTTPClient::errorToString(container->status_code).c_str());
152
153 this->status_momentary_error("failed", 1000);
154 container->end();
155 return nullptr;
156 }
157 if (!is_success(container->status_code)) {
158 ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), container->status_code);
159 this->status_momentary_error("failed", 1000);
160 // Still return the container, so it can be used to get the status code and error message
161 }
162
163 container->response_headers_.clear();
164 auto header_count = container->client_.headers();
165 for (int i = 0; i < header_count; i++) {
166 const std::string header_name = str_lower_case(container->client_.headerName(i).c_str());
167 if (should_collect_header(lower_case_collect_headers, header_name)) {
168 std::string header_value = container->client_.header(i).c_str();
169 ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str());
170 container->response_headers_.push_back({header_name, header_value});
171 }
172 }
173
174 // HTTPClient::getSize() returns -1 for chunked transfer encoding (no Content-Length).
175 // When cast to size_t, -1 becomes SIZE_MAX (4294967295 on 32-bit).
176 // The read() method uses a chunked transfer encoding decoder (read_chunked_) to strip
177 // chunk framing and deliver only decoded content. When the final 0-size chunk is received,
178 // is_chunked_ is cleared and content_length is set to the actual decoded size, so
179 // is_read_complete() returns true and callers exit their read loops correctly.
180 int content_length = container->client_.getSize();
181 ESP_LOGD(TAG, "Content-Length: %d", content_length);
182 container->content_length = (size_t) content_length;
183 // -1 (SIZE_MAX when cast to size_t) means chunked transfer encoding
184 container->set_chunked(content_length == -1);
185 container->duration_ms = millis() - start;
186
187 return container;
188}
189
190// Arduino HTTP read implementation
191//
192// WARNING: Return values differ from BSD sockets! See http_request.h for full documentation.
193//
194// Arduino's WiFiClient is inherently non-blocking - available() returns 0 when
195// no data is ready. We use connected() to distinguish "no data yet" from
196// "connection closed".
197//
198// WiFiClient behavior:
199// available() > 0: data ready to read
200// available() == 0 && connected(): no data yet, still connected
201// available() == 0 && !connected(): connection closed
202//
203// We normalize to HttpContainer::read() contract (NOT BSD socket semantics!):
204// > 0: bytes read
205// 0: no data yet, retry <-- NOTE: 0 means retry, NOT EOF!
206// < 0: error/connection closed <-- connection closed returns -1, not 0
207//
208// For chunked transfer encoding, read_chunked_() decodes chunk framing and delivers
209// only the payload data. When the final 0-size chunk is received, it clears is_chunked_
210// and sets content_length = bytes_read_ so is_read_complete() returns true.
211int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
212 const uint32_t start = millis();
213 watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
214
215 WiFiClient *stream_ptr = this->client_.getStreamPtr();
216 if (stream_ptr == nullptr) {
217 ESP_LOGE(TAG, "Stream pointer vanished!");
218 return HTTP_ERROR_CONNECTION_CLOSED;
219 }
220
221 if (this->is_chunked_) {
222 int result = this->read_chunked_(buf, max_len, stream_ptr);
223 this->duration_ms += (millis() - start);
224 if (result > 0) {
225 return result;
226 }
227 // result <= 0: check for completion or errors
228 if (this->is_read_complete()) {
229 return 0; // Chunked transfer complete (final 0-size chunk received)
230 }
231 if (result < 0) {
232 return result; // Stream error during chunk decoding
233 }
234 // read_chunked_ returned 0: no data was available (available() was 0).
235 // This happens when the TCP buffer is empty - either more data is in flight,
236 // or the connection dropped. Arduino's connected() returns false only when
237 // both the remote has closed AND the receive buffer is empty, so any buffered
238 // data is fully drained before we report the drop.
239 if (!stream_ptr->connected()) {
240 return HTTP_ERROR_CONNECTION_CLOSED;
241 }
242 return 0; // No data yet, caller should retry
243 }
244
245 // Non-chunked path
246 int available_data = stream_ptr->available();
247 size_t remaining = (this->content_length > 0) ? (this->content_length - this->bytes_read_) : max_len;
248 int bufsize = std::min(max_len, std::min(remaining, (size_t) available_data));
249
250 if (bufsize == 0) {
251 this->duration_ms += (millis() - start);
252 if (this->is_read_complete()) {
253 return 0; // All content read successfully
254 }
255 if (!stream_ptr->connected()) {
256 return HTTP_ERROR_CONNECTION_CLOSED;
257 }
258 return 0; // No data yet, caller should retry
259 }
260
261 App.feed_wdt();
262 int read_len = stream_ptr->readBytes(buf, bufsize);
263 this->bytes_read_ += read_len;
264
265 this->duration_ms += (millis() - start);
266
267 return read_len;
268}
269
271 if (this->chunk_remaining_ == 0) {
273 this->chunk_remaining_ = 1; // repurpose as at-start-of-line flag
274 } else {
276 }
277}
278
279// Chunked transfer encoding decoder
280//
281// On Arduino, getStreamPtr() returns raw TCP data. For chunked responses, this includes
282// chunk framing (size headers, CRLF delimiters) mixed with payload data. This decoder
283// strips the framing and delivers only decoded content to the caller.
284//
285// Chunk format (RFC 9112 Section 7.1):
286// <hex-size>[;extension]\r\n
287// <data bytes>\r\n
288// ...
289// 0\r\n
290// [trailer-field\r\n]*
291// \r\n
292//
293// Non-blocking: only processes bytes already in the TCP receive buffer.
294// State (chunk_state_, chunk_remaining_) is preserved between calls, so partial
295// chunk headers or split \r\n sequences resume correctly on the next call.
296// Framing bytes (hex sizes, \r\n) may be consumed without producing output;
297// the caller sees 0 and retries via the normal read timeout logic.
298//
299// WiFiClient::read() returns -1 on error despite available() > 0 (connection reset
300// between check and read). On any stream error (c < 0 or readBytes <= 0), we return
301// already-decoded data if any; otherwise HTTP_ERROR_CONNECTION_CLOSED. The error
302// will surface again on the next call since the stream stays broken.
303//
304// Returns: > 0 decoded bytes, 0 no data available, < 0 error
305int HttpContainerArduino::read_chunked_(uint8_t *buf, size_t max_len, WiFiClient *stream) {
306 int total_decoded = 0;
307
308 while (total_decoded < (int) max_len && this->chunk_state_ != ChunkedState::COMPLETE) {
309 // Non-blocking: only process what's already buffered
310 if (stream->available() == 0)
311 break;
312
313 // CHUNK_DATA reads multiple bytes; handle before the single-byte switch
315 // Only read what's available, what fits in buf, and what remains in this chunk
316 size_t to_read =
317 std::min({max_len - (size_t) total_decoded, this->chunk_remaining_, (size_t) stream->available()});
318 if (to_read == 0)
319 break;
320 App.feed_wdt();
321 int read_len = stream->readBytes(buf + total_decoded, to_read);
322 if (read_len <= 0)
323 return total_decoded > 0 ? total_decoded : HTTP_ERROR_CONNECTION_CLOSED;
324 total_decoded += read_len;
325 this->chunk_remaining_ -= read_len;
326 this->bytes_read_ += read_len;
327 if (this->chunk_remaining_ == 0)
329 continue;
330 }
331
332 // All other states consume a single byte
333 int c = stream->read();
334 if (c < 0)
335 return total_decoded > 0 ? total_decoded : HTTP_ERROR_CONNECTION_CLOSED;
336
337 switch (this->chunk_state_) {
338 // Parse hex chunk size, one byte at a time: "<hex>[;ext]\r\n"
339 // Note: if no hex digits are parsed (e.g., bare \r\n), chunk_remaining_ stays 0
340 // and is treated as the final chunk. This is intentionally lenient — on embedded
341 // devices, rejecting malformed framing is less useful than terminating cleanly.
342 // Overflow of chunk_remaining_ from extremely long hex strings (>8 digits on
343 // 32-bit) is not checked; >4GB chunks are unrealistic on embedded targets and
344 // would simply cause fewer bytes to be read from that chunk.
346 if (c == '\n') {
347 // \n terminates the size line; chunk_remaining_ == 0 means last chunk
349 } else {
350 uint8_t hex = parse_hex_char(c);
351 if (hex != INVALID_HEX_CHAR) {
352 this->chunk_remaining_ = (this->chunk_remaining_ << 4) | hex;
353 } else if (c != '\r') {
354 this->chunk_state_ = ChunkedState::CHUNK_HEADER_EXT; // ';' starts extension, skip to \n
355 }
356 }
357 break;
358
359 // Skip chunk extension bytes until \n (e.g., ";name=value\r\n")
361 if (c == '\n') {
363 }
364 break;
365
366 // Consume \r\n trailing each chunk's data
368 if (c == '\n') {
370 this->chunk_remaining_ = 0; // reset for next chunk's hex accumulation
371 }
372 // else: \r is consumed silently, next iteration gets \n
373 break;
374
375 // Consume optional trailer headers and terminating empty line after final chunk.
376 // Per RFC 9112 Section 7.1: "0\r\n" is followed by optional "field\r\n" lines
377 // and a final "\r\n". chunk_remaining_ is repurposed as a flag: 1 = at start
378 // of line (may be the empty terminator), 0 = mid-line (reading a trailer field).
380 if (c == '\n') {
381 if (this->chunk_remaining_ != 0) {
382 this->chunk_state_ = ChunkedState::COMPLETE; // Empty line terminates trailers
383 } else {
384 this->chunk_remaining_ = 1; // End of trailer field, at start of next line
385 }
386 } else if (c != '\r') {
387 this->chunk_remaining_ = 0; // Non-CRLF char: reading a trailer field
388 }
389 // \r doesn't change the flag — it's part of \r\n line endings
390 break;
391
392 default:
393 break;
394 }
395
397 // Clear chunked flag and set content_length to actual decoded size so
398 // is_read_complete() returns true and callers exit their read loops
399 this->is_chunked_ = false;
400 this->content_length = this->bytes_read_;
401 }
402 }
403
404 return total_decoded;
405}
406
408 watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
409 this->client_.end();
410}
411
412} // namespace esphome::http_request
413
414#endif // USE_ARDUINO && !USE_ESP32
uint8_t status
Definition bl0942.h:8
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.
int read_chunked_(uint8_t *buf, size_t max_len, WiFiClient *stream)
Decode chunked transfer encoding from the raw stream.
size_t chunk_remaining_
Bytes remaining in current chunk.
int read(uint8_t *buf, size_t max_len) override
void chunk_header_complete_()
Transition from chunk header to data or trailer based on parsed size.
virtual bool is_read_complete() const
Check if all expected content has been read.
bool is_chunked_
True if response uses chunked transfer encoding.
std::shared_ptr< HttpContainer > perform(const std::string &url, const std::string &method, const std::string &body, const std::vector< Header > &request_headers, const std::vector< std::string > &lower_case_collect_headers) override
std::shared_ptr< HttpContainer > start(const std::string &url, const std::string &method, const std::string &body, const std::vector< Header > &request_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.
@ CHUNK_DATA
Reading chunk data bytes.
@ COMPLETE
Finished: final chunk and trailers consumed.
@ CHUNK_HEADER
Reading hex digits of chunk size.
@ CHUNK_HEADER_EXT
Skipping chunk extensions until .
@ CHUNK_DATA_TRAIL
Skipping \r after chunk data.
@ CHUNK_TRAILER
Consuming trailer headers after final 0-size chunk.
bool is_connected()
Return whether the node is connected to the network (through wifi, eth, ...)
Definition util.cpp:25
std::string str_lower_case(const std::string &str)
Convert the string to lower case.
Definition helpers.cpp:201
constexpr uint8_t parse_hex_char(char c)
Definition helpers.h:1005
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:25
Application App
Global storage of Application pointer - only one Application can exist.