ESPHome 2026.8.0-dev
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_AUTH_DIGEST
20#include <esp_random.h>
21#include <esp_rom_md5.h>
22#endif
23
24#ifdef USE_WEBSERVER_OTA
25#include <multipart_parser.h>
26#include "multipart.h" // For parse_multipart_boundary and other utils
27#endif
28
29#ifdef USE_WEBSERVER
32#endif // USE_WEBSERVER
33
34// Include socket headers after Arduino headers to avoid IPADDR_NONE/INADDR_NONE macro conflicts
35#include <cerrno>
36#include <sys/socket.h>
37
39
40// Status strings not provided by esp_http_server.h
41#ifndef HTTPD_401
42#define HTTPD_401 "401 Unauthorized"
43#endif
44#ifndef HTTPD_409
45#define HTTPD_409 "409 Conflict"
46#endif
47#ifndef HTTPD_422
48#define HTTPD_422 "422 Unprocessable Entity"
49#endif
50
51#define CRLF_STR "\r\n"
52#define CRLF_LEN (sizeof(CRLF_STR) - 1)
53
54static const char *const TAG = "web_server_idf";
55
56// Chunk size for streaming request bodies; matches the Arduino AsyncWebServer buffer size.
57// Buffers of this size must live on the heap - the httpd task stack is too small.
58static constexpr size_t RECV_CHUNK_SIZE = 1460;
59static constexpr size_t YIELD_INTERVAL_BYTES = 16 * 1024; // Yield every 16KB to prevent watchdog
60
61// Global instance to avoid guard variable (saves 8 bytes)
62// This is initialized at program startup before any threads
63namespace {
64// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
65DefaultHeaders default_headers_instance;
66} // namespace
67
68DefaultHeaders &DefaultHeaders::Instance() { return default_headers_instance; }
69
70namespace {
71// Non-blocking send function to prevent watchdog timeouts when TCP buffers are full
86[[maybe_unused]] int nonblocking_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_len, int flags) {
87 if (buf == nullptr) {
88 return HTTPD_SOCK_ERR_INVALID;
89 }
90
91 // Use MSG_DONTWAIT to prevent blocking when TCP send buffer is full
92 int ret = send(sockfd, buf, buf_len, flags | MSG_DONTWAIT);
93 if (ret < 0) {
94 const int err = errno;
95 if (err == EAGAIN || err == EWOULDBLOCK) {
96 // Buffer full - retry later
97 return HTTPD_SOCK_ERR_TIMEOUT;
98 }
99 // Real error
100 ESP_LOGD(TAG, "send error: errno %d", err);
101 return HTTPD_SOCK_ERR_FAIL;
102 }
103 return ret;
104}
105} // namespace
106
107void AsyncWebServer::safe_close_with_shutdown(httpd_handle_t hd, int sockfd) {
108 // CRITICAL: Shut down receive BEFORE closing to prevent lwIP race conditions
109 //
110 // The race condition occurs because close() initiates lwIP teardown while
111 // the TCP/IP thread can still receive packets, causing assertions when
112 // recv_tcp() sees partially-torn-down state.
113 //
114 // By shutting down receive first, we tell lwIP to stop accepting new data BEFORE
115 // the teardown begins, eliminating the race window. We only shutdown RD (not RDWR)
116 // to allow the FIN packet to be sent cleanly during close().
117 //
118 // Note: This function may be called with an already-closed socket if the network
119 // stack closed it. In that case, shutdown() will fail but close() is safe to call.
120 //
121 // See: https://github.com/esphome/esphome-webserver/issues/163
122
123 // Attempt shutdown - ignore errors as socket may already be closed
124 shutdown(sockfd, SHUT_RD);
125
126 // Always close - safe even if socket is already closed by network stack
127 close(sockfd);
128}
129
131 if (this->server_) {
132 httpd_stop(this->server_);
133 this->server_ = nullptr;
134 }
135}
136
138 if (this->server_) {
139 this->end();
140 }
141 // Default httpd stack is defined by ESP-IDF. Increase to accommodate SerializationBuffer's
142 // 640-byte stack buffer used by web_server JSON request handlers.
143 httpd_config_t config = HTTPD_DEFAULT_CONFIG();
144 config.stack_size = config.stack_size + 256;
145 config.server_port = this->port_;
146 config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; };
147 // Always enable LRU purging to handle socket exhaustion gracefully.
148 // When max sockets is reached, the oldest connection is closed to make room for new ones.
149 // This prevents "httpd_accept_conn: error in accept (23)" errors.
150 // See: https://github.com/esphome/esphome/issues/12464
151 config.lru_purge_enable = true;
152 // Use custom close function that shuts down before closing to prevent lwIP race conditions
154 if (httpd_start(&this->server_, &config) == ESP_OK) {
155 const httpd_uri_t handler_get = {
156 .uri = "",
157 .method = HTTP_GET,
159 .user_ctx = this,
160 };
161 httpd_register_uri_handler(this->server_, &handler_get);
162
163 const httpd_uri_t handler_post = {
164 .uri = "",
165 .method = HTTP_POST,
167 .user_ctx = this,
168 };
169 httpd_register_uri_handler(this->server_, &handler_post);
170
171 const httpd_uri_t handler_options = {
172 .uri = "",
173 .method = HTTP_OPTIONS,
175 .user_ctx = this,
176 };
177 httpd_register_uri_handler(this->server_, &handler_options);
178 }
179}
180
181esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) {
182 ESP_LOGVV(TAG, "Enter AsyncWebServer::request_post_handler. uri=%s", r->uri);
183 auto content_type = request_get_header(r, "Content-Type");
184
185 if (!request_has_header(r, "Content-Length")) {
186 ESP_LOGW(TAG, "Content length is required for post: %s", r->uri);
187 httpd_resp_send_err(r, HTTPD_411_LENGTH_REQUIRED, nullptr);
188 return ESP_OK;
189 }
190
191 if (content_type.has_value()) {
192 const char *content_type_char = content_type.value().c_str();
193
194 // Check most common case first
195 size_t content_type_len = strlen(content_type_char);
196 if (strcasestr_n(content_type_char, content_type_len, "application/x-www-form-urlencoded") != nullptr) {
197 // Normal form data - proceed with regular handling
198#ifdef USE_WEBSERVER_OTA
199 } else if (strcasestr_n(content_type_char, content_type_len, "multipart/form-data") != nullptr) {
200 auto *server = static_cast<AsyncWebServer *>(r->user_ctx);
201 return server->handle_multipart_upload_(r, content_type_char);
202#endif
203 } else {
204 // Other content types (e.g. application/json) are delivered raw to a matching
205 // custom handler via handleBody(), like the Arduino AsyncWebServer does
206 auto *server = static_cast<AsyncWebServer *>(r->user_ctx);
207 return server->handle_raw_body_(r, content_type_char);
208 }
209 }
210
211 // Handle regular form data
212 if (r->content_len > CONFIG_HTTPD_MAX_REQ_HDR_LEN) {
213 ESP_LOGW(TAG, "Request size is to big: %zu", r->content_len);
214 httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
215 return ESP_FAIL;
216 }
217
218 std::string post_query;
219 if (r->content_len > 0) {
220 post_query.resize(r->content_len);
221 const int ret = httpd_req_recv(r, &post_query[0], r->content_len + 1);
222 if (ret <= 0) { // 0 return value indicates connection closed
223 if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
224 httpd_resp_send_err(r, HTTPD_408_REQ_TIMEOUT, nullptr);
225 return ESP_ERR_TIMEOUT;
226 }
227 httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
228 return ESP_FAIL;
229 }
230 }
231
232 AsyncWebServerRequest req(r, std::move(post_query));
233 return static_cast<AsyncWebServer *>(r->user_ctx)->request_handler_(&req);
234}
235
236esp_err_t AsyncWebServer::request_handler(httpd_req_t *r) {
237 ESP_LOGVV(TAG, "Enter AsyncWebServer::request_handler. method=%u, uri=%s", r->method, r->uri);
239 return static_cast<AsyncWebServer *>(r->user_ctx)->request_handler_(&req);
240}
241
243 for (auto *handler : this->handlers_) {
244 if (handler->canHandle(request)) {
245 // At now process only basic requests.
246 // OTA requires multipart request support and handleUpload for it
247 handler->handleRequest(request);
248 return ESP_OK;
249 }
250 }
251 if (this->on_not_found_) {
252 this->on_not_found_(request);
253 return ESP_OK;
254 }
255 return ESP_ERR_NOT_FOUND;
256}
257
258esp_err_t AsyncWebServer::handle_raw_body_(httpd_req_t *r, const char *content_type) {
260 AsyncWebHandler *handler = nullptr;
261 for (auto *h : this->handlers_) {
262 if (h->canHandle(&req)) {
263 handler = h;
264 break;
265 }
266 }
267
268 if (handler == nullptr) {
269 ESP_LOGW(TAG, "Unsupported content type for POST: %s", content_type);
270 // fallback to get handler to support backward compatibility
271 return this->request_handler_(&req);
272 }
273
274 const size_t total = r->content_len;
275 if (total > 0) {
276 auto buffer = std::make_unique_for_overwrite<char[]>(RECV_CHUNK_SIZE);
277 size_t bytes_since_yield = 0;
278
279 for (size_t index = 0; index < total;) {
280 int recv_len = httpd_req_recv(r, buffer.get(), std::min(total - index, RECV_CHUNK_SIZE));
281
282 if (recv_len <= 0) {
283 httpd_resp_send_err(r, recv_len == HTTPD_SOCK_ERR_TIMEOUT ? HTTPD_408_REQ_TIMEOUT : HTTPD_400_BAD_REQUEST,
284 nullptr);
285 return recv_len == HTTPD_SOCK_ERR_TIMEOUT ? ESP_ERR_TIMEOUT : ESP_FAIL;
286 }
287
288 handler->handleBody(&req, reinterpret_cast<uint8_t *>(buffer.get()), recv_len, index, total);
289 index += recv_len;
290 bytes_since_yield += recv_len;
291
292 if (bytes_since_yield > YIELD_INTERVAL_BYTES) {
293 vTaskDelay(1);
294 bytes_since_yield = 0;
295 }
296 }
297 }
298
299 handler->handleRequest(&req);
300 return ESP_OK;
301}
302
304 delete this->rsp_;
305 for (auto *param : this->params_) {
306 delete param; // NOLINT(cppcoreguidelines-owning-memory)
307 }
308}
309
310bool AsyncWebServerRequest::hasHeader(const char *name) const { return request_has_header(*this, name); }
311
312optional<std::string> AsyncWebServerRequest::get_header(const char *name) const {
313 return request_get_header(*this, name);
314}
315
316StringRef AsyncWebServerRequest::url_to(std::span<char, URL_BUF_SIZE> buffer) const {
317 const char *uri = this->req_->uri;
318 const char *query_start = strchr(uri, '?');
319 size_t uri_len = query_start ? static_cast<size_t>(query_start - uri) : strlen(uri);
320 size_t copy_len = std::min(uri_len, URL_BUF_SIZE - 1);
321 memcpy(buffer.data(), uri, copy_len);
322 buffer[copy_len] = '\0';
323 // Decode URL-encoded characters in-place (e.g., %20 -> space)
324 size_t decoded_len = url_decode(buffer.data());
325 return StringRef(buffer.data(), decoded_len);
326}
327
328void AsyncWebServerRequest::redirect(const std::string &url) {
329 httpd_resp_set_status(*this, "302 Found");
330 httpd_resp_set_hdr(*this, "Location", url.c_str());
331 httpd_resp_set_hdr(*this, "Connection", "close");
332 httpd_resp_send(*this, nullptr, 0);
333}
334
335void AsyncWebServerRequest::init_response_(AsyncWebServerResponse *rsp, int code, const char *content_type) {
336 // Set status code - use constants for common codes, default to 500 for unknown codes
337 const char *status;
338 switch (code) {
339 case 200:
340 status = HTTPD_200;
341 break;
342 case 204:
343 status = HTTPD_204;
344 break;
345 case 400:
346 status = HTTPD_400;
347 break;
348 case 401:
349 status = HTTPD_401;
350 break;
351 case 404:
352 status = HTTPD_404;
353 break;
354 case 409:
355 status = HTTPD_409;
356 break;
357 case 422:
358 status = HTTPD_422;
359 break;
360 default:
361 status = HTTPD_500;
362 break;
363 }
364 httpd_resp_set_status(*this, status);
365
366 if (content_type && *content_type) {
367 httpd_resp_set_type(*this, content_type);
368 }
369 httpd_resp_set_hdr(*this, "Accept-Ranges", "none");
370
371 for (const auto &header : DefaultHeaders::Instance().headers_) {
372 httpd_resp_set_hdr(*this, header.name, header.value);
373 }
374
375 delete this->rsp_;
376 this->rsp_ = rsp;
377}
378
379#ifdef USE_WEBSERVER_AUTH
380
381#ifdef USE_WEBSERVER_AUTH_DIGEST
382namespace {
383
384// Extract the value of a Digest auth parameter (e.g. "nonce") from the comma-separated
385// parameter list. Values may be quoted or bare. Returns an empty ref when the key is absent.
386// Only whole parameter names match, so "nc" does not match inside "cnonce".
387StringRef digest_param(StringRef params, const char *key) {
388 size_t key_len = strlen(key);
389 const char *base = params.c_str();
390 size_t n = params.size();
391 size_t i = 0;
392 while (i < n) {
393 while (i < n && (base[i] == ' ' || base[i] == ','))
394 i++;
395 size_t name_start = i;
396 while (i < n && base[i] != '=' && base[i] != ',')
397 i++;
398 if (i >= n || base[i] == ',')
399 continue; // token without a '=', skip it
400 size_t name_len = i - name_start;
401 while (name_len > 0 && base[name_start + name_len - 1] == ' ')
402 name_len--;
403 i++; // consume '='
404 const char *val_start;
405 size_t val_len;
406 if (i < n && base[i] == '"') {
407 i++;
408 val_start = base + i;
409 while (i < n && base[i] != '"')
410 i++;
411 val_len = (base + i) - val_start;
412 if (i < n)
413 i++; // consume closing quote
414 } else {
415 val_start = base + i;
416 while (i < n && base[i] != ',')
417 i++;
418 val_len = (base + i) - val_start;
419 }
420 if (name_len == key_len && memcmp(base + name_start, key, key_len) == 0)
421 return StringRef(val_start, val_len);
422 while (i < n && base[i] != ',')
423 i++;
424 }
425 return StringRef();
426}
427
428// Verify an RFC 2617 Digest response. Stateless (the nonce we issued is not tracked), which
429// matches the ESPAsyncWebServer backend used on the Arduino platforms.
430bool check_digest_auth(const char *username, const char *password, const std::string &header, const char *method) {
431 const size_t prefix_len = sizeof("Digest ") - 1;
432 StringRef params(header.c_str() + prefix_len, header.size() - prefix_len);
433
434 if (digest_param(params, "username") != username)
435 return false;
436
437 StringRef realm = digest_param(params, "realm");
438 StringRef nonce = digest_param(params, "nonce");
439 StringRef uri = digest_param(params, "uri");
440 StringRef qop = digest_param(params, "qop");
441 StringRef nc = digest_param(params, "nc");
442 StringRef cnonce = digest_param(params, "cnonce");
443 StringRef response = digest_param(params, "response");
444 if (response.size() != 32)
445 return false;
446
447 // Compute the three MD5 hashes by streaming the pieces straight into the ROM MD5 engine, so
448 // nothing is concatenated on the heap. Each hash is emitted as 32 lowercase hex characters.
449 md5_context_t ctx;
450 uint8_t digest[16];
451
452 // HA1 = MD5(username:realm:password) -- uses the realm the client echoed back.
453 char ha1[33];
454 esp_rom_md5_init(&ctx);
455 esp_rom_md5_update(&ctx, username, strlen(username));
456 esp_rom_md5_update(&ctx, ":", 1);
457 esp_rom_md5_update(&ctx, realm.c_str(), realm.size());
458 esp_rom_md5_update(&ctx, ":", 1);
459 esp_rom_md5_update(&ctx, password, strlen(password));
460 esp_rom_md5_final(digest, &ctx);
461 format_hex_to(ha1, digest, sizeof(digest));
462
463 // HA2 = MD5(method:uri) -- uses the uri the client echoed back.
464 char ha2[33];
465 esp_rom_md5_init(&ctx);
466 esp_rom_md5_update(&ctx, method, strlen(method));
467 esp_rom_md5_update(&ctx, ":", 1);
468 esp_rom_md5_update(&ctx, uri.c_str(), uri.size());
469 esp_rom_md5_final(digest, &ctx);
470 format_hex_to(ha2, digest, sizeof(digest));
471
472 // expected = MD5(HA1:nonce:nc:cnonce:qop:HA2)
473 char expected[33];
474 esp_rom_md5_init(&ctx);
475 esp_rom_md5_update(&ctx, ha1, 32);
476 esp_rom_md5_update(&ctx, ":", 1);
477 esp_rom_md5_update(&ctx, nonce.c_str(), nonce.size());
478 esp_rom_md5_update(&ctx, ":", 1);
479 esp_rom_md5_update(&ctx, nc.c_str(), nc.size());
480 esp_rom_md5_update(&ctx, ":", 1);
481 esp_rom_md5_update(&ctx, cnonce.c_str(), cnonce.size());
482 esp_rom_md5_update(&ctx, ":", 1);
483 esp_rom_md5_update(&ctx, qop.c_str(), qop.size());
484 esp_rom_md5_update(&ctx, ":", 1);
485 esp_rom_md5_update(&ctx, ha2, 32);
486 esp_rom_md5_final(digest, &ctx);
487 format_hex_to(expected, digest, sizeof(digest));
488
489 // Constant-time comparison of the two 32-char hex digests.
490 uint8_t result = 0;
491 for (size_t i = 0; i < 32; i++)
492 result |= static_cast<uint8_t>(expected[i] ^ response[i]);
493 return result == 0;
494}
495
496} // namespace
497#endif // USE_WEBSERVER_AUTH_DIGEST
498
499bool AsyncWebServerRequest::authenticate(const char *username, const char *password) const {
500 if (username == nullptr || password == nullptr || *username == 0) {
501 return true;
502 }
503 auto auth = this->get_header("Authorization");
504 if (!auth.has_value()) {
505 return false;
506 }
507
508 auto *auth_str = auth.value().c_str();
509
510#ifdef USE_WEBSERVER_AUTH_DIGEST
511 // The build fixed the scheme to Digest, so the Basic path is compiled out entirely.
512 const auto auth_prefix_len = sizeof("Digest ") - 1;
513 if (strncmp("Digest ", auth_str, auth_prefix_len) != 0) {
514 ESP_LOGW(TAG, "Only Digest authorization supported");
515 return false;
516 }
517 return check_digest_auth(username, password, auth.value(), http_method_str(this->method()));
518#else
519 const auto auth_prefix_len = sizeof("Basic ") - 1;
520 if (strncmp("Basic ", auth_str, auth_prefix_len) != 0) {
521 ESP_LOGW(TAG, "Only Basic authorization supported");
522 return false;
523 }
524
525 // Build user:pass in stack buffer to avoid heap allocation
526 constexpr size_t max_user_info_len = 256;
527 char user_info[max_user_info_len];
528 size_t user_len = strlen(username);
529 size_t pass_len = strlen(password);
530 size_t user_info_len = user_len + 1 + pass_len;
531
532 if (user_info_len >= max_user_info_len) {
533 ESP_LOGW(TAG, "Credentials too long for authentication");
534 return false;
535 }
536
537 memcpy(user_info, username, user_len);
538 user_info[user_len] = ':';
539 memcpy(user_info + user_len + 1, password, pass_len);
540 user_info[user_info_len] = '\0';
541
542 // Base64 output size is ceil(input_len * 4/3) + 1, with input bounded to 256 bytes
543 // max output is ceil(256 * 4/3) + 1 = 343 bytes, use 350 for safety
544 constexpr size_t max_digest_len = 350;
545 char digest[max_digest_len];
546 size_t out;
547 esp_crypto_base64_encode(reinterpret_cast<uint8_t *>(digest), max_digest_len, &out,
548 reinterpret_cast<const uint8_t *>(user_info), user_info_len);
549
550 // Constant-time comparison to avoid timing side channels.
551 // No early return on length mismatch — the length difference is folded
552 // into the accumulator so any mismatch is rejected.
553 const char *provided = auth_str + auth_prefix_len;
554 size_t digest_len = out; // length from esp_crypto_base64_encode
555 // Derive provided_len from the already-sized std::string rather than
556 // rescanning with strlen (avoids attacker-controlled scan length).
557 size_t provided_len = auth.value().size() - auth_prefix_len;
558 // Use full-width XOR so any bit difference in the lengths is preserved
559 // (uint8_t truncation would miss differences in higher bytes, e.g.
560 // digest_len vs digest_len + 256).
561 volatile size_t result = digest_len ^ provided_len;
562 // Iterate over the expected digest length only — the full-width length
563 // XOR above already rejects any length mismatch, and bounding the loop
564 // prevents a long Authorization header from forcing extra work.
565 for (size_t i = 0; i < digest_len; i++) {
566 char provided_ch = (i < provided_len) ? provided[i] : 0;
567 result |= static_cast<uint8_t>(digest[i] ^ provided_ch);
568 }
569 return result == 0;
570#endif // USE_WEBSERVER_AUTH_DIGEST
571}
572
574 httpd_resp_set_hdr(*this, "Connection", "keep-alive");
575#ifdef USE_WEBSERVER_AUTH_DIGEST
576 // Issue a fresh random nonce and opaque. The nonce is not stored, so this is stateless and
577 // does not defend against replay -- its purpose is to keep the password off the wire.
578 // The header value must stay alive until httpd_resp_send_err() below sends it, so the buffer
579 // lives on this stack frame (httpd_resp_set_hdr stores the pointer, it does not copy).
580 uint8_t random_bytes[16];
581 char nonce[33];
582 char opaque[33];
583 char header[160];
584 esp_fill_random(random_bytes, sizeof(random_bytes));
585 format_hex_to(nonce, random_bytes, sizeof(random_bytes));
586 esp_fill_random(random_bytes, sizeof(random_bytes));
587 format_hex_to(opaque, random_bytes, sizeof(random_bytes));
588 snprintf(header, sizeof(header), R"(Digest realm="Login Required", qop="auth", nonce="%s", opaque="%s")", nonce,
589 opaque);
590 httpd_resp_set_hdr(*this, "WWW-Authenticate", header);
591#else
592 httpd_resp_set_hdr(*this, "WWW-Authenticate", "Basic realm=\"Login Required\"");
593#endif // USE_WEBSERVER_AUTH_DIGEST
594 httpd_resp_send_err(*this, HTTPD_401_UNAUTHORIZED, nullptr);
595}
596#endif // USE_WEBSERVER_AUTH
597
599 // Check cache first - only successful lookups are cached
600 for (auto *param : this->params_) {
601 if (param->name() == name) {
602 return param;
603 }
604 }
605
606 // Look up value from query strings
607 auto val = this->find_query_value_(name);
608
609 // Don't cache misses to avoid wasting memory when handlers check for
610 // optional parameters that don't exist in the request
611 if (!val.has_value()) {
612 return nullptr;
613 }
614
615 auto *param = new AsyncWebParameter(name, val.value()); // NOLINT(cppcoreguidelines-owning-memory)
616 this->params_.push_back(param);
617 return param;
618}
619
623template<typename Func>
624static auto search_query_sources(httpd_req_t *req, const std::string &post_query, const char *name, Func func)
625 -> decltype(func(nullptr, size_t{0}, name)) {
626 if (!post_query.empty()) {
627 auto result = func(post_query.c_str(), post_query.size(), name);
628 if (result) {
629 return result;
630 }
631 }
632 // Use httpd API for query length, then access string directly from URI.
633 // http_parser identifies components by offset/length without modifying the URI string.
634 // This is the same pattern used by url_to().
635 auto len = httpd_req_get_url_query_len(req);
636 if (len == 0) {
637 return {};
638 }
639 const char *query = strchr(req->uri, '?');
640 if (query == nullptr) {
641 return {};
642 }
643 query++; // skip '?'
644 return func(query, len, name);
645}
646
647optional<std::string> AsyncWebServerRequest::find_query_value_(const char *name) const {
648 return search_query_sources(this->req_, this->post_query_, name,
649 [](const char *q, size_t len, const char *k) { return query_key_value(q, len, k); });
650}
651
652bool AsyncWebServerRequest::hasArg(const char *name) {
653 return search_query_sources(this->req_, this->post_query_, name, query_has_key);
654}
655
656std::string AsyncWebServerRequest::arg(const char *name) {
657 auto val = this->find_query_value_(name);
658 if (val.has_value()) {
659 return std::move(val.value());
660 }
661 return {};
662}
663
664void AsyncWebServerResponse::addHeader(const char *name, const char *value) {
665 httpd_resp_set_hdr(*this->req_, name, value);
666}
667
668void AsyncResponseStream::print(float value) {
669 // Use stack buffer to avoid temporary string allocation
670 // Size: sign (1) + digits (10) + decimal (1) + precision (6) + exponent (5) + null (1) = 24, use 32 for safety
671 char buf[32];
672 int len = snprintf(buf, sizeof(buf), "%f", value);
673 this->content_.append(buf, len);
674}
675
676void AsyncResponseStream::printf(const char *fmt, ...) {
677 va_list args;
678
679 va_start(args, fmt);
680 const int length = vsnprintf(nullptr, 0, fmt, args);
681 va_end(args);
682
683 std::string str;
684 str.resize(length);
685
686 va_start(args, fmt);
687 vsnprintf(&str[0], length + 1, fmt, args);
688 va_end(args);
689
690 this->print(str);
691}
692
693#ifdef USE_WEBSERVER
695 LockGuard guard{this->pending_mutex_};
696 for (auto *vec : {&this->sessions_, &this->pending_sessions_}) {
697 for (auto *ses : *vec) {
698 delete ses; // NOLINT(cppcoreguidelines-owning-memory)
699 }
700 }
701}
702
704 // Httpd task: set up the live httpd_req_t and park the session; main loop does the rest.
705 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory,clang-analyzer-cplusplus.NewDeleteLeaks)
706 auto *rsp = new AsyncEventSourceResponse(request, this, this->web_server_);
707 {
708 LockGuard guard{this->pending_mutex_};
709 this->pending_sessions_.push_back(rsp);
710 this->has_pending_sessions_.store(true, std::memory_order_release);
711 }
713}
714
715// clang-analyzer traces a false-positive leak path from loop() through
716// adopt_pending_sessions_main_loop_() into start_session_main_loop_() and
717// finally ArduinoJson. Suppress along the entire in-our-code call chain.
718// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks)
720 // Fast path: one atomic load per tick. Slow path is out-of-line on connect.
721 if (this->has_pending_sessions_.load(std::memory_order_acquire)) {
722 this->adopt_pending_sessions_main_loop_();
723 }
724
725 // Clean up dead sessions safely
726 // This follows the ESP-IDF pattern where free_ctx marks resources as dead
727 // and the main loop handles the actual cleanup to avoid race conditions
728 for (size_t i = 0; i < this->sessions_.size();) {
729 auto *ses = this->sessions_[i];
730 // If the session has a dead socket (marked by destroy callback)
731 if (ses->fd_.load() == 0) {
732 // destroy() already logged the close with the fd; don't double-log here.
733 delete ses; // NOLINT(cppcoreguidelines-owning-memory)
734 // Remove by swapping with last element (O(1) removal, order doesn't matter for sessions)
735 this->sessions_[i] = this->sessions_.back();
736 this->sessions_.pop_back();
737 } else {
738 ses->loop();
739 ++i;
740 }
741 }
742 return !this->sessions_.empty();
743}
744
745void AsyncEventSource::adopt_pending_sessions_main_loop_() {
746 std::vector<AsyncEventSourceResponse *> incoming;
747 {
748 LockGuard guard{this->pending_mutex_};
749 incoming.swap(this->pending_sessions_);
750 this->has_pending_sessions_.store(false, std::memory_order_relaxed);
751 }
752 for (auto *rsp : incoming) {
753 // Already disconnected? Drop it; skip on_connect_/session start on a dead session.
754 if (rsp->fd_.load() == 0) {
755 delete rsp; // NOLINT(cppcoreguidelines-owning-memory)
756 continue;
757 }
758 this->sessions_.push_back(rsp);
759 // Prime first so on_connect_ observes a session that has already sent its
760 // initial ping/config/sorting_groups, matching the pre-refactor ordering.
761 rsp->start_session_main_loop_();
762 if (this->on_connect_) {
763 this->on_connect_(rsp);
764 }
765 }
766}
767// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
768
769void AsyncEventSource::try_send_nodefer(const char *message, size_t message_len, const char *event, uint32_t id,
770 uint32_t reconnect) {
771 for (auto *ses : this->sessions_) {
772 if (ses->fd_.load() != 0) { // Skip dead sessions
773 ses->try_send_nodefer(message, message_len, event, id, reconnect);
774 }
775 }
776}
777
778void AsyncEventSource::deferrable_send_state(void *source, const char *event_type,
779 message_generator_t *message_generator) {
780 // Skip if no connected clients to avoid unnecessary processing
781 if (this->empty())
782 return;
783 for (auto *ses : this->sessions_) {
784 if (ses->fd_.load() != 0) { // Skip dead sessions
785 ses->deferrable_send_state(source, event_type, message_generator);
786 }
787 }
788}
789
793 : server_(server), web_server_(ws), entities_iterator_(ws, server) {
794 // Httpd task only. start_session_main_loop_() handles event_buffer_ / iterator setup.
795 httpd_req_t *req = *request;
796
797 httpd_resp_set_status(req, HTTPD_200);
798 httpd_resp_set_type(req, "text/event-stream");
799 httpd_resp_set_hdr(req, "Cache-Control", "no-cache");
800 httpd_resp_set_hdr(req, "Connection", "keep-alive");
801
802 for (const auto &header : DefaultHeaders::Instance().headers_) {
803 httpd_resp_set_hdr(req, header.name, header.value);
804 }
805
806 httpd_resp_send_chunk(req, CRLF_STR, CRLF_LEN);
807
808 req->sess_ctx = this;
809 req->free_ctx = AsyncEventSourceResponse::destroy;
810
811 this->hd_ = req->handle;
812 this->fd_.store(httpd_req_to_sockfd(req));
813
814 // Use non-blocking send to prevent watchdog timeouts when TCP buffers are full
815 httpd_sess_set_send_override(this->hd_, this->fd_.load(), nonblocking_send);
816}
817
818// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
820 auto *ws = this->web_server_;
821
822 // tcp send buffer is empty on connect, so these should always go through
823 auto message = ws->get_config_json();
824 this->try_send_nodefer(message.c_str(), message.size(), "ping", millis(), 30000);
825
826#ifdef USE_WEBSERVER_SORTING
827 for (auto &group : ws->sorting_groups_) {
828 json::JsonBuilder builder;
829 JsonObject root = builder.root();
830 root["name"] = group.second.name;
831 root["sorting_weight"] = group.second.weight;
832 message = builder.serialize();
833
834 // a (very) large number of these should be able to be queued initially without defer
835 // since the only thing in the send buffer at this point is the initial ping/config
836 this->try_send_nodefer(message.c_str(), message.size(), "sorting_group");
837 }
838#endif
839
840 this->entities_iterator_.begin(ws->include_internal_);
841}
842// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
843
845 auto *rsp = static_cast<AsyncEventSourceResponse *>(ptr);
846 int fd = rsp->fd_.exchange(0); // Atomically get and clear fd
847 ESP_LOGD(TAG, "Event source connection closed (fd: %d)", fd);
848 // Mark as dead - will be cleaned up in the main loop
849 // Note: We don't delete or remove from set here to avoid race conditions
850 // httpd will call our custom close_fn (safe_close_with_shutdown) which handles
851 // shutdown() before close() to prevent lwIP race conditions
852}
853
854// helper for allowing only unique entries in the queue
856 DeferredEvent item(source, message_generator);
857
858 // Use range-based for loop instead of std::find_if to reduce template instantiation overhead and binary size
859 for (auto &event : this->deferred_queue_) {
860 if (event == item) {
861 return; // Already in queue, no need to update since items are equal
862 }
863 }
864 this->deferred_queue_.push_back(item);
865}
866
868 while (!deferred_queue_.empty()) {
869 DeferredEvent &de = deferred_queue_.front();
871 if (this->try_send_nodefer(message.c_str(), message.size(), "state")) {
872 // O(n) but memory efficiency is more important than speed here which is why std::vector was chosen
873 deferred_queue_.erase(deferred_queue_.begin());
874 } else {
875 break;
876 }
877 }
878}
879
881 if (event_buffer_.empty()) {
882 return;
883 }
884 if (event_bytes_sent_ == event_buffer_.size()) {
885 event_buffer_.resize(0);
887 return;
888 }
889
890 size_t remaining = event_buffer_.size() - event_bytes_sent_;
891 int bytes_sent =
892 httpd_socket_send(this->hd_, this->fd_.load(), event_buffer_.c_str() + event_bytes_sent_, remaining, 0);
893 if (bytes_sent == HTTPD_SOCK_ERR_TIMEOUT) {
894 // EAGAIN/EWOULDBLOCK - socket buffer full, try again later
895 // NOTE: Similar logic exists in web_server/web_server.cpp in DeferredUpdateEventSource::process_deferred_queue_()
896 // The implementations differ due to platform-specific APIs (HTTPD_SOCK_ERR_TIMEOUT vs DISCARDED, fd_.store(0) vs
897 // close()), but the failure counting and timeout logic should be kept in sync. If you change this logic, also
898 // update the Arduino implementation.
901 // Too many failures, connection is likely dead
902 ESP_LOGW(TAG, "Closing stuck EventSource connection after %" PRIu16 " failed sends",
904 this->fd_.store(0); // Mark for cleanup
905 this->deferred_queue_.clear();
906 }
907 return;
908 }
909 if (bytes_sent == HTTPD_SOCK_ERR_FAIL) {
910 // Real socket error - connection will be closed by httpd and destroy callback will be called
911 return;
912 }
913 if (bytes_sent <= 0) {
914 // Unexpected error or zero bytes sent
915 ESP_LOGW(TAG, "Unexpected send result: %d", bytes_sent);
916 return;
917 }
918
919 // Successful send - reset failure counter
921 event_bytes_sent_ += bytes_sent;
922
923 // Log partial sends for debugging
924 if (event_bytes_sent_ < event_buffer_.size()) {
925 ESP_LOGV(TAG, "Partial send: %d/%zu bytes (total: %zu/%zu)", bytes_sent, remaining, event_bytes_sent_,
926 event_buffer_.size());
927 }
928
929 if (event_bytes_sent_ == event_buffer_.size()) {
930 event_buffer_.resize(0);
932 }
933}
934
941
942bool AsyncEventSourceResponse::try_send_nodefer(const char *message, size_t message_len, const char *event, uint32_t id,
943 uint32_t reconnect) {
944 if (this->fd_.load() == 0) {
945 return false;
946 }
947
949 if (!event_buffer_.empty()) {
950 // there is still pending event data to send first
951 return false;
952 }
953
954 // 8 spaces are standing in for the hexidecimal chunk length to print later
955 const char chunk_len_header[] = " " CRLF_STR;
956 const int chunk_len_header_len = sizeof(chunk_len_header) - 1;
957
958 event_buffer_.append(chunk_len_header);
959
960 // Use stack buffer for formatting numeric fields to avoid temporary string allocations
961 // Size: "retry: " (7) + max uint32 (10 digits) + CRLF (2) + null (1) = 20 bytes, use 32 for safety
962 constexpr size_t num_buf_size = 32;
963 char num_buf[num_buf_size];
964
965 if (reconnect) {
966 int len = snprintf(num_buf, num_buf_size, "retry: %" PRIu32 CRLF_STR, reconnect);
967 event_buffer_.append(num_buf, len);
968 }
969
970 if (id) {
971 int len = snprintf(num_buf, num_buf_size, "id: %" PRIu32 CRLF_STR, id);
972 event_buffer_.append(num_buf, len);
973 }
974
975 if (event && *event) {
976 event_buffer_.append("event: ", sizeof("event: ") - 1);
977 event_buffer_.append(event);
978 event_buffer_.append(CRLF_STR, CRLF_LEN);
979 }
980
981 // Match ESPAsyncWebServer: null message means no data lines and no terminating blank line
982 if (message) {
983 // SSE spec requires each line of a multi-line message to have its own "data:" prefix
984 // Handle \n, \r, and \r\n line endings (matching ESPAsyncWebServer behavior)
985
986 // Fast path: check if message contains any newlines at all
987 // Most SSE messages (JSON state updates) have no newlines
988 const char *first_n = static_cast<const char *>(memchr(message, '\n', message_len));
989 const char *first_r = static_cast<const char *>(memchr(message, '\r', message_len));
990
991 if (first_n == nullptr && first_r == nullptr) {
992 // No newlines - fast path (most common case)
993 event_buffer_.append("data: ", sizeof("data: ") - 1);
994 event_buffer_.append(message, message_len);
995 event_buffer_.append(CRLF_STR CRLF_STR, CRLF_LEN * 2); // data line + blank line terminator
996 } else {
997 // Has newlines - handle multi-line message
998 const char *line_start = message;
999 const char *msg_end = message + message_len;
1000
1001 // Reuse the first search results
1002 const char *next_n = first_n;
1003 const char *next_r = first_r;
1004
1005 while (line_start <= msg_end) {
1006 const char *line_end;
1007 const char *next_line;
1008
1009 if (next_n == nullptr && next_r == nullptr) {
1010 // No more line breaks - output remaining text as final line
1011 event_buffer_.append("data: ", sizeof("data: ") - 1);
1012 event_buffer_.append(line_start, msg_end - line_start);
1013 event_buffer_.append(CRLF_STR, CRLF_LEN);
1014 break;
1015 }
1016
1017 // Determine line ending type and next line start
1018 if (next_n != nullptr && next_r != nullptr) {
1019 if (next_r + 1 == next_n) {
1020 // \r\n sequence
1021 line_end = next_r;
1022 next_line = next_n + 1;
1023 } else {
1024 // Mixed \n and \r - use whichever comes first
1025 line_end = (next_r < next_n) ? next_r : next_n;
1026 next_line = line_end + 1;
1027 }
1028 } else if (next_n != nullptr) {
1029 // Unix LF
1030 line_end = next_n;
1031 next_line = next_n + 1;
1032 } else {
1033 // Old Mac CR
1034 line_end = next_r;
1035 next_line = next_r + 1;
1036 }
1037
1038 // Output this line
1039 event_buffer_.append("data: ", sizeof("data: ") - 1);
1040 event_buffer_.append(line_start, line_end - line_start);
1041 event_buffer_.append(CRLF_STR, CRLF_LEN);
1042
1043 line_start = next_line;
1044
1045 // Check if we've consumed all content
1046 if (line_start >= msg_end) {
1047 break;
1048 }
1049
1050 // Search for next newlines only in remaining string
1051 next_n = static_cast<const char *>(memchr(line_start, '\n', msg_end - line_start));
1052 next_r = static_cast<const char *>(memchr(line_start, '\r', msg_end - line_start));
1053 }
1054
1055 // Terminate message with blank line
1056 event_buffer_.append(CRLF_STR, CRLF_LEN);
1057 }
1058 }
1059
1060 if (event_buffer_.size() == static_cast<size_t>(chunk_len_header_len)) {
1061 // Nothing was added, reset buffer
1062 event_buffer_.resize(0);
1063 return true;
1064 }
1065
1066 event_buffer_.append(CRLF_STR, CRLF_LEN);
1067
1068 // chunk length header itself and the final chunk terminating CRLF are not counted as part of the chunk
1069 int chunk_len = event_buffer_.size() - CRLF_LEN - chunk_len_header_len;
1070 char chunk_len_str[9];
1071 snprintf(chunk_len_str, 9, "%08x", chunk_len);
1072 std::memcpy(&event_buffer_[0], chunk_len_str, 8);
1073
1076
1077 return true;
1078}
1079
1080void AsyncEventSourceResponse::deferrable_send_state(void *source, const char *event_type,
1081 message_generator_t *message_generator) {
1082 // allow all json "details_all" to go through before publishing bare state events, this avoids unnamed entries showing
1083 // up in the web GUI and reduces event load during initial connect
1084 if (!this->entities_iterator_.completed() && 0 != strcmp(event_type, "state_detail_all"))
1085 return;
1086
1087 if (source == nullptr)
1088 return;
1089 if (event_type == nullptr)
1090 return;
1091 if (message_generator == nullptr)
1092 return;
1093
1094 if (0 != strcmp(event_type, "state_detail_all") && 0 != strcmp(event_type, "state")) {
1095 ESP_LOGE(TAG, "Can't defer non-state event");
1096 }
1097
1100
1101 if (!event_buffer_.empty() || !deferred_queue_.empty()) {
1102 // outgoing event buffer or deferred queue still not empty which means downstream tcp send buffer full, no point
1103 // trying to send first
1104 deq_push_back_with_dedup_(source, message_generator);
1105 } else {
1106 auto message = message_generator(web_server_, source);
1107 if (!this->try_send_nodefer(message.c_str(), message.size(), "state")) {
1108 deq_push_back_with_dedup_(source, message_generator);
1109 }
1110 }
1111}
1112#endif
1113
1114#ifdef USE_WEBSERVER_OTA
1115esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *content_type) {
1116 // Parse boundary and create reader
1117 const char *boundary_start;
1118 size_t boundary_len;
1119 if (!parse_multipart_boundary(content_type, &boundary_start, &boundary_len)) {
1120 ESP_LOGE(TAG, "Failed to parse multipart boundary");
1121 httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
1122 return ESP_FAIL;
1123 }
1124
1125 AsyncWebServerRequest req(r);
1126 AsyncWebHandler *handler = nullptr;
1127 for (auto *h : this->handlers_) {
1128 if (h->canHandle(&req)) {
1129 handler = h;
1130 break;
1131 }
1132 }
1133
1134 if (!handler) {
1135 ESP_LOGW(TAG, "No handler found for OTA request");
1136 httpd_resp_send_err(r, HTTPD_404_NOT_FOUND, nullptr);
1137 return ESP_OK;
1138 }
1139
1140 // Upload state
1141 std::string filename;
1142 size_t index = 0;
1143 // Create reader on heap to reduce stack usage
1144 auto reader = std::make_unique<MultipartReader>("--" + std::string(boundary_start, boundary_len));
1145
1146 // Configure callbacks
1147 reader->set_data_callback([&](const uint8_t *data, size_t len) {
1148 if (!reader->has_file() || !len)
1149 return;
1150
1151 if (filename.empty()) {
1152 filename = reader->get_current_part().filename;
1153 ESP_LOGV(TAG, "Processing file: '%s'", filename.c_str());
1154 handler->handleUpload(&req, filename, 0, nullptr, 0, false); // Start
1155 }
1156
1157 handler->handleUpload(&req, filename, index, const_cast<uint8_t *>(data), len, false);
1158 index += len;
1159 });
1160
1161 reader->set_part_complete_callback([&]() {
1162 if (index > 0) {
1163 handler->handleUpload(&req, filename, index, nullptr, 0, true); // End
1164 filename.clear();
1165 index = 0;
1166 }
1167 });
1168
1169 auto buffer = std::make_unique_for_overwrite<char[]>(RECV_CHUNK_SIZE);
1170 size_t bytes_since_yield = 0;
1171
1172 for (size_t remaining = r->content_len; remaining > 0;) {
1173 int recv_len = httpd_req_recv(r, buffer.get(), std::min(remaining, RECV_CHUNK_SIZE));
1174
1175 if (recv_len <= 0) {
1176 httpd_resp_send_err(r, recv_len == HTTPD_SOCK_ERR_TIMEOUT ? HTTPD_408_REQ_TIMEOUT : HTTPD_400_BAD_REQUEST,
1177 nullptr);
1178 return recv_len == HTTPD_SOCK_ERR_TIMEOUT ? ESP_ERR_TIMEOUT : ESP_FAIL;
1179 }
1180
1181 if (reader->parse(buffer.get(), recv_len) != static_cast<size_t>(recv_len)) {
1182 ESP_LOGW(TAG, "Multipart parser error");
1183 httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
1184 return ESP_FAIL;
1185 }
1186
1187 remaining -= recv_len;
1188 bytes_since_yield += recv_len;
1189
1190 if (bytes_since_yield > YIELD_INTERVAL_BYTES) {
1191 vTaskDelay(1);
1192 bytes_since_yield = 0;
1193 }
1194 }
1195
1196 handler->handleRequest(&req);
1197 return ESP_OK;
1198}
1199#endif // USE_WEBSERVER_OTA
1200
1201} // namespace esphome::web_server_idf
1202
1203#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:1943
StringRef is a reference to a string owned by something else.
Definition string_ref.h:26
constexpr const char * c_str() const
Definition string_ref.h:73
constexpr size_type size() const
Definition string_ref.h:74
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:193
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)
void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator)
esphome::web_server::WebServer * web_server_
void handleRequest(AsyncWebServerRequest *request) override
bool try_send_nodefer(const char *message, size_t message_len, const char *event=nullptr, uint32_t id=0, uint32_t reconnect=0)
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_
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
ESPDEPRECATED("Use url_to() instead. Removed in 2026.9.0", "2026.3.0") std void requestAuthentication() const
std::vector< AsyncWebParameter * > params_
void addHeader(const char *name, const char *value)
const LogString * 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
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
const char int const __FlashStringHelper va_list args
Definition log.h:74
va_end(args)
const void size_t len
Definition hal.h:64
size_t size_t const char va_start(args, fmt)
size_t size_t const char * fmt
Definition helpers.h:1053
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:28
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:334
static void uint32_t
std::string print()
uint16_t length
Definition tt21100.cpp:0