ESPHome 2026.5.0b1
Loading...
Searching...
No Matches
web_server_idf.cpp
Go to the documentation of this file.
1#ifdef USE_ESP32
2
3#include <cstdarg>
4#include <memory>
5#include <cstring>
6#include <cctype>
7#include <cinttypes>
8
10#include "esphome/core/log.h"
11
12#include "esp_tls_crypto.h"
13#include <freertos/FreeRTOS.h>
14#include <freertos/task.h>
15
16#include "utils.h"
17#include "web_server_idf.h"
18
19#ifdef USE_WEBSERVER_OTA
20#include <multipart_parser.h>
21#include "multipart.h" // For parse_multipart_boundary and other utils
22#endif
23
24#ifdef USE_WEBSERVER
27#endif // USE_WEBSERVER
28
29// Include socket headers after Arduino headers to avoid IPADDR_NONE/INADDR_NONE macro conflicts
30#include <cerrno>
31#include <sys/socket.h>
32
34
35#ifndef HTTPD_409
36#define HTTPD_409 "409 Conflict"
37#endif
38
39#define CRLF_STR "\r\n"
40#define CRLF_LEN (sizeof(CRLF_STR) - 1)
41
42static const char *const TAG = "web_server_idf";
43
44// Global instance to avoid guard variable (saves 8 bytes)
45// This is initialized at program startup before any threads
46namespace {
47// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
48DefaultHeaders default_headers_instance;
49} // namespace
50
51DefaultHeaders &DefaultHeaders::Instance() { return default_headers_instance; }
52
53namespace {
54// Non-blocking send function to prevent watchdog timeouts when TCP buffers are full
69int nonblocking_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_len, int flags) {
70 if (buf == nullptr) {
71 return HTTPD_SOCK_ERR_INVALID;
72 }
73
74 // Use MSG_DONTWAIT to prevent blocking when TCP send buffer is full
75 int ret = send(sockfd, buf, buf_len, flags | MSG_DONTWAIT);
76 if (ret < 0) {
77 const int err = errno;
78 if (err == EAGAIN || err == EWOULDBLOCK) {
79 // Buffer full - retry later
80 return HTTPD_SOCK_ERR_TIMEOUT;
81 }
82 // Real error
83 ESP_LOGD(TAG, "send error: errno %d", err);
84 return HTTPD_SOCK_ERR_FAIL;
85 }
86 return ret;
87}
88} // namespace
89
90void AsyncWebServer::safe_close_with_shutdown(httpd_handle_t hd, int sockfd) {
91 // CRITICAL: Shut down receive BEFORE closing to prevent lwIP race conditions
92 //
93 // The race condition occurs because close() initiates lwIP teardown while
94 // the TCP/IP thread can still receive packets, causing assertions when
95 // recv_tcp() sees partially-torn-down state.
96 //
97 // By shutting down receive first, we tell lwIP to stop accepting new data BEFORE
98 // the teardown begins, eliminating the race window. We only shutdown RD (not RDWR)
99 // to allow the FIN packet to be sent cleanly during close().
100 //
101 // Note: This function may be called with an already-closed socket if the network
102 // stack closed it. In that case, shutdown() will fail but close() is safe to call.
103 //
104 // See: https://github.com/esphome/esphome-webserver/issues/163
105
106 // Attempt shutdown - ignore errors as socket may already be closed
107 shutdown(sockfd, SHUT_RD);
108
109 // Always close - safe even if socket is already closed by network stack
110 close(sockfd);
111}
112
114 if (this->server_) {
115 httpd_stop(this->server_);
116 this->server_ = nullptr;
117 }
118}
119
121 if (this->server_) {
122 this->end();
123 }
124 // Default httpd stack is defined by ESP-IDF. Increase to accommodate SerializationBuffer's
125 // 640-byte stack buffer used by web_server JSON request handlers.
126 httpd_config_t config = HTTPD_DEFAULT_CONFIG();
127 config.stack_size = config.stack_size + 256;
128 config.server_port = this->port_;
129 config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; };
130 // Always enable LRU purging to handle socket exhaustion gracefully.
131 // When max sockets is reached, the oldest connection is closed to make room for new ones.
132 // This prevents "httpd_accept_conn: error in accept (23)" errors.
133 // See: https://github.com/esphome/esphome/issues/12464
134 config.lru_purge_enable = true;
135 // Use custom close function that shuts down before closing to prevent lwIP race conditions
137 if (httpd_start(&this->server_, &config) == ESP_OK) {
138 const httpd_uri_t handler_get = {
139 .uri = "",
140 .method = HTTP_GET,
142 .user_ctx = this,
143 };
144 httpd_register_uri_handler(this->server_, &handler_get);
145
146 const httpd_uri_t handler_post = {
147 .uri = "",
148 .method = HTTP_POST,
150 .user_ctx = this,
151 };
152 httpd_register_uri_handler(this->server_, &handler_post);
153
154 const httpd_uri_t handler_options = {
155 .uri = "",
156 .method = HTTP_OPTIONS,
158 .user_ctx = this,
159 };
160 httpd_register_uri_handler(this->server_, &handler_options);
161 }
162}
163
164esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) {
165 ESP_LOGVV(TAG, "Enter AsyncWebServer::request_post_handler. uri=%s", r->uri);
166 auto content_type = request_get_header(r, "Content-Type");
167
168 if (!request_has_header(r, "Content-Length")) {
169 ESP_LOGW(TAG, "Content length is required for post: %s", r->uri);
170 httpd_resp_send_err(r, HTTPD_411_LENGTH_REQUIRED, nullptr);
171 return ESP_OK;
172 }
173
174 if (content_type.has_value()) {
175 const char *content_type_char = content_type.value().c_str();
176
177 // Check most common case first
178 size_t content_type_len = strlen(content_type_char);
179 if (strcasestr_n(content_type_char, content_type_len, "application/x-www-form-urlencoded") != nullptr) {
180 // Normal form data - proceed with regular handling
181#ifdef USE_WEBSERVER_OTA
182 } else if (strcasestr_n(content_type_char, content_type_len, "multipart/form-data") != nullptr) {
183 auto *server = static_cast<AsyncWebServer *>(r->user_ctx);
184 return server->handle_multipart_upload_(r, content_type_char);
185#endif
186 } else {
187 ESP_LOGW(TAG, "Unsupported content type for POST: %s", content_type_char);
188 // fallback to get handler to support backward compatibility
190 }
191 }
192
193 // Handle regular form data
194 if (r->content_len > CONFIG_HTTPD_MAX_REQ_HDR_LEN) {
195 ESP_LOGW(TAG, "Request size is to big: %zu", r->content_len);
196 httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
197 return ESP_FAIL;
198 }
199
200 std::string post_query;
201 if (r->content_len > 0) {
202 post_query.resize(r->content_len);
203 const int ret = httpd_req_recv(r, &post_query[0], r->content_len + 1);
204 if (ret <= 0) { // 0 return value indicates connection closed
205 if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
206 httpd_resp_send_err(r, HTTPD_408_REQ_TIMEOUT, nullptr);
207 return ESP_ERR_TIMEOUT;
208 }
209 httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
210 return ESP_FAIL;
211 }
212 }
213
214 AsyncWebServerRequest req(r, std::move(post_query));
215 return static_cast<AsyncWebServer *>(r->user_ctx)->request_handler_(&req);
216}
217
218esp_err_t AsyncWebServer::request_handler(httpd_req_t *r) {
219 ESP_LOGVV(TAG, "Enter AsyncWebServer::request_handler. method=%u, uri=%s", r->method, r->uri);
221 return static_cast<AsyncWebServer *>(r->user_ctx)->request_handler_(&req);
222}
223
225 for (auto *handler : this->handlers_) {
226 if (handler->canHandle(request)) {
227 // At now process only basic requests.
228 // OTA requires multipart request support and handleUpload for it
229 handler->handleRequest(request);
230 return ESP_OK;
231 }
232 }
233 if (this->on_not_found_) {
234 this->on_not_found_(request);
235 return ESP_OK;
236 }
237 return ESP_ERR_NOT_FOUND;
238}
239
241 delete this->rsp_;
242 for (auto *param : this->params_) {
243 delete param; // NOLINT(cppcoreguidelines-owning-memory)
244 }
245}
246
247bool AsyncWebServerRequest::hasHeader(const char *name) const { return request_has_header(*this, name); }
248
249optional<std::string> AsyncWebServerRequest::get_header(const char *name) const {
250 return request_get_header(*this, name);
251}
252
253StringRef AsyncWebServerRequest::url_to(std::span<char, URL_BUF_SIZE> buffer) const {
254 const char *uri = this->req_->uri;
255 const char *query_start = strchr(uri, '?');
256 size_t uri_len = query_start ? static_cast<size_t>(query_start - uri) : strlen(uri);
257 size_t copy_len = std::min(uri_len, URL_BUF_SIZE - 1);
258 memcpy(buffer.data(), uri, copy_len);
259 buffer[copy_len] = '\0';
260 // Decode URL-encoded characters in-place (e.g., %20 -> space)
261 size_t decoded_len = url_decode(buffer.data());
262 return StringRef(buffer.data(), decoded_len);
263}
264
265void AsyncWebServerRequest::redirect(const std::string &url) {
266 httpd_resp_set_status(*this, "302 Found");
267 httpd_resp_set_hdr(*this, "Location", url.c_str());
268 httpd_resp_set_hdr(*this, "Connection", "close");
269 httpd_resp_send(*this, nullptr, 0);
270}
271
272void AsyncWebServerRequest::init_response_(AsyncWebServerResponse *rsp, int code, const char *content_type) {
273 // Set status code - use constants for common codes, default to 500 for unknown codes
274 const char *status;
275 switch (code) {
276 case 200:
277 status = HTTPD_200;
278 break;
279 case 404:
280 status = HTTPD_404;
281 break;
282 case 409:
283 status = HTTPD_409;
284 break;
285 default:
286 status = HTTPD_500;
287 break;
288 }
289 httpd_resp_set_status(*this, status);
290
291 if (content_type && *content_type) {
292 httpd_resp_set_type(*this, content_type);
293 }
294 httpd_resp_set_hdr(*this, "Accept-Ranges", "none");
295
296 for (const auto &header : DefaultHeaders::Instance().headers_) {
297 httpd_resp_set_hdr(*this, header.name, header.value);
298 }
299
300 delete this->rsp_;
301 this->rsp_ = rsp;
302}
303
304#ifdef USE_WEBSERVER_AUTH
305bool AsyncWebServerRequest::authenticate(const char *username, const char *password) const {
306 if (username == nullptr || password == nullptr || *username == 0) {
307 return true;
308 }
309 auto auth = this->get_header("Authorization");
310 if (!auth.has_value()) {
311 return false;
312 }
313
314 auto *auth_str = auth.value().c_str();
315
316 const auto auth_prefix_len = sizeof("Basic ") - 1;
317 if (strncmp("Basic ", auth_str, auth_prefix_len) != 0) {
318 ESP_LOGW(TAG, "Only Basic authorization supported yet");
319 return false;
320 }
321
322 // Build user:pass in stack buffer to avoid heap allocation
323 constexpr size_t max_user_info_len = 256;
324 char user_info[max_user_info_len];
325 size_t user_len = strlen(username);
326 size_t pass_len = strlen(password);
327 size_t user_info_len = user_len + 1 + pass_len;
328
329 if (user_info_len >= max_user_info_len) {
330 ESP_LOGW(TAG, "Credentials too long for authentication");
331 return false;
332 }
333
334 memcpy(user_info, username, user_len);
335 user_info[user_len] = ':';
336 memcpy(user_info + user_len + 1, password, pass_len);
337 user_info[user_info_len] = '\0';
338
339 // Base64 output size is ceil(input_len * 4/3) + 1, with input bounded to 256 bytes
340 // max output is ceil(256 * 4/3) + 1 = 343 bytes, use 350 for safety
341 constexpr size_t max_digest_len = 350;
342 char digest[max_digest_len];
343 size_t out;
344 esp_crypto_base64_encode(reinterpret_cast<uint8_t *>(digest), max_digest_len, &out,
345 reinterpret_cast<const uint8_t *>(user_info), user_info_len);
346
347 // Constant-time comparison to avoid timing side channels.
348 // No early return on length mismatch — the length difference is folded
349 // into the accumulator so any mismatch is rejected.
350 const char *provided = auth_str + auth_prefix_len;
351 size_t digest_len = out; // length from esp_crypto_base64_encode
352 // Derive provided_len from the already-sized std::string rather than
353 // rescanning with strlen (avoids attacker-controlled scan length).
354 size_t provided_len = auth.value().size() - auth_prefix_len;
355 // Use full-width XOR so any bit difference in the lengths is preserved
356 // (uint8_t truncation would miss differences in higher bytes, e.g.
357 // digest_len vs digest_len + 256).
358 volatile size_t result = digest_len ^ provided_len;
359 // Iterate over the expected digest length only — the full-width length
360 // XOR above already rejects any length mismatch, and bounding the loop
361 // prevents a long Authorization header from forcing extra work.
362 for (size_t i = 0; i < digest_len; i++) {
363 char provided_ch = (i < provided_len) ? provided[i] : 0;
364 result |= static_cast<uint8_t>(digest[i] ^ provided_ch);
365 }
366 return result == 0;
367}
368
369void AsyncWebServerRequest::requestAuthentication(const char *realm) const {
370 httpd_resp_set_hdr(*this, "Connection", "keep-alive");
371 // Note: realm is never configured in ESPHome, always nullptr -> "Login Required"
372 (void) realm; // Unused - always use default
373 httpd_resp_set_hdr(*this, "WWW-Authenticate", "Basic realm=\"Login Required\"");
374 httpd_resp_send_err(*this, HTTPD_401_UNAUTHORIZED, nullptr);
375}
376#endif
377
379 // Check cache first - only successful lookups are cached
380 for (auto *param : this->params_) {
381 if (param->name() == name) {
382 return param;
383 }
384 }
385
386 // Look up value from query strings
387 auto val = this->find_query_value_(name);
388
389 // Don't cache misses to avoid wasting memory when handlers check for
390 // optional parameters that don't exist in the request
391 if (!val.has_value()) {
392 return nullptr;
393 }
394
395 auto *param = new AsyncWebParameter(name, val.value()); // NOLINT(cppcoreguidelines-owning-memory)
396 this->params_.push_back(param);
397 return param;
398}
399
403template<typename Func>
404static auto search_query_sources(httpd_req_t *req, const std::string &post_query, const char *name, Func func)
405 -> decltype(func(nullptr, size_t{0}, name)) {
406 if (!post_query.empty()) {
407 auto result = func(post_query.c_str(), post_query.size(), name);
408 if (result) {
409 return result;
410 }
411 }
412 // Use httpd API for query length, then access string directly from URI.
413 // http_parser identifies components by offset/length without modifying the URI string.
414 // This is the same pattern used by url_to().
415 auto len = httpd_req_get_url_query_len(req);
416 if (len == 0) {
417 return {};
418 }
419 const char *query = strchr(req->uri, '?');
420 if (query == nullptr) {
421 return {};
422 }
423 query++; // skip '?'
424 return func(query, len, name);
425}
426
427optional<std::string> AsyncWebServerRequest::find_query_value_(const char *name) const {
428 return search_query_sources(this->req_, this->post_query_, name,
429 [](const char *q, size_t len, const char *k) { return query_key_value(q, len, k); });
430}
431
432bool AsyncWebServerRequest::hasArg(const char *name) {
433 return search_query_sources(this->req_, this->post_query_, name, query_has_key);
434}
435
436std::string AsyncWebServerRequest::arg(const char *name) {
437 auto val = this->find_query_value_(name);
438 if (val.has_value()) {
439 return std::move(val.value());
440 }
441 return {};
442}
443
444void AsyncWebServerResponse::addHeader(const char *name, const char *value) {
445 httpd_resp_set_hdr(*this->req_, name, value);
446}
447
448void AsyncResponseStream::print(float value) {
449 // Use stack buffer to avoid temporary string allocation
450 // Size: sign (1) + digits (10) + decimal (1) + precision (6) + exponent (5) + null (1) = 24, use 32 for safety
451 char buf[32];
452 int len = snprintf(buf, sizeof(buf), "%f", value);
453 this->content_.append(buf, len);
454}
455
456void AsyncResponseStream::printf(const char *fmt, ...) {
457 va_list args;
458
459 va_start(args, fmt);
460 const int length = vsnprintf(nullptr, 0, fmt, args);
461 va_end(args);
462
463 std::string str;
464 str.resize(length);
465
466 va_start(args, fmt);
467 vsnprintf(&str[0], length + 1, fmt, args);
468 va_end(args);
469
470 this->print(str);
471}
472
473#ifdef USE_WEBSERVER
475 LockGuard guard{this->pending_mutex_};
476 for (auto *vec : {&this->sessions_, &this->pending_sessions_}) {
477 for (auto *ses : *vec) {
478 delete ses; // NOLINT(cppcoreguidelines-owning-memory)
479 }
480 }
481}
482
484 // Httpd task: set up the live httpd_req_t and park the session; main loop does the rest.
485 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory,clang-analyzer-cplusplus.NewDeleteLeaks)
486 auto *rsp = new AsyncEventSourceResponse(request, this, this->web_server_);
487 {
488 LockGuard guard{this->pending_mutex_};
489 this->pending_sessions_.push_back(rsp);
490 this->has_pending_sessions_.store(true, std::memory_order_release);
491 }
493}
494
495// clang-analyzer traces a false-positive leak path from loop() through
496// adopt_pending_sessions_main_loop_() into start_session_main_loop_() and
497// finally ArduinoJson. Suppress along the entire in-our-code call chain.
498// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks)
500 // Fast path: one atomic load per tick. Slow path is out-of-line on connect.
501 if (this->has_pending_sessions_.load(std::memory_order_acquire)) {
502 this->adopt_pending_sessions_main_loop_();
503 }
504
505 // Clean up dead sessions safely
506 // This follows the ESP-IDF pattern where free_ctx marks resources as dead
507 // and the main loop handles the actual cleanup to avoid race conditions
508 for (size_t i = 0; i < this->sessions_.size();) {
509 auto *ses = this->sessions_[i];
510 // If the session has a dead socket (marked by destroy callback)
511 if (ses->fd_.load() == 0) {
512 // destroy() already logged the close with the fd; don't double-log here.
513 delete ses; // NOLINT(cppcoreguidelines-owning-memory)
514 // Remove by swapping with last element (O(1) removal, order doesn't matter for sessions)
515 this->sessions_[i] = this->sessions_.back();
516 this->sessions_.pop_back();
517 } else {
518 ses->loop();
519 ++i;
520 }
521 }
522 return !this->sessions_.empty();
523}
524
525void AsyncEventSource::adopt_pending_sessions_main_loop_() {
526 std::vector<AsyncEventSourceResponse *> incoming;
527 {
528 LockGuard guard{this->pending_mutex_};
529 incoming.swap(this->pending_sessions_);
530 this->has_pending_sessions_.store(false, std::memory_order_relaxed);
531 }
532 for (auto *rsp : incoming) {
533 // Already disconnected? Drop it; skip on_connect_/session start on a dead session.
534 if (rsp->fd_.load() == 0) {
535 delete rsp; // NOLINT(cppcoreguidelines-owning-memory)
536 continue;
537 }
538 this->sessions_.push_back(rsp);
539 // Prime first so on_connect_ observes a session that has already sent its
540 // initial ping/config/sorting_groups, matching the pre-refactor ordering.
541 rsp->start_session_main_loop_();
542 if (this->on_connect_) {
543 this->on_connect_(rsp);
544 }
545 }
546}
547// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
548
549void AsyncEventSource::try_send_nodefer(const char *message, const char *event, uint32_t id, uint32_t reconnect) {
550 for (auto *ses : this->sessions_) {
551 if (ses->fd_.load() != 0) { // Skip dead sessions
552 ses->try_send_nodefer(message, event, id, reconnect);
553 }
554 }
555}
556
557void AsyncEventSource::deferrable_send_state(void *source, const char *event_type,
558 message_generator_t *message_generator) {
559 // Skip if no connected clients to avoid unnecessary processing
560 if (this->empty())
561 return;
562 for (auto *ses : this->sessions_) {
563 if (ses->fd_.load() != 0) { // Skip dead sessions
564 ses->deferrable_send_state(source, event_type, message_generator);
565 }
566 }
567}
568
572 : server_(server), web_server_(ws), entities_iterator_(ws, server) {
573 // Httpd task only. start_session_main_loop_() handles event_buffer_ / iterator setup.
574 httpd_req_t *req = *request;
575
576 httpd_resp_set_status(req, HTTPD_200);
577 httpd_resp_set_type(req, "text/event-stream");
578 httpd_resp_set_hdr(req, "Cache-Control", "no-cache");
579 httpd_resp_set_hdr(req, "Connection", "keep-alive");
580
581 for (const auto &header : DefaultHeaders::Instance().headers_) {
582 httpd_resp_set_hdr(req, header.name, header.value);
583 }
584
585 httpd_resp_send_chunk(req, CRLF_STR, CRLF_LEN);
586
587 req->sess_ctx = this;
588 req->free_ctx = AsyncEventSourceResponse::destroy;
589
590 this->hd_ = req->handle;
591 this->fd_.store(httpd_req_to_sockfd(req));
592
593 // Use non-blocking send to prevent watchdog timeouts when TCP buffers are full
594 httpd_sess_set_send_override(this->hd_, this->fd_.load(), nonblocking_send);
595}
596
597// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
599 auto *ws = this->web_server_;
600
601 // tcp send buffer is empty on connect, so these should always go through
602 auto message = ws->get_config_json();
603 this->try_send_nodefer(message.c_str(), "ping", millis(), 30000);
604
605#ifdef USE_WEBSERVER_SORTING
606 for (auto &group : ws->sorting_groups_) {
607 json::JsonBuilder builder;
608 JsonObject root = builder.root();
609 root["name"] = group.second.name;
610 root["sorting_weight"] = group.second.weight;
611 message = builder.serialize();
612
613 // a (very) large number of these should be able to be queued initially without defer
614 // since the only thing in the send buffer at this point is the initial ping/config
615 this->try_send_nodefer(message.c_str(), "sorting_group");
616 }
617#endif
618
619 this->entities_iterator_.begin(ws->include_internal_);
620}
621// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
622
624 auto *rsp = static_cast<AsyncEventSourceResponse *>(ptr);
625 int fd = rsp->fd_.exchange(0); // Atomically get and clear fd
626 ESP_LOGD(TAG, "Event source connection closed (fd: %d)", fd);
627 // Mark as dead - will be cleaned up in the main loop
628 // Note: We don't delete or remove from set here to avoid race conditions
629 // httpd will call our custom close_fn (safe_close_with_shutdown) which handles
630 // shutdown() before close() to prevent lwIP race conditions
631}
632
633// helper for allowing only unique entries in the queue
635 DeferredEvent item(source, message_generator);
636
637 // Use range-based for loop instead of std::find_if to reduce template instantiation overhead and binary size
638 for (auto &event : this->deferred_queue_) {
639 if (event == item) {
640 return; // Already in queue, no need to update since items are equal
641 }
642 }
643 this->deferred_queue_.push_back(item);
644}
645
647 while (!deferred_queue_.empty()) {
648 DeferredEvent &de = deferred_queue_.front();
650 if (this->try_send_nodefer(message.c_str(), "state")) {
651 // O(n) but memory efficiency is more important than speed here which is why std::vector was chosen
652 deferred_queue_.erase(deferred_queue_.begin());
653 } else {
654 break;
655 }
656 }
657}
658
660 if (event_buffer_.empty()) {
661 return;
662 }
663 if (event_bytes_sent_ == event_buffer_.size()) {
664 event_buffer_.resize(0);
666 return;
667 }
668
669 size_t remaining = event_buffer_.size() - event_bytes_sent_;
670 int bytes_sent =
671 httpd_socket_send(this->hd_, this->fd_.load(), event_buffer_.c_str() + event_bytes_sent_, remaining, 0);
672 if (bytes_sent == HTTPD_SOCK_ERR_TIMEOUT) {
673 // EAGAIN/EWOULDBLOCK - socket buffer full, try again later
674 // NOTE: Similar logic exists in web_server/web_server.cpp in DeferredUpdateEventSource::process_deferred_queue_()
675 // The implementations differ due to platform-specific APIs (HTTPD_SOCK_ERR_TIMEOUT vs DISCARDED, fd_.store(0) vs
676 // close()), but the failure counting and timeout logic should be kept in sync. If you change this logic, also
677 // update the Arduino implementation.
680 // Too many failures, connection is likely dead
681 ESP_LOGW(TAG, "Closing stuck EventSource connection after %" PRIu16 " failed sends",
683 this->fd_.store(0); // Mark for cleanup
684 this->deferred_queue_.clear();
685 }
686 return;
687 }
688 if (bytes_sent == HTTPD_SOCK_ERR_FAIL) {
689 // Real socket error - connection will be closed by httpd and destroy callback will be called
690 return;
691 }
692 if (bytes_sent <= 0) {
693 // Unexpected error or zero bytes sent
694 ESP_LOGW(TAG, "Unexpected send result: %d", bytes_sent);
695 return;
696 }
697
698 // Successful send - reset failure counter
700 event_bytes_sent_ += bytes_sent;
701
702 // Log partial sends for debugging
703 if (event_bytes_sent_ < event_buffer_.size()) {
704 ESP_LOGV(TAG, "Partial send: %d/%zu bytes (total: %zu/%zu)", bytes_sent, remaining, event_bytes_sent_,
705 event_buffer_.size());
706 }
707
708 if (event_bytes_sent_ == event_buffer_.size()) {
709 event_buffer_.resize(0);
711 }
712}
713
720
721bool AsyncEventSourceResponse::try_send_nodefer(const char *message, const char *event, uint32_t id,
722 uint32_t reconnect) {
723 if (this->fd_.load() == 0) {
724 return false;
725 }
726
728 if (!event_buffer_.empty()) {
729 // there is still pending event data to send first
730 return false;
731 }
732
733 // 8 spaces are standing in for the hexidecimal chunk length to print later
734 const char chunk_len_header[] = " " CRLF_STR;
735 const int chunk_len_header_len = sizeof(chunk_len_header) - 1;
736
737 event_buffer_.append(chunk_len_header);
738
739 // Use stack buffer for formatting numeric fields to avoid temporary string allocations
740 // Size: "retry: " (7) + max uint32 (10 digits) + CRLF (2) + null (1) = 20 bytes, use 32 for safety
741 constexpr size_t num_buf_size = 32;
742 char num_buf[num_buf_size];
743
744 if (reconnect) {
745 int len = snprintf(num_buf, num_buf_size, "retry: %" PRIu32 CRLF_STR, reconnect);
746 event_buffer_.append(num_buf, len);
747 }
748
749 if (id) {
750 int len = snprintf(num_buf, num_buf_size, "id: %" PRIu32 CRLF_STR, id);
751 event_buffer_.append(num_buf, len);
752 }
753
754 if (event && *event) {
755 event_buffer_.append("event: ", sizeof("event: ") - 1);
756 event_buffer_.append(event);
757 event_buffer_.append(CRLF_STR, CRLF_LEN);
758 }
759
760 // Match ESPAsyncWebServer: null message means no data lines and no terminating blank line
761 if (message) {
762 // SSE spec requires each line of a multi-line message to have its own "data:" prefix
763 // Handle \n, \r, and \r\n line endings (matching ESPAsyncWebServer behavior)
764
765 // Fast path: check if message contains any newlines at all
766 // Most SSE messages (JSON state updates) have no newlines
767 const char *first_n = strchr(message, '\n');
768 const char *first_r = strchr(message, '\r');
769
770 if (first_n == nullptr && first_r == nullptr) {
771 // No newlines - fast path (most common case)
772 event_buffer_.append("data: ", sizeof("data: ") - 1);
773 event_buffer_.append(message);
774 event_buffer_.append(CRLF_STR CRLF_STR, CRLF_LEN * 2); // data line + blank line terminator
775 } else {
776 // Has newlines - handle multi-line message
777 const char *line_start = message;
778 size_t msg_len = strlen(message);
779 const char *msg_end = message + msg_len;
780
781 // Reuse the first search results
782 const char *next_n = first_n;
783 const char *next_r = first_r;
784
785 while (line_start <= msg_end) {
786 const char *line_end;
787 const char *next_line;
788
789 if (next_n == nullptr && next_r == nullptr) {
790 // No more line breaks - output remaining text as final line
791 event_buffer_.append("data: ", sizeof("data: ") - 1);
792 event_buffer_.append(line_start);
793 event_buffer_.append(CRLF_STR, CRLF_LEN);
794 break;
795 }
796
797 // Determine line ending type and next line start
798 if (next_n != nullptr && next_r != nullptr) {
799 if (next_r + 1 == next_n) {
800 // \r\n sequence
801 line_end = next_r;
802 next_line = next_n + 1;
803 } else {
804 // Mixed \n and \r - use whichever comes first
805 line_end = (next_r < next_n) ? next_r : next_n;
806 next_line = line_end + 1;
807 }
808 } else if (next_n != nullptr) {
809 // Unix LF
810 line_end = next_n;
811 next_line = next_n + 1;
812 } else {
813 // Old Mac CR
814 line_end = next_r;
815 next_line = next_r + 1;
816 }
817
818 // Output this line
819 event_buffer_.append("data: ", sizeof("data: ") - 1);
820 event_buffer_.append(line_start, line_end - line_start);
821 event_buffer_.append(CRLF_STR, CRLF_LEN);
822
823 line_start = next_line;
824
825 // Check if we've consumed all content
826 if (line_start >= msg_end) {
827 break;
828 }
829
830 // Search for next newlines only in remaining string
831 next_n = strchr(line_start, '\n');
832 next_r = strchr(line_start, '\r');
833 }
834
835 // Terminate message with blank line
836 event_buffer_.append(CRLF_STR, CRLF_LEN);
837 }
838 }
839
840 if (event_buffer_.size() == static_cast<size_t>(chunk_len_header_len)) {
841 // Nothing was added, reset buffer
842 event_buffer_.resize(0);
843 return true;
844 }
845
846 event_buffer_.append(CRLF_STR, CRLF_LEN);
847
848 // chunk length header itself and the final chunk terminating CRLF are not counted as part of the chunk
849 int chunk_len = event_buffer_.size() - CRLF_LEN - chunk_len_header_len;
850 char chunk_len_str[9];
851 snprintf(chunk_len_str, 9, "%08x", chunk_len);
852 std::memcpy(&event_buffer_[0], chunk_len_str, 8);
853
856
857 return true;
858}
859
860void AsyncEventSourceResponse::deferrable_send_state(void *source, const char *event_type,
861 message_generator_t *message_generator) {
862 // allow all json "details_all" to go through before publishing bare state events, this avoids unnamed entries showing
863 // up in the web GUI and reduces event load during initial connect
864 if (!this->entities_iterator_.completed() && 0 != strcmp(event_type, "state_detail_all"))
865 return;
866
867 if (source == nullptr)
868 return;
869 if (event_type == nullptr)
870 return;
871 if (message_generator == nullptr)
872 return;
873
874 if (0 != strcmp(event_type, "state_detail_all") && 0 != strcmp(event_type, "state")) {
875 ESP_LOGE(TAG, "Can't defer non-state event");
876 }
877
880
881 if (!event_buffer_.empty() || !deferred_queue_.empty()) {
882 // outgoing event buffer or deferred queue still not empty which means downstream tcp send buffer full, no point
883 // trying to send first
884 deq_push_back_with_dedup_(source, message_generator);
885 } else {
886 auto message = message_generator(web_server_, source);
887 if (!this->try_send_nodefer(message.c_str(), "state")) {
888 deq_push_back_with_dedup_(source, message_generator);
889 }
890 }
891}
892#endif
893
894#ifdef USE_WEBSERVER_OTA
895esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *content_type) {
896 static constexpr size_t MULTIPART_CHUNK_SIZE = 1460; // Match Arduino AsyncWebServer buffer size
897 static constexpr size_t YIELD_INTERVAL_BYTES = 16 * 1024; // Yield every 16KB to prevent watchdog
898
899 // Parse boundary and create reader
900 const char *boundary_start;
901 size_t boundary_len;
902 if (!parse_multipart_boundary(content_type, &boundary_start, &boundary_len)) {
903 ESP_LOGE(TAG, "Failed to parse multipart boundary");
904 httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
905 return ESP_FAIL;
906 }
907
909 AsyncWebHandler *handler = nullptr;
910 for (auto *h : this->handlers_) {
911 if (h->canHandle(&req)) {
912 handler = h;
913 break;
914 }
915 }
916
917 if (!handler) {
918 ESP_LOGW(TAG, "No handler found for OTA request");
919 httpd_resp_send_err(r, HTTPD_404_NOT_FOUND, nullptr);
920 return ESP_OK;
921 }
922
923 // Upload state
924 std::string filename;
925 size_t index = 0;
926 // Create reader on heap to reduce stack usage
927 auto reader = std::make_unique<MultipartReader>("--" + std::string(boundary_start, boundary_len));
928
929 // Configure callbacks
930 reader->set_data_callback([&](const uint8_t *data, size_t len) {
931 if (!reader->has_file() || !len)
932 return;
933
934 if (filename.empty()) {
935 filename = reader->get_current_part().filename;
936 ESP_LOGV(TAG, "Processing file: '%s'", filename.c_str());
937 handler->handleUpload(&req, filename, 0, nullptr, 0, false); // Start
938 }
939
940 handler->handleUpload(&req, filename, index, const_cast<uint8_t *>(data), len, false);
941 index += len;
942 });
943
944 reader->set_part_complete_callback([&]() {
945 if (index > 0) {
946 handler->handleUpload(&req, filename, index, nullptr, 0, true); // End
947 filename.clear();
948 index = 0;
949 }
950 });
951
952 // Use heap buffer - 1460 bytes is too large for the httpd task stack
953 auto buffer = std::make_unique_for_overwrite<char[]>(MULTIPART_CHUNK_SIZE);
954 size_t bytes_since_yield = 0;
955
956 for (size_t remaining = r->content_len; remaining > 0;) {
957 int recv_len = httpd_req_recv(r, buffer.get(), std::min(remaining, MULTIPART_CHUNK_SIZE));
958
959 if (recv_len <= 0) {
960 httpd_resp_send_err(r, recv_len == HTTPD_SOCK_ERR_TIMEOUT ? HTTPD_408_REQ_TIMEOUT : HTTPD_400_BAD_REQUEST,
961 nullptr);
962 return recv_len == HTTPD_SOCK_ERR_TIMEOUT ? ESP_ERR_TIMEOUT : ESP_FAIL;
963 }
964
965 if (reader->parse(buffer.get(), recv_len) != static_cast<size_t>(recv_len)) {
966 ESP_LOGW(TAG, "Multipart parser error");
967 httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
968 return ESP_FAIL;
969 }
970
971 remaining -= recv_len;
972 bytes_since_yield += recv_len;
973
974 if (bytes_since_yield > YIELD_INTERVAL_BYTES) {
975 vTaskDelay(1);
976 bytes_since_yield = 0;
977 }
978 }
979
980 handler->handleRequest(&req);
981 return ESP_OK;
982}
983#endif // USE_WEBSERVER_OTA
984
985} // namespace esphome::web_server_idf
986
987#endif // !defined(USE_ESP32)
uint8_t h
Definition bl0906.h:2
uint8_t status
Definition bl0942.h:8
void enable_loop_soon_any_context()
Thread and ISR-safe version of enable_loop() that can be called from any context.
void begin(bool include_internal=false)
Helper class that wraps a mutex with a RAII-style API.
Definition helpers.h:1923
StringRef is a reference to a string owned by something else.
Definition string_ref.h:26
Builder class for creating JSON documents without lambdas.
Definition json_util.h:169
SerializationBuffer serialize()
Serialize the JSON document to a SerializationBuffer (stack-first allocation) Uses 512-byte stack buf...
Definition json_util.cpp:69
This class allows users to create a web server with their ESP nodes.
Definition web_server.h:191
json::SerializationBuffer get_config_json()
Return the webserver configuration as JSON.
std::vector< AsyncEventSourceResponse * > pending_sessions_
bool loop()
Returns true if there are sessions remaining (including pending cleanup).
std::vector< AsyncEventSourceResponse * > sessions_
void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator)
esphome::web_server::WebServer * web_server_
void try_send_nodefer(const char *message, const char *event=nullptr, uint32_t id=0, uint32_t reconnect=0)
void handleRequest(AsyncWebServerRequest *request) override
void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator)
esphome::web_server::WebServer * web_server_
void deq_push_back_with_dedup_(void *source, message_generator_t *message_generator)
AsyncEventSourceResponse(const AsyncWebServerRequest *request, esphome::web_server_idf::AsyncEventSource *server, esphome::web_server::WebServer *ws)
esphome::web_server::ListEntitiesIterator entities_iterator_
bool try_send_nodefer(const char *message, const char *event=nullptr, uint32_t id=0, uint32_t reconnect=0)
void printf(const char *fmt,...) __attribute__((format(printf
virtual void handleRequest(AsyncWebServerRequest *request)
virtual void handleUpload(AsyncWebServerRequest *request, const std::string &filename, size_t index, uint8_t *data, size_t len, bool final)
std::function< void(AsyncWebServerRequest *request)> on_not_found_
static esp_err_t request_post_handler(httpd_req_t *r)
std::vector< AsyncWebHandler * > handlers_
esp_err_t request_handler_(AsyncWebServerRequest *request) const
esp_err_t handle_multipart_upload_(httpd_req_t *r, const char *content_type)
static void safe_close_with_shutdown(httpd_handle_t hd, int sockfd)
static esp_err_t request_handler(httpd_req_t *r)
AsyncWebParameter * getParam(const char *name)
optional< std::string > get_header(const char *name) const
StringRef url_to(std::span< char, URL_BUF_SIZE > buffer) const
Write URL (without query string) to buffer, returns StringRef pointing to buffer.
void init_response_(AsyncWebServerResponse *rsp, int code, const char *content_type)
static constexpr size_t URL_BUF_SIZE
Buffer size for url_to()
optional< std::string > find_query_value_(const char *name) const
ESPDEPRECATED("Use url_to() instead. Removed in 2026.9.0", "2026.3.0") std void requestAuthentication(const char *realm=nullptr) const
std::vector< AsyncWebParameter * > params_
void addHeader(const char *name, const char *value)
const char * message
Definition component.cpp:35
uint16_t flags
mopeka_std_values val[3]
const char *const TAG
Definition spi.cpp:7
bool query_has_key(const char *query_url, size_t query_len, const char *key)
Definition utils.cpp:70
json::SerializationBuffer<>(esphome::web_server::WebServer *, void *) message_generator_t
optional< std::string > request_get_header(httpd_req_t *req, const char *name)
Definition utils.cpp:36
bool parse_multipart_boundary(const char *content_type, const char **boundary_start, size_t *boundary_len)
optional< std::string > query_key_value(const char *query_url, size_t query_len, const char *key)
Definition utils.cpp:53
const char * strcasestr_n(const char *haystack, size_t haystack_len, const char *needle)
Definition utils.cpp:93
size_t url_decode(char *str)
Decode URL-encoded string in-place (e.g., %20 -> space, + -> space) Returns the new length of the dec...
Definition utils.cpp:11
bool request_has_header(httpd_req_t *req, const char *name)
Definition utils.cpp:34
const char int const __FlashStringHelper va_list args
Definition log.h:74
va_end(args)
std::string size_t len
size_t size_t const char va_start(args, fmt)
size_t size_t const char * fmt
Definition helpers.h:1039
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:28
static void uint32_t
std::string print()
uint16_t length
Definition tt21100.cpp:0