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