ESPHome 2026.2.1
Loading...
Searching...
No Matches
http_request.h
Go to the documentation of this file.
1#pragma once
2
3#include <list>
4#include <map>
5#include <memory>
6#include <set>
7#include <utility>
8#include <vector>
9
16#include "esphome/core/log.h"
17
18namespace esphome::http_request {
19
20struct Header {
21 std::string name;
22 std::string value;
23};
24
25// Some common HTTP status codes
53
60inline bool is_redirect(int const status) {
61 switch (status) {
67 return true;
68 default:
69 return false;
70 }
71}
72
81inline bool is_success(int const status) { return status >= HTTP_STATUS_OK && status < HTTP_STATUS_MULTIPLE_CHOICES; }
82
83/*
84 * HTTP Container Read Semantics
85 * =============================
86 *
87 * IMPORTANT: These semantics differ from standard BSD sockets!
88 *
89 * BSD socket read() returns:
90 * > 0: bytes read
91 * == 0: connection closed (EOF)
92 * < 0: error (check errno)
93 *
94 * HttpContainer::read() returns:
95 * > 0: bytes read successfully
96 * == 0: no data available yet OR all content read
97 * (caller should check bytes_read vs content_length)
98 * < 0: error or connection closed (caller should EXIT)
99 * HTTP_ERROR_CONNECTION_CLOSED (-1) = connection closed prematurely
100 * other negative values = platform-specific errors
101 *
102 * Platform behaviors:
103 * - ESP-IDF: blocking reads, 0 only returned when all content read
104 * - Arduino: non-blocking, 0 means "no data yet" or "all content read"
105 *
106 * Chunked responses that complete in a reasonable time work correctly on both
107 * platforms. The limitation below applies only to *streaming* chunked
108 * responses where data arrives slowly over a long period.
109 *
110 * Streaming chunked responses are NOT supported (all platforms):
111 * The read helpers (http_read_loop_result, http_read_fully) block the main
112 * event loop until all response data is received. For streaming responses
113 * where data trickles in slowly (e.g., TTS streaming via ffmpeg proxy),
114 * this starves the event loop on both ESP-IDF and Arduino. If data arrives
115 * just often enough to avoid the caller's timeout, the loop runs
116 * indefinitely. If data stops entirely, ESP-IDF fails with
117 * -ESP_ERR_HTTP_EAGAIN (transport timeout) while Arduino spins with
118 * delay(1) until the caller's timeout fires. Supporting streaming requires
119 * a non-blocking incremental read pattern that yields back to the event
120 * loop between chunks. Components that need streaming should use
121 * esp_http_client directly on a separate FreeRTOS task with
122 * esp_http_client_is_complete_data_received() for completion detection
123 * (see audio_reader.cpp for an example).
124 *
125 * Chunked transfer encoding - platform differences:
126 * - ESP-IDF HttpContainer:
127 * HttpContainerIDF overrides is_read_complete() to call
128 * esp_http_client_is_complete_data_received(), which is the
129 * authoritative completion check for both chunked and non-chunked
130 * transfers. When esp_http_client_read() returns 0 for a completed
131 * chunked response, read() returns 0 and is_read_complete() returns
132 * true, so callers get COMPLETE from http_read_loop_result().
133 *
134 * - Arduino HttpContainer:
135 * Chunked responses are decoded internally (see
136 * HttpContainerArduino::read_chunked_()). When the final chunk arrives,
137 * is_chunked_ is cleared and content_length is set to bytes_read_.
138 * Completion is then detected via is_read_complete(), and a subsequent
139 * read() returns 0 to indicate "all content read" (not
140 * HTTP_ERROR_CONNECTION_CLOSED).
141 *
142 * Use the helper functions below instead of checking return values directly:
143 * - http_read_loop_result(): for manual loops with per-chunk processing
144 * - http_read_fully(): for simple "read N bytes into buffer" operations
145 */
146
149static constexpr int HTTP_ERROR_CONNECTION_CLOSED = -1;
150
152enum class HttpReadStatus : uint8_t {
153 OK,
154 ERROR,
155 TIMEOUT,
156};
157
163
165enum class HttpReadLoopResult : uint8_t {
166 DATA,
167 COMPLETE,
168 RETRY,
169 ERROR,
170 TIMEOUT,
171};
172
179inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time, uint32_t timeout_ms,
180 bool is_read_complete) {
181 if (bytes_read_or_error > 0) {
182 last_data_time = millis();
184 }
185 if (bytes_read_or_error < 0) {
187 }
188 // bytes_read_or_error == 0: either "no data yet" or "all content read"
189 if (is_read_complete) {
191 }
192 if (millis() - last_data_time >= timeout_ms) {
194 }
195 delay(1); // Small delay to prevent tight spinning
197}
198
199class HttpRequestComponent;
200
201class HttpContainer : public Parented<HttpRequestComponent> {
202 public:
203 virtual ~HttpContainer() = default;
204 size_t content_length{0};
205 int status_code{-1};
206 uint32_t duration_ms{0};
207
235 virtual int read(uint8_t *buf, size_t max_len) = 0;
236 virtual void end() = 0;
237
238 void set_secure(bool secure) { this->secure_ = secure; }
239 void set_chunked(bool chunked) { this->is_chunked_ = chunked; }
240
241 size_t get_bytes_read() const { return this->bytes_read_; }
242
249 virtual bool is_read_complete() const {
250 // Per RFC 9112, these responses have no body:
251 // - 1xx (Informational), 204 No Content, 205 Reset Content, 304 Not Modified
252 if ((this->status_code >= 100 && this->status_code < 200) || this->status_code == HTTP_STATUS_NO_CONTENT ||
254 return true;
255 }
256 // For non-chunked responses, complete when bytes_read >= content_length
257 // This handles both Content-Length: 0 and Content-Length: N cases
258 return !this->is_chunked_ && this->bytes_read_ >= this->content_length;
259 }
260
266 std::map<std::string, std::list<std::string>> get_response_headers() { return this->response_headers_; }
267
268 std::string get_response_header(const std::string &header_name);
269
270 protected:
271 size_t bytes_read_{0};
272 bool secure_{false};
273 bool is_chunked_{false};
274 std::map<std::string, std::list<std::string>> response_headers_{};
275};
276
285inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer, size_t total_size, size_t chunk_size,
286 uint32_t timeout_ms) {
287 size_t read_index = 0;
288 uint32_t last_data_time = millis();
289
290 while (read_index < total_size) {
291 int read_bytes_or_error = container->read(buffer + read_index, std::min(chunk_size, total_size - read_index));
292
293 App.feed_wdt();
294 yield();
295
296 auto result = http_read_loop_result(read_bytes_or_error, last_data_time, timeout_ms, container->is_read_complete());
297 if (result == HttpReadLoopResult::RETRY)
298 continue;
299 if (result == HttpReadLoopResult::COMPLETE)
300 break; // Server sent less data than requested, but transfer is complete
301 if (result == HttpReadLoopResult::ERROR)
302 return {HttpReadStatus::ERROR, read_bytes_or_error};
303 if (result == HttpReadLoopResult::TIMEOUT)
304 return {HttpReadStatus::TIMEOUT, 0};
305
306 read_index += read_bytes_or_error;
307 }
308 return {HttpReadStatus::OK, 0};
309}
310
311class HttpRequestResponseTrigger : public Trigger<std::shared_ptr<HttpContainer>, std::string &> {
312 public:
313 void process(const std::shared_ptr<HttpContainer> &container, std::string &response_body) {
314 this->trigger(container, response_body);
315 }
316};
317
319 public:
320 void dump_config() override;
321 float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
322
323 void set_useragent(const char *useragent) { this->useragent_ = useragent; }
324 void set_timeout(uint32_t timeout) { this->timeout_ = timeout; }
325 uint32_t get_timeout() const { return this->timeout_; }
326 void set_watchdog_timeout(uint32_t watchdog_timeout) { this->watchdog_timeout_ = watchdog_timeout; }
327 uint32_t get_watchdog_timeout() const { return this->watchdog_timeout_; }
328 void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; }
329 void set_redirect_limit(uint16_t limit) { this->redirect_limit_ = limit; }
330
331 std::shared_ptr<HttpContainer> get(const std::string &url) { return this->start(url, "GET", "", {}); }
332 std::shared_ptr<HttpContainer> get(const std::string &url, const std::list<Header> &request_headers) {
333 return this->start(url, "GET", "", request_headers);
334 }
335 std::shared_ptr<HttpContainer> get(const std::string &url, const std::list<Header> &request_headers,
336 const std::set<std::string> &collect_headers) {
337 return this->start(url, "GET", "", request_headers, collect_headers);
338 }
339 std::shared_ptr<HttpContainer> post(const std::string &url, const std::string &body) {
340 return this->start(url, "POST", body, {});
341 }
342 std::shared_ptr<HttpContainer> post(const std::string &url, const std::string &body,
343 const std::list<Header> &request_headers) {
344 return this->start(url, "POST", body, request_headers);
345 }
346 std::shared_ptr<HttpContainer> post(const std::string &url, const std::string &body,
347 const std::list<Header> &request_headers,
348 const std::set<std::string> &collect_headers) {
349 return this->start(url, "POST", body, request_headers, collect_headers);
350 }
351
352 std::shared_ptr<HttpContainer> start(const std::string &url, const std::string &method, const std::string &body,
353 const std::list<Header> &request_headers) {
354 return this->start(url, method, body, request_headers, {});
355 }
356
357 std::shared_ptr<HttpContainer> start(const std::string &url, const std::string &method, const std::string &body,
358 const std::list<Header> &request_headers,
359 const std::set<std::string> &collect_headers) {
360 std::set<std::string> lower_case_collect_headers;
361 for (const std::string &collect_header : collect_headers) {
362 lower_case_collect_headers.insert(str_lower_case(collect_header));
363 }
364 return this->perform(url, method, body, request_headers, lower_case_collect_headers);
365 }
366
367 protected:
368 virtual std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method,
369 const std::string &body, const std::list<Header> &request_headers,
370 const std::set<std::string> &collect_headers) = 0;
371 const char *useragent_{nullptr};
373 uint16_t redirect_limit_{};
374 uint32_t timeout_{4500};
375 uint32_t watchdog_timeout_{0};
376};
377
378template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
379 public:
381 TEMPLATABLE_VALUE(std::string, url)
382 TEMPLATABLE_VALUE(const char *, method)
383 TEMPLATABLE_VALUE(std::string, body)
384#ifdef USE_HTTP_REQUEST_RESPONSE
385 TEMPLATABLE_VALUE(bool, capture_response)
386#endif
387
389 this->request_headers_.insert({key, value});
390 }
391
392 void add_collect_header(const char *value) { this->collect_headers_.insert(value); }
393
394 void add_json(const char *key, TemplatableValue<std::string, Ts...> value) { this->json_.insert({key, value}); }
395
396 void set_json(std::function<void(Ts..., JsonObject)> json_func) { this->json_func_ = json_func; }
397
398#ifdef USE_HTTP_REQUEST_RESPONSE
402#endif
404
405 Trigger<Ts...> *get_error_trigger() { return &this->error_trigger_; }
406
407 void set_max_response_buffer_size(size_t max_response_buffer_size) {
408 this->max_response_buffer_size_ = max_response_buffer_size;
409 }
410
411 void play(const Ts &...x) override {
412 std::string body;
413 if (this->body_.has_value()) {
414 body = this->body_.value(x...);
415 }
416 if (!this->json_.empty()) {
417 auto f = std::bind(&HttpRequestSendAction<Ts...>::encode_json_, this, x..., std::placeholders::_1);
418 body = json::build_json(f);
419 }
420 if (this->json_func_ != nullptr) {
421 auto f = std::bind(&HttpRequestSendAction<Ts...>::encode_json_func_, this, x..., std::placeholders::_1);
422 body = json::build_json(f);
423 }
424 std::list<Header> request_headers;
425 for (const auto &item : this->request_headers_) {
426 auto val = item.second;
427 Header header;
428 header.name = item.first;
429 header.value = val.value(x...);
430 request_headers.push_back(header);
431 }
432
433 auto container = this->parent_->start(this->url_.value(x...), this->method_.value(x...), body, request_headers,
434 this->collect_headers_);
435
436 auto captured_args = std::make_tuple(x...);
437
438 if (container == nullptr) {
439 std::apply([this](Ts... captured_args_inner) { this->error_trigger_.trigger(captured_args_inner...); },
440 captured_args);
441 return;
442 }
443
444 size_t max_length = this->max_response_buffer_size_;
445#ifdef USE_HTTP_REQUEST_RESPONSE
446 if (this->capture_response_.value(x...)) {
447 std::string response_body;
448 RAMAllocator<uint8_t> allocator;
449 uint8_t *buf = allocator.allocate(max_length);
450 if (buf != nullptr) {
451 // NOTE: HttpContainer::read() has non-BSD socket semantics - see top of this file
452 // Use http_read_loop_result() helper instead of checking return values directly
453 size_t read_index = 0;
454 uint32_t last_data_time = millis();
455 const uint32_t read_timeout = this->parent_->get_timeout();
456 while (container->get_bytes_read() < max_length) {
457 int read_or_error = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
458 App.feed_wdt();
459 yield();
460 auto result =
461 http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete());
462 if (result == HttpReadLoopResult::RETRY)
463 continue;
464 if (result != HttpReadLoopResult::DATA)
465 break; // COMPLETE, ERROR, or TIMEOUT
466 read_index += read_or_error;
467 }
468 response_body.reserve(read_index);
469 response_body.assign((char *) buf, read_index);
470 allocator.deallocate(buf, max_length);
471 }
472 std::apply(
473 [this, &container, &response_body](Ts... captured_args_inner) {
474 this->success_trigger_with_response_.trigger(container, response_body, captured_args_inner...);
475 },
476 captured_args);
477 } else
478#endif
479 {
480 std::apply([this, &container](
481 Ts... captured_args_inner) { this->success_trigger_.trigger(container, captured_args_inner...); },
482 captured_args);
483 }
484 container->end();
485 }
486
487 protected:
488 void encode_json_(Ts... x, JsonObject root) {
489 for (const auto &item : this->json_) {
490 auto val = item.second;
491 root[item.first] = val.value(x...);
492 }
493 }
494 void encode_json_func_(Ts... x, JsonObject root) { this->json_func_(x..., root); }
496 std::map<const char *, TemplatableValue<const char *, Ts...>> request_headers_{};
497 std::set<std::string> collect_headers_{"content-type", "content-length"};
498 std::map<const char *, TemplatableValue<std::string, Ts...>> json_{};
499 std::function<void(Ts..., JsonObject)> json_func_{nullptr};
500#ifdef USE_HTTP_REQUEST_RESPONSE
502#endif
505
507};
508
509} // namespace esphome::http_request
uint8_t status
Definition bl0942.h:8
void feed_wdt(uint32_t time=0)
Helper class to easily give an object a parent of type T.
Definition helpers.h:1471
An STL allocator that uses SPI or internal RAM.
Definition helpers.h:1647
void deallocate(T *p, size_t n)
Definition helpers.h:1705
T * allocate(size_t n)
Definition helpers.h:1667
std::string get_response_header(const std::string &header_name)
std::map< std::string, std::list< std::string > > get_response_headers()
Get response headers.
virtual int read(uint8_t *buf, size_t max_len)=0
Read data from the HTTP response body.
virtual bool is_read_complete() const
Check if all expected content has been read.
int status_code
-1 indicates no response received yet
std::map< std::string, std::list< std::string > > response_headers_
bool is_chunked_
True if response uses chunked transfer encoding.
std::shared_ptr< HttpContainer > post(const std::string &url, const std::string &body, const std::list< Header > &request_headers, const std::set< std::string > &collect_headers)
std::shared_ptr< HttpContainer > post(const std::string &url, const std::string &body)
std::shared_ptr< HttpContainer > post(const std::string &url, const std::string &body, const std::list< Header > &request_headers)
void set_useragent(const char *useragent)
virtual 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)=0
std::shared_ptr< HttpContainer > start(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)
std::shared_ptr< HttpContainer > get(const std::string &url, const std::list< Header > &request_headers)
void set_follow_redirects(bool follow_redirects)
std::shared_ptr< HttpContainer > start(const std::string &url, const std::string &method, const std::string &body, const std::list< Header > &request_headers)
void set_watchdog_timeout(uint32_t watchdog_timeout)
std::shared_ptr< HttpContainer > get(const std::string &url, const std::list< Header > &request_headers, const std::set< std::string > &collect_headers)
std::shared_ptr< HttpContainer > get(const std::string &url)
void process(const std::shared_ptr< HttpContainer > &container, std::string &response_body)
void set_max_response_buffer_size(size_t max_response_buffer_size)
std::map< const char *, TemplatableValue< std::string, Ts... > > json_
method capture_response void add_request_header(const char *key, TemplatableValue< const char *, Ts... > value)
TEMPLATABLE_VALUE(std::string, url) TEMPLATABLE_VALUE(const char *
Trigger< std::shared_ptr< HttpContainer >, std::string &, Ts... > success_trigger_with_response_
std::function< void(Ts..., JsonObject)> json_func_
Trigger< std::shared_ptr< HttpContainer >, Ts... > success_trigger_
void encode_json_func_(Ts... x, JsonObject root)
void set_json(std::function< void(Ts..., JsonObject)> json_func)
void add_json(const char *key, TemplatableValue< std::string, Ts... > value)
Trigger< std::shared_ptr< HttpContainer >, std::string &, Ts... > * get_success_trigger_with_response()
void encode_json_(Ts... x, JsonObject root)
std::map< const char *, TemplatableValue< const char *, Ts... > > request_headers_
Trigger< std::shared_ptr< HttpContainer >, Ts... > * get_success_trigger()
HttpRequestSendAction(HttpRequestComponent *parent)
mopeka_std_values val[4]
HttpReadLoopResult
Result of processing a non-blocking read with timeout (for manual loops)
@ TIMEOUT
Timeout waiting for data, caller should exit loop.
@ COMPLETE
All content has been read, caller should exit loop.
@ ERROR
Read error, caller should exit loop.
@ RETRY
No data yet, already delayed, caller should continue loop.
@ DATA
Data was read, process it.
bool is_success(int const status)
Checks if the given HTTP status code indicates a successful request.
HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time, uint32_t timeout_ms, bool is_read_complete)
Process a read result with timeout tracking and delay handling.
bool is_redirect(int const status)
Returns true if the HTTP status code is a redirect.
HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer, size_t total_size, size_t chunk_size, uint32_t timeout_ms)
Read data from HTTP container into buffer with timeout handling Handles feed_wdt, yield,...
HttpReadStatus
Status of a read operation.
@ TIMEOUT
Timeout waiting for data.
@ OK
Read completed successfully.
std::string build_json(const json_build_t &f)
Build a JSON string with the provided json build function.
Definition json_util.cpp:18
const float AFTER_WIFI
For components that should be initialized after WiFi is connected.
Definition component.cpp:91
std::string str_lower_case(const std::string &str)
Convert the string to lower case.
Definition helpers.cpp:201
void IRAM_ATTR HOT yield()
Definition core.cpp:24
void IRAM_ATTR HOT delay(uint32_t ms)
Definition core.cpp:26
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:25
Application App
Global storage of Application pointer - only one Application can exist.
Result of an HTTP read operation.
HttpReadStatus status
Status of the read operation.
int error_code
Error code from read() on failure, 0 on success.
uint16_t x
Definition tt21100.cpp:5