ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
web_server_idf.cpp
Go to the documentation of this file.
1#ifdef USE_ESP32
2
3#include <algorithm>
4#include <cstdarg>
5#include <memory>
6#include <cstdio>
7#include <cstring>
8#include <cctype>
9#include <cinttypes>
10
13#include "esphome/core/log.h"
14
15#include <mbedtls/base64.h>
16#include <freertos/FreeRTOS.h>
17#include <freertos/task.h>
18
19#include "utils.h"
20#include "sse_chunk.h"
21#include "web_server_idf.h"
22
23#ifdef USE_WEBSERVER_AUTH_DIGEST
24#include <esp_random.h>
25#include <esp_rom_md5.h>
26#endif
27
28#ifdef USE_WEBSERVER_OTA
29#include <multipart_parser.h>
30#include "multipart.h" // For parse_multipart_boundary and other utils
31#endif
32
33#ifdef USE_WEBSERVER
36#endif // USE_WEBSERVER
37
38// Include socket headers after Arduino headers to avoid IPADDR_NONE/INADDR_NONE macro conflicts
39#include <cerrno>
40#include <sys/socket.h>
41
43
44// Status strings not provided by esp_http_server.h
45#ifndef HTTPD_401
46#define HTTPD_401 "401 Unauthorized"
47#endif
48#ifndef HTTPD_409
49#define HTTPD_409 "409 Conflict"
50#endif
51#ifndef HTTPD_422
52#define HTTPD_422 "422 Unprocessable Entity"
53#endif
54
55#define CRLF_STR "\r\n"
56#define CRLF_LEN (sizeof(CRLF_STR) - 1)
57
58ESPHOME_LOG_TAG(TAG, "web_server_idf");
59
60// Only prepare_json_() may hold the JSON arena: every other frame in this file is capped below one
61// arena. Measured at -Os on GCC 14; newer toolchains stay checked on purpose, older ones skip it.
62#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 14 && defined(__OPTIMIZE_SIZE__)
63#pragma GCC diagnostic error "-Wstack-usage=2048"
64#endif
65
66// Chunk size for streaming request bodies; matches the Arduino AsyncWebServer buffer size.
67// Buffers of this size must live on the heap - the httpd task stack is too small.
68static constexpr size_t RECV_CHUNK_SIZE = 1460;
69static constexpr size_t YIELD_INTERVAL_BYTES = 16 * 1024; // Yield every 16KB to prevent watchdog
70
71// Global instance to avoid guard variable (saves 8 bytes)
72// This is initialized at program startup before any threads
73namespace {
74// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
75DefaultHeaders default_headers_instance;
76} // namespace
77
78DefaultHeaders &DefaultHeaders::Instance() { return default_headers_instance; }
79
80namespace {
81// Non-blocking send function to prevent watchdog timeouts when TCP buffers are full
96[[maybe_unused]] int nonblocking_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_len, int flags) {
97 if (buf == nullptr) {
98 return HTTPD_SOCK_ERR_INVALID;
99 }
100
101 // Use MSG_DONTWAIT to prevent blocking when TCP send buffer is full
102 int ret = send(sockfd, buf, buf_len, flags | MSG_DONTWAIT);
103 if (ret < 0) {
104 const int err = errno;
105 if (err == EAGAIN || err == EWOULDBLOCK) {
106 // Buffer full - retry later
107 return HTTPD_SOCK_ERR_TIMEOUT;
108 }
109 // Real error
110 ESP_LOGD(TAG, "send error: errno %d", err);
111 return HTTPD_SOCK_ERR_FAIL;
112 }
113 return ret;
114}
115} // namespace
116
117void AsyncWebServer::safe_close_with_shutdown(httpd_handle_t hd, int sockfd) {
118 // CRITICAL: Shut down receive BEFORE closing to prevent lwIP race conditions
119 //
120 // The race condition occurs because close() initiates lwIP teardown while
121 // the TCP/IP thread can still receive packets, causing assertions when
122 // recv_tcp() sees partially-torn-down state.
123 //
124 // By shutting down receive first, we tell lwIP to stop accepting new data BEFORE
125 // the teardown begins, eliminating the race window. We only shutdown RD (not RDWR)
126 // to allow the FIN packet to be sent cleanly during close().
127 //
128 // Note: This function may be called with an already-closed socket if the network
129 // stack closed it. In that case, shutdown() will fail but close() is safe to call.
130 //
131 // See: https://github.com/esphome/esphome-webserver/issues/163
132
133 // Attempt shutdown - ignore errors as socket may already be closed
134 shutdown(sockfd, SHUT_RD);
135
136 // Always close - safe even if socket is already closed by network stack
137 close(sockfd);
138}
139
141 if (this->server_) {
142 httpd_stop(this->server_);
143 this->server_ = nullptr;
144 }
145}
146
148 if (this->server_) {
149 this->end();
150 }
151 // Allow room for SerializationBuffer's 640-byte stack buffer and HTTP request handling.
152 httpd_config_t config = HTTPD_DEFAULT_CONFIG();
153 config.stack_size = 4512;
154 config.server_port = this->port_;
155 config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; };
156 // Always enable LRU purging to handle socket exhaustion gracefully.
157 // When max sockets is reached, the oldest connection is closed to make room for new ones.
158 // This prevents "httpd_accept_conn: error in accept (23)" errors.
159 // See: https://github.com/esphome/esphome/issues/12464
160 config.lru_purge_enable = true;
161 // Use custom close function that shuts down before closing to prevent lwIP race conditions
163 if (httpd_start(&this->server_, &config) == ESP_OK) {
164 const httpd_uri_t handler_get = {
165 .uri = "",
166 .method = HTTP_GET,
168 .user_ctx = this,
169 };
170 httpd_register_uri_handler(this->server_, &handler_get);
171
172 const httpd_uri_t handler_post = {
173 .uri = "",
174 .method = HTTP_POST,
176 .user_ctx = this,
177 };
178 httpd_register_uri_handler(this->server_, &handler_post);
179
180 const httpd_uri_t handler_options = {
181 .uri = "",
182 .method = HTTP_OPTIONS,
184 .user_ctx = this,
185 };
186 httpd_register_uri_handler(this->server_, &handler_options);
187 }
188}
189
190esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) {
191 ESP_LOGVV(TAG, "Enter AsyncWebServer::request_post_handler. uri=%s", r->uri);
192 auto content_type = request_get_header(r, "Content-Type");
193
194 if (!request_has_header(r, "Content-Length")) {
195 ESP_LOGW(TAG, "Content length is required for post: %s", r->uri);
196 httpd_resp_send_err(r, HTTPD_411_LENGTH_REQUIRED, nullptr);
197 return ESP_OK;
198 }
199
200 if (content_type.has_value()) {
201 const char *content_type_char = content_type.value().c_str();
202
203 // Check most common case first
204 size_t content_type_len = strlen(content_type_char);
205 if (strcasestr_n(content_type_char, content_type_len, "application/x-www-form-urlencoded") != nullptr) {
206 // Normal form data - proceed with regular handling
207#ifdef USE_WEBSERVER_OTA
208 } else if (strcasestr_n(content_type_char, content_type_len, "multipart/form-data") != nullptr) {
209 auto *server = static_cast<AsyncWebServer *>(r->user_ctx);
210 return server->handle_multipart_upload_(r, content_type_char);
211#endif
212 } else {
213 // Other content types (e.g. application/json) are delivered raw to a matching
214 // custom handler via handleBody(), like the Arduino AsyncWebServer does
215 auto *server = static_cast<AsyncWebServer *>(r->user_ctx);
216 return server->handle_raw_body_(r, content_type_char);
217 }
218 }
219
220 // Handle regular form data
221 if (r->content_len > CONFIG_HTTPD_MAX_REQ_HDR_LEN) {
222 ESP_LOGW(TAG, "Request size is to big: %zu", r->content_len);
223 httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
224 return ESP_FAIL;
225 }
226
227 std::string post_query;
228 if (r->content_len > 0) {
229 post_query.resize(r->content_len);
230 const int ret = httpd_req_recv(r, &post_query[0], r->content_len + 1);
231 if (ret <= 0) { // 0 return value indicates connection closed
232 if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
233 httpd_resp_send_err(r, HTTPD_408_REQ_TIMEOUT, nullptr);
234 return ESP_ERR_TIMEOUT;
235 }
236 httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
237 return ESP_FAIL;
238 }
239 }
240
241 AsyncWebServerRequest req(r, std::move(post_query));
242 return static_cast<AsyncWebServer *>(r->user_ctx)->request_handler_(&req);
243}
244
245esp_err_t AsyncWebServer::request_handler(httpd_req_t *r) {
246 ESP_LOGVV(TAG, "Enter AsyncWebServer::request_handler. method=%u, uri=%s", r->method, r->uri);
248 return static_cast<AsyncWebServer *>(r->user_ctx)->request_handler_(&req);
249}
250
252 for (auto *handler : this->handlers_) {
253 if (handler->canHandle(request)) {
254 // At now process only basic requests.
255 // OTA requires multipart request support and handleUpload for it
256 handler->handleRequest(request);
257 return ESP_OK;
258 }
259 }
260 if (this->on_not_found_) {
261 this->on_not_found_(request);
262 return ESP_OK;
263 }
264 return ESP_ERR_NOT_FOUND;
265}
266
267esp_err_t AsyncWebServer::handle_raw_body_(httpd_req_t *r, const char *content_type) {
269 AsyncWebHandler *handler = nullptr;
270 for (auto *h : this->handlers_) {
271 if (h->canHandle(&req)) {
272 handler = h;
273 break;
274 }
275 }
276
277 if (handler == nullptr) {
278 ESP_LOGW(TAG, "Unsupported content type for POST: %s", content_type);
279 // fallback to get handler to support backward compatibility
280 return this->request_handler_(&req);
281 }
282
283 const size_t total = r->content_len;
284 if (total > 0) {
285 auto buffer = std::make_unique_for_overwrite<char[]>(RECV_CHUNK_SIZE);
286 size_t bytes_since_yield = 0;
287
288 for (size_t index = 0; index < total;) {
289 int recv_len = httpd_req_recv(r, buffer.get(), std::min(total - index, RECV_CHUNK_SIZE));
290
291 if (recv_len <= 0) {
292 httpd_resp_send_err(r, recv_len == HTTPD_SOCK_ERR_TIMEOUT ? HTTPD_408_REQ_TIMEOUT : HTTPD_400_BAD_REQUEST,
293 nullptr);
294 return recv_len == HTTPD_SOCK_ERR_TIMEOUT ? ESP_ERR_TIMEOUT : ESP_FAIL;
295 }
296
297 handler->handleBody(&req, reinterpret_cast<uint8_t *>(buffer.get()), recv_len, index, total);
298 index += recv_len;
299 bytes_since_yield += recv_len;
300
301 if (bytes_since_yield > YIELD_INTERVAL_BYTES) {
302 vTaskDelay(1);
303 bytes_since_yield = 0;
304 }
305 }
306 }
307
308 handler->handleRequest(&req);
309 return ESP_OK;
310}
311
313#ifdef USE_WEBSERVER_OTA
314 if (this->on_disconnect_)
315 this->on_disconnect_();
316#endif
317 delete this->rsp_;
318 for (auto *param : this->params_) {
319 delete param; // NOLINT(cppcoreguidelines-owning-memory)
320 }
321}
322
323bool AsyncWebServerRequest::hasHeader(const char *name) const { return request_has_header(*this, name); }
324
325optional<std::string> AsyncWebServerRequest::get_header(const char *name) const {
326 return request_get_header(*this, name);
327}
328
329StringRef AsyncWebServerRequest::url_to(std::span<char, URL_BUF_SIZE> buffer) const {
330 const char *uri = this->req_->uri;
331 const char *query_start = strchr(uri, '?');
332 size_t uri_len = query_start ? static_cast<size_t>(query_start - uri) : strlen(uri);
333 size_t copy_len = std::min(uri_len, URL_BUF_SIZE - 1);
334 memcpy(buffer.data(), uri, copy_len);
335 buffer[copy_len] = '\0';
336 // Decode URL-encoded characters in-place (e.g., %20 -> space)
337 size_t decoded_len = url_decode(buffer.data());
338 return StringRef(buffer.data(), decoded_len);
339}
340
341void AsyncWebServerRequest::redirect(const char *url) {
342 httpd_resp_set_status(*this, "302 Found");
343 httpd_resp_set_hdr(*this, "Location", url);
344 httpd_resp_set_hdr(*this, "Connection", "close");
345 httpd_resp_send(*this, nullptr, 0);
346}
347
348void AsyncWebServerRequest::init_response_(AsyncWebServerResponse *rsp, int code, const char *content_type) {
349 // Set status code - use constants for common codes, default to 500 for unknown codes
350 const char *status;
351 switch (code) {
352 case 200:
353 status = HTTPD_200;
354 break;
355 case 204:
356 status = HTTPD_204;
357 break;
358 case 400:
359 status = HTTPD_400;
360 break;
361 case 401:
362 status = HTTPD_401;
363 break;
364 case 404:
365 status = HTTPD_404;
366 break;
367 case 409:
368 status = HTTPD_409;
369 break;
370 case 422:
371 status = HTTPD_422;
372 break;
373 default:
374 status = HTTPD_500;
375 break;
376 }
377 httpd_resp_set_status(*this, status);
378
379 if (content_type && *content_type) {
380 httpd_resp_set_type(*this, content_type);
381 }
382 httpd_resp_set_hdr(*this, "Accept-Ranges", "none");
383
384 for (const auto &header : DefaultHeaders::Instance().headers_) {
385 httpd_resp_set_hdr(*this, header.name, header.value);
386 }
387
388 delete this->rsp_;
389 this->rsp_ = rsp;
390}
391
392#ifdef USE_WEBSERVER_AUTH
393
394#ifdef USE_WEBSERVER_AUTH_DIGEST
395namespace {
396
397// Extract the value of a Digest auth parameter (e.g. "nonce") from the comma-separated
398// parameter list. Values may be quoted or bare. Returns an empty ref when the key is absent.
399// Only whole parameter names match, so "nc" does not match inside "cnonce".
400StringRef digest_param(StringRef params, const char *key) {
401 size_t key_len = strlen(key);
402 const char *base = params.c_str();
403 size_t n = params.size();
404 size_t i = 0;
405 while (i < n) {
406 while (i < n && (base[i] == ' ' || base[i] == ','))
407 i++;
408 size_t name_start = i;
409 while (i < n && base[i] != '=' && base[i] != ',')
410 i++;
411 if (i >= n || base[i] == ',')
412 continue; // token without a '=', skip it
413 size_t name_len = i - name_start;
414 while (name_len > 0 && base[name_start + name_len - 1] == ' ')
415 name_len--;
416 i++; // consume '='
417 const char *val_start;
418 size_t val_len;
419 if (i < n && base[i] == '"') {
420 i++;
421 val_start = base + i;
422 while (i < n && base[i] != '"')
423 i++;
424 val_len = (base + i) - val_start;
425 if (i < n)
426 i++; // consume closing quote
427 } else {
428 val_start = base + i;
429 while (i < n && base[i] != ',')
430 i++;
431 val_len = (base + i) - val_start;
432 }
433 if (name_len == key_len && memcmp(base + name_start, key, key_len) == 0)
434 return StringRef(val_start, val_len);
435 while (i < n && base[i] != ',')
436 i++;
437 }
438 return StringRef();
439}
440
441// Verify an RFC 2617 Digest response. Stateless (the nonce we issued is not tracked), which
442// matches the ESPAsyncWebServer backend used on the Arduino platforms.
443bool check_digest_auth(const char *username, const char *password, const std::string &header, const char *method) {
444 const size_t prefix_len = sizeof("Digest ") - 1;
445 StringRef params(header.c_str() + prefix_len, header.size() - prefix_len);
446
447 if (digest_param(params, "username") != username)
448 return false;
449
450 StringRef realm = digest_param(params, "realm");
451 StringRef nonce = digest_param(params, "nonce");
452 StringRef uri = digest_param(params, "uri");
453 StringRef qop = digest_param(params, "qop");
454 StringRef nc = digest_param(params, "nc");
455 StringRef cnonce = digest_param(params, "cnonce");
456 StringRef response = digest_param(params, "response");
457 if (response.size() != 32)
458 return false;
459
460 // Compute the three MD5 hashes by streaming the pieces straight into the ROM MD5 engine, so
461 // nothing is concatenated on the heap. Each hash is emitted as 32 lowercase hex characters.
462 md5_context_t ctx;
463 uint8_t digest[16];
464
465 // HA1 = MD5(username:realm:password) -- uses the realm the client echoed back.
466 char ha1[33];
467 esp_rom_md5_init(&ctx);
468 esp_rom_md5_update(&ctx, username, strlen(username));
469 esp_rom_md5_update(&ctx, ":", 1);
470 esp_rom_md5_update(&ctx, realm.c_str(), realm.size());
471 esp_rom_md5_update(&ctx, ":", 1);
472 esp_rom_md5_update(&ctx, password, strlen(password));
473 esp_rom_md5_final(digest, &ctx);
474 format_hex_to(ha1, digest, sizeof(digest));
475
476 // HA2 = MD5(method:uri) -- uses the uri the client echoed back.
477 char ha2[33];
478 esp_rom_md5_init(&ctx);
479 esp_rom_md5_update(&ctx, method, strlen(method));
480 esp_rom_md5_update(&ctx, ":", 1);
481 esp_rom_md5_update(&ctx, uri.c_str(), uri.size());
482 esp_rom_md5_final(digest, &ctx);
483 format_hex_to(ha2, digest, sizeof(digest));
484
485 // expected = MD5(HA1:nonce:nc:cnonce:qop:HA2)
486 char expected[33];
487 esp_rom_md5_init(&ctx);
488 esp_rom_md5_update(&ctx, ha1, 32);
489 esp_rom_md5_update(&ctx, ":", 1);
490 esp_rom_md5_update(&ctx, nonce.c_str(), nonce.size());
491 esp_rom_md5_update(&ctx, ":", 1);
492 esp_rom_md5_update(&ctx, nc.c_str(), nc.size());
493 esp_rom_md5_update(&ctx, ":", 1);
494 esp_rom_md5_update(&ctx, cnonce.c_str(), cnonce.size());
495 esp_rom_md5_update(&ctx, ":", 1);
496 esp_rom_md5_update(&ctx, qop.c_str(), qop.size());
497 esp_rom_md5_update(&ctx, ":", 1);
498 esp_rom_md5_update(&ctx, ha2, 32);
499 esp_rom_md5_final(digest, &ctx);
500 format_hex_to(expected, digest, sizeof(digest));
501
502 // Constant-time comparison of the two 32-char hex digests.
503 uint8_t result = 0;
504 for (size_t i = 0; i < 32; i++)
505 result |= static_cast<uint8_t>(expected[i] ^ response[i]);
506 return result == 0;
507}
508
509} // namespace
510#endif // USE_WEBSERVER_AUTH_DIGEST
511
512bool AsyncWebServerRequest::authenticate(const char *username, const char *password) const {
513 if (username == nullptr || password == nullptr || *username == 0) {
514 return true;
515 }
516 auto auth = this->get_header("Authorization");
517 if (!auth.has_value()) {
518 return false;
519 }
520
521 auto *auth_str = auth.value().c_str();
522
523#ifdef USE_WEBSERVER_AUTH_DIGEST
524 // The build fixed the scheme to Digest, so the Basic path is compiled out entirely.
525 const auto auth_prefix_len = sizeof("Digest ") - 1;
526 if (strncmp("Digest ", auth_str, auth_prefix_len) != 0) {
527 ESP_LOGW(TAG, "Only Digest authorization supported");
528 return false;
529 }
530 return check_digest_auth(username, password, auth.value(), http_method_str(this->method()));
531#else
532 const auto auth_prefix_len = sizeof("Basic ") - 1;
533 if (strncmp("Basic ", auth_str, auth_prefix_len) != 0) {
534 ESP_LOGW(TAG, "Only Basic authorization supported");
535 return false;
536 }
537
538 // Build user:pass in stack buffer to avoid heap allocation
539 constexpr size_t max_user_info_len = 256;
540 char user_info[max_user_info_len];
541 size_t user_len = strlen(username);
542 size_t pass_len = strlen(password);
543 size_t user_info_len = user_len + 1 + pass_len;
544
545 if (user_info_len >= max_user_info_len) {
546 ESP_LOGW(TAG, "Credentials too long for authentication");
547 return false;
548 }
549
550 memcpy(user_info, username, user_len);
551 user_info[user_len] = ':';
552 memcpy(user_info + user_len + 1, password, pass_len);
553 user_info[user_info_len] = '\0';
554
555 // Base64 output size is ceil(input_len * 4/3) + 1, with input bounded to 256 bytes
556 // max output is ceil(256 * 4/3) + 1 = 343 bytes, use 350 for safety
557 constexpr size_t max_digest_len = 350;
558 char digest[max_digest_len];
559 size_t out;
560 // The buffer bound above makes failure unreachable; reject rather than
561 // compare against an unwritten digest if that ever changes.
562 if (mbedtls_base64_encode(reinterpret_cast<uint8_t *>(digest), max_digest_len, &out,
563 reinterpret_cast<const uint8_t *>(user_info), user_info_len) != 0) {
564 return false;
565 }
566
567 // Constant-time comparison to avoid timing side channels.
568 // No early return on length mismatch — the length difference is folded
569 // into the accumulator so any mismatch is rejected.
570 const char *provided = auth_str + auth_prefix_len;
571 size_t digest_len = out;
572 // Derive provided_len from the already-sized std::string rather than
573 // rescanning with strlen (avoids attacker-controlled scan length).
574 size_t provided_len = auth.value().size() - auth_prefix_len;
575 // Use full-width XOR so any bit difference in the lengths is preserved
576 // (uint8_t truncation would miss differences in higher bytes, e.g.
577 // digest_len vs digest_len + 256).
578 volatile size_t result = digest_len ^ provided_len;
579 // Iterate over the expected digest length only — the full-width length
580 // XOR above already rejects any length mismatch, and bounding the loop
581 // prevents a long Authorization header from forcing extra work.
582 for (size_t i = 0; i < digest_len; i++) {
583 char provided_ch = (i < provided_len) ? provided[i] : 0;
584 result |= static_cast<uint8_t>(digest[i] ^ provided_ch);
585 }
586 return result == 0;
587#endif // USE_WEBSERVER_AUTH_DIGEST
588}
589
591 httpd_resp_set_hdr(*this, "Connection", "keep-alive");
592#ifdef USE_WEBSERVER_AUTH_DIGEST
593 // Issue a fresh random nonce and opaque. The nonce is not stored, so this is stateless and
594 // does not defend against replay -- its purpose is to keep the password off the wire.
595 // The header value must stay alive until httpd_resp_send_err() below sends it, so the buffer
596 // lives on this stack frame (httpd_resp_set_hdr stores the pointer, it does not copy).
597 uint8_t random_bytes[16];
598 char nonce[33];
599 char opaque[33];
600 char header[160];
601 esp_fill_random(random_bytes, sizeof(random_bytes));
602 format_hex_to(nonce, random_bytes, sizeof(random_bytes));
603 esp_fill_random(random_bytes, sizeof(random_bytes));
604 format_hex_to(opaque, random_bytes, sizeof(random_bytes));
605 snprintf(header, sizeof(header), R"(Digest realm="Login Required", qop="auth", nonce="%s", opaque="%s")", nonce,
606 opaque);
607 httpd_resp_set_hdr(*this, "WWW-Authenticate", header);
608#else
609 httpd_resp_set_hdr(*this, "WWW-Authenticate", "Basic realm=\"Login Required\"");
610#endif // USE_WEBSERVER_AUTH_DIGEST
611 httpd_resp_send_err(*this, HTTPD_401_UNAUTHORIZED, nullptr);
612}
613#endif // USE_WEBSERVER_AUTH
614
616 // Check cache first - only successful lookups are cached
617 for (auto *param : this->params_) {
618 if (param->name() == name) {
619 return param;
620 }
621 }
622
623 // Look up value from query strings
624 auto val = this->find_query_value_(name);
625
626 // Don't cache misses to avoid wasting memory when handlers check for
627 // optional parameters that don't exist in the request
628 if (!val.has_value()) {
629 return nullptr;
630 }
631
632 auto *param = new AsyncWebParameter(name, val.value()); // NOLINT(cppcoreguidelines-owning-memory)
633 this->params_.push_back(param);
634 return param;
635}
636
640template<typename Func>
641static auto search_query_sources(httpd_req_t *req, const std::string &post_query, const char *name, Func func)
642 -> decltype(func(nullptr, size_t{0}, name)) {
643 if (!post_query.empty()) {
644 auto result = func(post_query.c_str(), post_query.size(), name);
645 if (result) {
646 return result;
647 }
648 }
649 // Use httpd API for query length, then access string directly from URI.
650 // http_parser identifies components by offset/length without modifying the URI string.
651 // This is the same pattern used by url_to().
652 auto len = httpd_req_get_url_query_len(req);
653 if (len == 0) {
654 return {};
655 }
656 const char *query = strchr(req->uri, '?');
657 if (query == nullptr) {
658 return {};
659 }
660 query++; // skip '?'
661 return func(query, len, name);
662}
663
664optional<std::string> AsyncWebServerRequest::find_query_value_(const char *name) const {
665 return search_query_sources(this->req_, this->post_query_, name,
666 [](const char *q, size_t len, const char *k) { return query_key_value(q, len, k); });
667}
668
669bool AsyncWebServerRequest::hasArg(const char *name) {
670 return search_query_sources(this->req_, this->post_query_, name, query_has_key);
671}
672
673std::string AsyncWebServerRequest::arg(const char *name) {
674 auto val = this->find_query_value_(name);
675 if (val.has_value()) {
676 return std::move(val.value());
677 }
678 return {};
679}
680
681void AsyncWebServerResponse::addHeader(const char *name, const char *value) {
682 httpd_resp_set_hdr(*this->req_, name, value);
683}
684
685void AsyncResponseStream::print(float value) {
686 // Use stack buffer to avoid temporary string allocation
687 // Size: sign (1) + digits (10) + decimal (1) + precision (6) + exponent (5) + null (1) = 24, use 32 for safety
688 char buf[32];
689 int len = snprintf(buf, sizeof(buf), "%f", value);
690 this->content_.append(buf, len);
691}
692
693void AsyncResponseStream::printf(const char *fmt, ...) {
694 va_list args;
695
696 va_start(args, fmt);
697 const int length = vsnprintf(nullptr, 0, fmt, args);
698 va_end(args);
699
700 std::string str;
701 str.resize(length);
702
703 va_start(args, fmt);
704 vsnprintf(&str[0], length + 1, fmt, args);
705 va_end(args);
706
707 this->print(str);
708}
709
710#ifdef USE_WEBSERVER
712 LockGuard guard{this->pending_mutex_};
713 for (auto *vec : {&this->sessions_, &this->pending_sessions_}) {
714 for (auto *ses : *vec) {
715 delete ses; // NOLINT(cppcoreguidelines-owning-memory)
716 }
717 }
718}
719
721 // Httpd task: set up the live httpd_req_t and park the session; main loop does the rest.
722 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory,clang-analyzer-cplusplus.NewDeleteLeaks)
723 auto *rsp = new AsyncEventSourceResponse(request, this, this->web_server_);
724 {
725 LockGuard guard{this->pending_mutex_};
726 this->pending_sessions_.push_back(rsp);
727 this->has_pending_sessions_.store(true, std::memory_order_release);
728 }
730}
731
732// clang-analyzer traces a false-positive leak path from loop() through
733// adopt_pending_sessions_main_loop_() into start_session_main_loop_() and
734// finally ArduinoJson. Suppress along the entire in-our-code call chain.
735// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks)
737 // Fast path: one atomic load per tick. Slow path is out-of-line on connect.
738 if (this->has_pending_sessions_.load(std::memory_order_acquire)) {
739 this->adopt_pending_sessions_main_loop_();
740 }
741
742 // Clean up dead sessions safely
743 // This follows the ESP-IDF pattern where free_ctx marks resources as dead
744 // and the main loop handles the actual cleanup to avoid race conditions
745 for (size_t i = 0; i < this->sessions_.size();) {
746 auto *ses = this->sessions_[i];
747 // If the session has a dead socket (marked by destroy callback)
748 if (ses->safe_to_delete_()) {
749 // destroy() already logged the close with the fd; don't double-log here.
750 delete ses; // NOLINT(cppcoreguidelines-owning-memory)
751 // Remove by swapping with last element (O(1) removal, order doesn't matter for sessions)
752 this->sessions_[i] = this->sessions_.back();
753 this->sessions_.pop_back();
754 } else {
755 ses->loop();
756 ++i;
757 }
758 }
759 // A session still waiting for httpd to commit its context keeps the loop alive too
760 return !this->sessions_.empty() || this->has_pending_sessions_.load(std::memory_order_acquire);
761}
762
763void AsyncEventSource::adopt_pending_sessions_main_loop_() {
764 std::vector<AsyncEventSourceResponse *> incoming;
765 {
766 LockGuard guard{this->pending_mutex_};
767 incoming.swap(this->pending_sessions_);
768 this->has_pending_sessions_.store(false, std::memory_order_relaxed);
769 }
770 for (auto *rsp : incoming) {
771 // Already disconnected? Drop it; skip session start on a dead session.
772 if (rsp->safe_to_delete_()) {
773 delete rsp; // NOLINT(cppcoreguidelines-owning-memory)
774 continue;
775 }
776 // httpd commits the session context only after the creating handler returns, so stay
777 // pending until then; httpd_req_cleanup() always commits it or calls destroy()
778 if (httpd_sess_get_ctx(rsp->hd_, rsp->fd_.load()) != rsp) {
779 LockGuard guard{this->pending_mutex_};
780 this->pending_sessions_.push_back(rsp);
781 this->has_pending_sessions_.store(true, std::memory_order_release);
782 continue;
783 }
784 this->sessions_.push_back(rsp);
785 rsp->start_session_main_loop_();
786 }
787}
788// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
789
790void AsyncEventSource::try_send_nodefer(const char *message, size_t message_len, const char *event, uint32_t id,
791 uint32_t reconnect) {
792 for (auto *ses : this->sessions_) {
793 if (ses->fd_.load() != 0) { // Skip dead sessions
794 ses->try_send_nodefer(message, message_len, event, id, reconnect);
795 }
796 }
797}
798
799void AsyncEventSource::deferrable_send_state(void *source, bool detail_all, message_generator_t *message_generator) {
800 // Skip if no connected clients to avoid unnecessary processing
801 if (this->empty())
802 return;
803 for (auto *ses : this->sessions_) {
804 if (ses->fd_.load() != 0) { // Skip dead sessions
805 ses->deferrable_send_state(source, detail_all, message_generator);
806 }
807 }
808}
809
813 : server_(server), web_server_(ws), entities_iterator_(ws, server) {
814 // Httpd task only. start_session_main_loop_() sends the greeting and starts the iterator.
815 httpd_req_t *req = *request;
816
817 httpd_resp_set_status(req, HTTPD_200);
818 httpd_resp_set_type(req, "text/event-stream");
819 httpd_resp_set_hdr(req, "Cache-Control", "no-cache");
820 httpd_resp_set_hdr(req, "Connection", "keep-alive");
821
822 for (const auto &header : DefaultHeaders::Instance().headers_) {
823 httpd_resp_set_hdr(req, header.name, header.value);
824 }
825
826 httpd_resp_send_chunk(req, CRLF_STR, CRLF_LEN);
827
828 req->sess_ctx = this;
829 req->free_ctx = AsyncEventSourceResponse::destroy;
830
831 this->hd_ = req->handle;
832 this->fd_.store(httpd_req_to_sockfd(req));
833
834 // Use non-blocking send to prevent watchdog timeouts when TCP buffers are full
835 httpd_sess_set_send_override(this->hd_, this->fd_.load(), nonblocking_send);
836}
837
838// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
840 auto *ws = this->web_server_;
841
842 // The tcp send buffer is empty on connect. A refusal is a closing session or a failed tail
843 // allocation; nothing retries the greeting, so close and let the client reconnect.
844 auto message = ws->get_config_json();
845 if (!this->try_send_nodefer(message.c_str(), message.size(), "ping", millis(), 30000)) {
846 ESP_LOGW(TAG, "Config not sent to fd %d", this->fd_.load());
847 this->request_close_();
848 return;
849 }
850
851#ifdef USE_WEBSERVER_SORTING
852 for (auto &group : ws->sorting_groups_) {
853 json::JsonBuilder builder;
854 JsonObject root = builder.root();
855 root["name"] = json::linked(group.second.name);
856 root["sorting_weight"] = group.second.weight;
857 message = builder.serialize();
858
859 // a (very) large number of these should be able to be queued initially without defer
860 // since the only thing in the send buffer at this point is the initial ping/config.
861 // A refusal means the socket is full or closing; the remaining groups are not sent. The
862 // session stays up on purpose: partial grouping beats a reconnect loop on a slow link.
863 if (!this->try_send_nodefer(message.c_str(), message.size(), "sorting_group")) {
864 ESP_LOGW(TAG, "Sorting groups not sent to fd %d", this->fd_.load());
865 break;
866 }
867 }
868#endif
869
870 this->entities_iterator_.begin(ws->include_internal_);
871}
872// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
873
875 auto *rsp = static_cast<AsyncEventSourceResponse *>(ptr);
876 int fd = rsp->fd_.exchange(0); // Atomically get and clear fd
877 ESP_LOGD(TAG, "Event source connection closed (fd: %d)", fd);
878 // Mark as dead - will be cleaned up in the main loop
879 // Note: We don't delete or remove from set here to avoid race conditions
880 // httpd will call our custom close_fn (safe_close_with_shutdown) which handles
881 // shutdown() before close() to prevent lwIP race conditions
882}
883
884// helper for allowing only unique entries in the queue
886 DeferredEvent item(source, message_generator);
887
888 // Use range-based for loop instead of std::find_if to reduce template instantiation overhead and binary size
889 for (auto &event : this->deferred_queue_) {
890 if (event == item) {
891 return; // Already in queue, no need to update since items are equal
892 }
893 }
894 this->deferred_queue_.push_back(item);
895}
896
898 if (this->close_requested_) {
899 return;
900 }
901 while (!deferred_queue_.empty()) {
902 DeferredEvent &de = deferred_queue_.front();
903 if (this->send_json_(de.source_, de.message_generator_)) {
904 if (this->close_requested_ || deferred_queue_.empty()) {
905 return;
906 }
907 // O(n) but memory efficiency is more important than speed here which is why std::vector was chosen
908 deferred_queue_.erase(deferred_queue_.begin());
909 } else {
910 break;
911 }
912 }
913}
914
916 if (!this->close_requested_) {
917 this->close_requested_ = true;
918 this->deferred_queue_.clear();
919 this->tail_.reset();
920 this->tail_cap_ = 0;
921 this->tail_len_ = 0;
922 this->tail_sent_ = 0;
924 }
925
926 this->process_close_();
927}
928
930 if (!this->close_requested_ || this->close_work_queued_.load(std::memory_order_acquire)) {
931 return;
932 }
933 const int fd = this->fd_.load();
934 if (fd == 0) {
935 return;
936 }
937
939 if (static_cast<int32_t>(now - this->next_close_attempt_ms_) < 0) {
940 return;
941 }
942
943 // Queue an identity-checked shutdown on the HTTPD task. The public
944 // httpd_sess_trigger_close() queues only a reusable fd/session slot and can
945 // therefore close a new client if the original peer disconnects meanwhile.
946 this->close_work_queued_.store(true, std::memory_order_release);
947 const esp_err_t err = httpd_queue_work(this->hd_, &AsyncEventSourceResponse::close_session_work, this);
949 if (err == ESP_OK) {
950 return;
951 }
952
953 this->close_work_queued_.store(false, std::memory_order_release);
954 if (!this->close_retry_warning_logged_) {
955 ESP_LOGW(TAG, "Failed to queue EventSource close (%s); retrying", esp_err_to_name(err));
956 this->close_retry_warning_logged_ = true;
957 }
958}
959
961 // httpd only advances a session's LRU counter when a request arrives, and an event stream receives one
962 // request ever, so the purge that frees a socket for a new client would evict the live streams first.
963 // The HTTPD task owns the session table, so the refresh runs there.
964 // A stalled stream is the socket the purge should take, and a zeroed fd has no session left to refresh.
965 if (this->send_failure_started_ms_ != 0 || this->fd_.load() == 0)
966 return;
969 this->lru_work_queued_.load(std::memory_order_acquire)) {
970 return;
971 }
972 this->last_lru_refresh_ms_ = now; // a full queue also waits for the next interval
973 this->lru_work_queued_.store(true, std::memory_order_release);
974 if (httpd_queue_work(this->hd_, &AsyncEventSourceResponse::refresh_lru_work, this) != ESP_OK) {
975 this->lru_work_queued_.store(false, std::memory_order_release);
976 }
977}
978
980 auto *response = static_cast<AsyncEventSourceResponse *>(arg);
981 const int fd = response->fd_.load();
982 if (response->owns_session_(fd)) {
983 httpd_sess_update_lru_counter(response->hd_, fd);
984 }
985 // Release self only after the HTTPD-task callback has finished every access.
986 response->lru_work_queued_.store(false, std::memory_order_release);
987}
988
990 auto *response = static_cast<AsyncEventSourceResponse *>(arg);
991 const int fd = response->fd_.load();
992 if (response->owns_session_(fd)) {
993 // The HTTPD task remains the session owner. Shutting the socket down makes
994 // its next select/recv path delete the session and invoke destroy().
995 shutdown(fd, SHUT_RDWR);
996 }
997
998 // Release self only after the HTTPD-task callback has finished every access.
999 response->close_work_queued_.store(false, std::memory_order_release);
1000}
1001
1003 // httpd frees a session before closing its socket, so the fd may already be a new client's.
1004 // Treated as would-block; the stall timer ends a session that never becomes ours again.
1005 const int fd = this->fd_.load();
1006 if (!this->owns_session_(fd)) {
1007 return 0;
1008 }
1009 struct msghdr msg{};
1010 msg.msg_iov = iov;
1011 msg.msg_iovlen = iovcnt;
1012 const ssize_t sent = sendmsg(fd, &msg, MSG_DONTWAIT);
1013 if (sent >= 0) {
1014 return sent;
1015 }
1016 const int err = errno;
1017 if (err == EAGAIN || err == EWOULDBLOCK) {
1018 return 0;
1019 }
1020 ESP_LOGD(TAG, "send error: errno %d", err);
1021 this->request_close_();
1022 return -1;
1023}
1024
1026 if (this->sending_ || this->close_requested_ || this->tail_len_ == 0) {
1027 return;
1028 }
1029 SendGuard guard{*this};
1030
1031 const size_t remaining = this->tail_len_ - this->tail_sent_;
1032 struct iovec iov = {this->tail_.get() + this->tail_sent_, remaining};
1033 const ssize_t sent = this->send_(&iov, 1);
1034 if (sent < 0) {
1035 return;
1036 }
1037 if (sent == 0) {
1038 // Socket buffer full, try again later
1039 // NOTE: Similar logic exists in web_server/web_server.cpp in DeferredUpdateEventSource::process_deferred_queue_().
1040 // The IDF path is intentionally time-based and closes through HTTPD to preserve session ownership.
1042 if (this->send_failure_started_ms_ == 0) {
1043 this->send_failure_started_ms_ = now != 0 ? now : 1; // Reserve zero for no stall.
1044 }
1045 if (static_cast<int32_t>(now - (this->send_failure_started_ms_ + SEND_STALL_TIMEOUT_MS)) >= 0) {
1046 ESP_LOGW(TAG, "Closing stuck EventSource connection after %" PRIu32 " ms without send progress",
1047 now - this->send_failure_started_ms_);
1048 this->request_close_();
1049 }
1050 return;
1051 }
1052
1053 this->send_failure_started_ms_ = 0;
1054 this->tail_sent_ += sent;
1055 if (this->tail_sent_ < this->tail_len_) {
1056 ESP_LOGV(TAG, "Partial send: %zd/%zu bytes (total: %u/%u)", sent, remaining, this->tail_sent_, this->tail_len_);
1057 return;
1058 }
1059 // Fully sent; the storage stays for the next stall
1060 this->tail_len_ = 0;
1061}
1062
1064 if (this->tail_cap_ >= len) {
1065 return true;
1066 }
1067 if (len > TAIL_MAX_SIZE) {
1068 return false;
1069 }
1070 // Nothing is pending while the tail grows, so free the old block first. PREFER_INTERNAL keeps
1071 // the tail where plain new put it.
1072 this->tail_.reset();
1074 this->tail_cap_ = this->tail_ ? len : 0;
1075 return this->tail_cap_ != 0;
1076}
1077
1078bool AsyncEventSourceResponse::stash_chunk_(const char *prefix, size_t prefix_len, const char *message,
1079 size_t message_len, size_t total, size_t sent) {
1080 // A log event of nothing but line breaks is the largest chunk; reserve_tail_() must never
1081 // refuse it for size, only for memory
1083 "the log cut in web_server.h must keep a worst case log event inside the tail ceiling");
1084 if (!this->reserve_tail_(total)) {
1085 if (sent != 0) {
1086 // Part of the chunk is on the wire, so the stream is broken and the client has to go
1087 ESP_LOGW(TAG, "Cannot buffer a %zu byte chunk, closing", total);
1088 this->request_close_();
1089 } else {
1090 this->tail_alloc_failed_(total); // nothing on the wire, the caller retries on the stall clock
1091 }
1092 return false;
1093 }
1094 uint8_t *dst = this->tail_.get();
1095 std::memcpy(dst, prefix, prefix_len);
1096 dst += prefix_len;
1098 message, message_len,
1099 [](void *ctx, const char *piece, size_t len) {
1100 auto &out = *static_cast<uint8_t **>(ctx);
1101 std::memcpy(out, piece, len);
1102 out += len;
1103 },
1104 &dst);
1105 this->tail_len_ = total;
1106 this->tail_sent_ = sent;
1107 return true;
1108}
1109
1111 if (this->close_requested_) {
1112 this->process_close_();
1113 return;
1114 }
1115 this->refresh_lru_();
1116 drain_tail_();
1118 if (this->close_requested_)
1119 return;
1120 // One step per loop; refusals retry next pass
1122}
1123
1124#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 14 && defined(__OPTIMIZE_SIZE__)
1125#pragma GCC diagnostic push
1126#pragma GCC diagnostic ignored "-Wstack-usage=" // the one frame that holds the JSON arena
1127#endif
1128AsyncEventSourceResponse::JsonPrepareResult AsyncEventSourceResponse::prepare_json_(void *source,
1129 message_generator_t *generator,
1130 char *buffer, size_t *size_out) {
1131 // Log lines emitted while the generator runs are dropped for this session: on the large path they would fill
1132 // tail_ before it is overwritten, and on the small path they would go out ahead of the event being built.
1133 SendGuard guard{*this};
1135 json::JsonBuilder builder(&arena);
1136 generator(this->web_server_, source, builder);
1137 *size_out = builder.serialize_to(buffer, JSON_BUF_SIZE);
1138 if (*size_out < JSON_BUF_SIZE) {
1140 }
1141
1142 char prefix[PREFIX_BUF_SIZE];
1143 const size_t prefix_len = build_chunk_prefix(prefix, sizeof(prefix), "state", 0, 0, true);
1144
1145 // Nothing reaches the wire until the complete document and its framing are owned by the caller.
1146 size_t json_len = 0;
1147 size_t cap = std::max<size_t>(JSON_BUF_SIZE * 2, this->tail_cap_);
1148 for (;;) {
1149 if (!this->reserve_tail_(cap)) {
1150 *size_out = cap;
1152 }
1153 const size_t room = cap - prefix_len - SSE_SUFFIX_LEN;
1154 json_len = builder.serialize_to(reinterpret_cast<char *>(this->tail_.get()) + prefix_len, room);
1155 if (json_len < room) {
1156 break;
1157 }
1158 if (cap >= TAIL_MAX_SIZE) {
1159 *size_out = room;
1161 }
1162 cap = std::min<size_t>(cap * 2, TAIL_MAX_SIZE);
1163 }
1164
1165 const size_t total = prefix_len + json_len + SSE_SUFFIX_LEN;
1167 uint8_t *dst = this->tail_.get();
1168 std::memcpy(dst, prefix, prefix_len);
1169 std::memcpy(dst + prefix_len + json_len, SSE_SUFFIX, SSE_SUFFIX_LEN);
1170 this->tail_len_ = total;
1171 this->tail_sent_ = 0;
1173}
1174#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 14 && defined(__OPTIMIZE_SIZE__)
1175#pragma GCC diagnostic pop
1176#endif
1177
1179 if (!this->ready_to_send_()) {
1180 return false;
1181 }
1182 char buffer[JSON_BUF_SIZE];
1183 size_t size;
1184 const auto result = this->prepare_json_(source, generator, buffer, &size);
1185 switch (result) {
1187 return this->try_send_nodefer(buffer, size, "state");
1189 this->drain_tail_();
1190 return true;
1192 SendGuard guard{*this};
1193 this->tail_alloc_failed_(size);
1194 return false;
1195 }
1197 SendGuard guard{*this};
1198 ESP_LOGW(TAG, "State event does not fit %zu bytes, dropped", size);
1199 this->tail_.reset();
1200 this->tail_cap_ = 0;
1201 this->send_failure_started_ms_ = 0;
1202 return true;
1203 }
1204 }
1205 return false;
1206}
1207
1209 // Same stall clock as a socket that stops draining, so a session cannot retry forever
1211 if (this->send_failure_started_ms_ == 0) {
1212 this->send_failure_started_ms_ = now != 0 ? now : 1; // Reserve zero for no stall.
1213 ESP_LOGW(TAG, "No memory for a %zu byte chunk", cap);
1214 return;
1215 }
1216 if (static_cast<int32_t>(now - (this->send_failure_started_ms_ + SEND_STALL_TIMEOUT_MS)) >= 0) {
1217 ESP_LOGW(TAG, "Closing EventSource after %" PRIu32 " ms without memory", now - this->send_failure_started_ms_);
1218 this->request_close_();
1219 }
1220}
1221
1223 if (this->sending_ || this->fd_.load() == 0 || this->close_requested_) {
1224 return false;
1225 }
1226 drain_tail_();
1227 return !this->close_requested_ && this->tail_len_ == 0;
1228}
1229
1230bool AsyncEventSourceResponse::try_send_nodefer(const char *message, size_t message_len, const char *event, uint32_t id,
1231 uint32_t reconnect) {
1232 if (!this->ready_to_send_()) {
1233 return false;
1234 }
1235 SendGuard guard{*this};
1236
1237 // Everything after the prefix goes out straight from the caller's buffer
1238 char prefix[PREFIX_BUF_SIZE];
1239 const size_t prefix_len = build_chunk_prefix(prefix, sizeof(prefix), event, id, reconnect, message != nullptr);
1240 if (message == nullptr && prefix_len == CHUNK_HDR_LEN) {
1241 return true; // Match ESPAsyncWebServer: nothing to send
1242 }
1243 if (prefix_len >= PREFIX_BUF_SIZE - 1) {
1244 // The appenders truncate silently, which would put a malformed event on the wire
1245 ESP_LOGW(TAG, "Event name too long, dropped");
1246 return true;
1247 }
1248
1249 // Gather list: the prefix, then the data lines and their separators from the message
1250 struct Gather {
1251 struct iovec iov[MAX_SEND_IOV]; // left uninitialized on purpose
1252 int iovcnt{1};
1253 size_t total{0};
1254 bool fits{true};
1255 } g;
1256 g.total = prefix_len;
1258 message, message_len,
1259 [](void *ctx, const char *piece, size_t len) {
1260 auto &g = *static_cast<Gather *>(ctx);
1261 g.total += len;
1262 if (len == 0) {
1263 return;
1264 }
1265 if (g.iovcnt == MAX_SEND_IOV) {
1266 g.fits = false;
1267 return;
1268 }
1269 g.iov[g.iovcnt++] = {const_cast<char *>(piece), len};
1270 },
1271 &g);
1272 // The header and the terminator are not part of the chunk length
1273 write_chunk_header(prefix, g.total - CHUNK_HDR_LEN - CHUNK_END_LEN);
1274 g.iov[0] = {prefix, prefix_len};
1275
1276 // A message with more lines than the list holds skips straight to the tail
1277 const ssize_t sent = g.fits ? this->send_(g.iov, g.iovcnt) : 0;
1278 if (sent < 0) {
1279 return false;
1280 }
1281 if (static_cast<size_t>(sent) == g.total) {
1282 this->send_failure_started_ms_ = 0; // progress, whichever stall clock was running
1283 return true;
1284 }
1285 // The caller's buffers do not outlive this call, so keep the chunk and continue from loop()
1286 return this->stash_chunk_(prefix, prefix_len, message, message_len, g.total, sent);
1287}
1288
1289void AsyncEventSourceResponse::deferrable_send_state(void *source, bool detail_all,
1290 message_generator_t *message_generator) {
1291 // Let the detail_all listing finish before bare state events, so the web UI never shows unnamed entries and
1292 // initial connect sends less
1293 if (!detail_all && !this->entities_iterator_.completed())
1294 return;
1295
1296 drain_tail_();
1298
1299 if (this->close_requested_) {
1300 return;
1301 }
1302
1303 if (this->tail_len_ != 0 || !deferred_queue_.empty()) {
1304 // outgoing event buffer or deferred queue still not empty which means downstream tcp send buffer full, no point
1305 // trying to send first
1306 deq_push_back_with_dedup_(source, message_generator);
1307 } else {
1308 // A send error closes the session and clears the queue; nothing is queued after that
1309 if (!this->send_json_(source, message_generator) && !this->close_requested_) {
1310 deq_push_back_with_dedup_(source, message_generator);
1311 }
1312 }
1313}
1314#endif
1315
1316#ifdef USE_WEBSERVER_OTA
1317esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *content_type) {
1318 // Parse boundary and create reader
1319 const char *boundary_start;
1320 size_t boundary_len;
1321 if (!parse_multipart_boundary(content_type, &boundary_start, &boundary_len)) {
1322 ESP_LOGE(TAG, "Failed to parse multipart boundary");
1323 httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
1324 return ESP_FAIL;
1325 }
1326
1327 AsyncWebServerRequest req(r);
1328 AsyncWebHandler *handler = nullptr;
1329 for (auto *h : this->handlers_) {
1330 if (h->canHandle(&req)) {
1331 handler = h;
1332 break;
1333 }
1334 }
1335
1336 if (!handler) {
1337 ESP_LOGW(TAG, "No handler found for OTA request");
1338 httpd_resp_send_err(r, HTTPD_404_NOT_FOUND, nullptr);
1339 return ESP_OK;
1340 }
1341
1342 // Upload state
1343 std::string filename;
1344 size_t index = 0;
1345 // Create reader on heap to reduce stack usage
1346 auto reader = std::make_unique<MultipartReader>("--" + std::string(boundary_start, boundary_len));
1347
1348 // Configure callbacks
1349 reader->set_data_callback([&](const uint8_t *data, size_t len) {
1350 if (!reader->has_file() || !len)
1351 return;
1352
1353 if (filename.empty()) {
1354 filename = reader->get_current_part().filename;
1355 ESP_LOGV(TAG, "Processing file: '%s'", filename.c_str());
1356 handler->handleUpload(&req, filename, 0, nullptr, 0, false); // Start
1357 }
1358
1359 handler->handleUpload(&req, filename, index, const_cast<uint8_t *>(data), len, false);
1360 index += len;
1361 });
1362
1363 reader->set_part_complete_callback([&]() {
1364 if (index > 0) {
1365 handler->handleUpload(&req, filename, index, nullptr, 0, true); // End
1366 filename.clear();
1367 index = 0;
1368 }
1369 });
1370
1371 auto buffer = std::make_unique_for_overwrite<char[]>(RECV_CHUNK_SIZE);
1372 size_t bytes_since_yield = 0;
1373
1374 for (size_t remaining = r->content_len; remaining > 0;) {
1375 int recv_len = httpd_req_recv(r, buffer.get(), std::min(remaining, RECV_CHUNK_SIZE));
1376
1377 if (recv_len <= 0) {
1378 httpd_resp_send_err(r, recv_len == HTTPD_SOCK_ERR_TIMEOUT ? HTTPD_408_REQ_TIMEOUT : HTTPD_400_BAD_REQUEST,
1379 nullptr);
1380 return recv_len == HTTPD_SOCK_ERR_TIMEOUT ? ESP_ERR_TIMEOUT : ESP_FAIL;
1381 }
1382
1383 if (reader->parse(buffer.get(), recv_len) != static_cast<size_t>(recv_len)) {
1384 ESP_LOGW(TAG, "Multipart parser error");
1385 httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
1386 return ESP_FAIL;
1387 }
1388
1389 remaining -= recv_len;
1390 bytes_since_yield += recv_len;
1391
1392 if (bytes_since_yield > YIELD_INTERVAL_BYTES) {
1393 vTaskDelay(1);
1394 bytes_since_yield = 0;
1395 }
1396 }
1397
1398 handler->handleRequest(&req);
1399 return ESP_OK;
1400}
1401#endif // USE_WEBSERVER_OTA
1402
1403} // namespace esphome::web_server_idf
1404
1405#endif // !defined(USE_ESP32)
uint8_t h
Definition bl0906.h:2
uint8_t status
Definition bl0942.h:8
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
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)
ESPHOME_ALWAYS_INLINE void try_advance(size_t max_steps)
Run up to max_steps iteration steps; stops early when iteration completes or a callback refuses (that...
Helper class that wraps a mutex with a RAII-style API.
Definition helpers.h:2135
An STL allocator that uses SPI or internal RAM.
Definition helpers.h:2298
RAMUniquePtr< T[]> make_unique_array_for_overwrite(size_t n)
n elements left uninitialized, as std::make_unique_for_overwrite does; empty on exhaustion,...
Definition helpers.h:2371
StringRef is a reference to a string owned by something else.
Definition string_ref.h:31
constexpr const char * c_str() const
Definition string_ref.h:78
constexpr size_type size() const
Definition string_ref.h:79
Bump allocator over a fixed buffer for a document built and serialized in one scope.
Definition json_util.h:194
Builder class for creating JSON documents without lambdas.
Definition json_util.h:256
SerializationBuffer serialize()
Serialize the JSON document to a SerializationBuffer (stack-first allocation) Uses 512-byte stack buf...
Definition json_util.cpp:96
This class allows users to create a web server with their ESP nodes.
Definition web_server.h:201
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 try_send_nodefer(const char *message, size_t message_len, const char *event=nullptr, uint32_t id=0, uint32_t reconnect=0)
esphome::web_server::WebServer * web_server_
void handleRequest(AsyncWebServerRequest *request) override
void deferrable_send_state(void *source, bool detail_all, message_generator_t *message_generator)
void deferrable_send_state(void *source, bool detail_all, message_generator_t *message_generator)
bool try_send_nodefer(const char *message, size_t message_len, const char *event=nullptr, uint32_t id=0, uint32_t reconnect=0)
JsonPrepareResult message_generator_t char * buffer
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)
JsonPrepareResult message_generator_t * generator
JsonPrepareResult message_generator_t char size_t * size_out
ssize_t send_(struct iovec *iov, int iovcnt)
bool stash_chunk_(const char *prefix, size_t prefix_len, const char *message, size_t message_len, size_t total, size_t sent)
esphome::web_server::ListEntitiesIterator entities_iterator_
bool send_json_(void *source, message_generator_t *generator)
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)
virtual void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total)
std::function< void(AsyncWebServerRequest *request)> on_not_found_
esp_err_t handle_raw_body_(httpd_req_t *r, const char *content_type)
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
bool authenticate(const char *username, const char *password) const
std::vector< AsyncWebParameter * > params_
void addHeader(const char *name, const char *value)
const LogString * message
Definition component.cpp:35
uint16_t flags
const char * key
std::vector< uint8_t > & out
Definition gif.cpp:49
uint32_t buffer
Definition gif.cpp:50
__int64 ssize_t
Definition httplib.h:178
int ret
mopeka_std_values val[3]
JsonString linked(const char *s)
Definition json_util.h:24
constexpr size_t CHUNK_END_LEN
Definition sse_chunk.h:17
size_t build_chunk_prefix(char *buf, size_t size, const char *event, uint32_t id, uint32_t reconnect, bool with_data)
Definition sse_chunk.cpp:38
bool query_has_key(const char *query_url, size_t query_len, const char *key)
Definition utils.cpp:70
void(esphome::web_server::WebServer *, void *, json::JsonBuilder &) message_generator_t
optional< std::string > request_get_header(httpd_req_t *req, const char *name)
Definition utils.cpp:36
constexpr size_t SSE_SUFFIX_LEN
Definition sse_chunk.h:23
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
constexpr size_t SSE_SEP_LEN
Definition sse_chunk.h:20
void write_chunk_header(char *buf, size_t chunk_len)
Definition sse_chunk.cpp:57
constexpr size_t CHUNK_HDR_LEN
Definition sse_chunk.h:14
constexpr char SSE_SUFFIX[]
Definition sse_chunk.h:22
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
ESPHOME_LOG_TAG(TAG, "multipart")
bool request_has_header(httpd_req_t *req, const char *name)
Definition utils.cpp:34
void for_each_chunk_piece(const char *message, size_t message_len, ChunkPieceSink sink, void *ctx)
Definition sse_chunk.cpp:10
constexpr size_t LOG_EVENT_MAX_LEN
Definition web_server.h:43
bool random_bytes(uint8_t *data, size_t len)
Generate len random bytes using the platform's secure RNG (hardware RNG or OS CSPRNG).
Definition helpers.cpp:20
va_end(args)
const void size_t len
Definition hal.h:68
uint16_t size
Definition helpers.cpp:34
size_t size_t const char va_start(args, fmt)
size_t size_t const char * fmt
Definition helpers.h:1256
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:25
char * format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length)
Format byte array as lowercase hex to buffer (base implementation).
Definition helpers.cpp:428
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t
std::string print()
uint16_t length
Definition tt21100.cpp:0