From e42cc2e3944d30e5ac64fb16addaceb14c2847ee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 11 Feb 2026 18:23:38 -0600 Subject: [PATCH 1/5] Add query_has_key for efficient hasArg without string allocation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hasArg only needs to know if a key exists, not its value. query_has_key uses a 1-byte buffer and checks the return code from httpd_query_key_value — no url_decode, no std::string. --- esphome/components/web_server_idf/utils.cpp | 11 +++++++++++ esphome/components/web_server_idf/utils.h | 1 + esphome/components/web_server_idf/web_server_idf.cpp | 11 ++++++++++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/esphome/components/web_server_idf/utils.cpp b/esphome/components/web_server_idf/utils.cpp index 81ae626277f..542a7fc0c0a 100644 --- a/esphome/components/web_server_idf/utils.cpp +++ b/esphome/components/web_server_idf/utils.cpp @@ -88,6 +88,17 @@ optional query_key_value(const char *query_url, size_t query_len, c return {val.get()}; } +bool query_has_key(const char *query_url, size_t query_len, const char *key) { + if (query_url == nullptr || query_len == 0) { + return false; + } + // Minimal buffer — we only care if the key exists, not the value + char buf[1]; + // httpd_query_key_value returns ESP_OK if key found (even if buffer too small for value), + // ESP_ERR_NOT_FOUND if key absent + return httpd_query_key_value(query_url, key, buf, sizeof(buf)) != ESP_ERR_NOT_FOUND; +} + // Helper function for case-insensitive string region comparison bool str_ncmp_ci(const char *s1, const char *s2, size_t n) { for (size_t i = 0; i < n; i++) { diff --git a/esphome/components/web_server_idf/utils.h b/esphome/components/web_server_idf/utils.h index 87635c04584..8eee82696fd 100644 --- a/esphome/components/web_server_idf/utils.h +++ b/esphome/components/web_server_idf/utils.h @@ -18,6 +18,7 @@ optional query_key_value(const char *query_url, size_t query_len, c inline optional query_key_value(const std::string &query_url, const std::string &key) { return query_key_value(query_url.c_str(), query_url.size(), key.c_str()); } +bool query_has_key(const char *query_url, size_t query_len, const char *key); // Helper function for case-insensitive character comparison inline bool char_equals_ci(char a, char b) { return ::tolower(a) == ::tolower(b); } diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 1f598352680..1c33df97103 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -424,7 +424,16 @@ optional AsyncWebServerRequest::find_query_value_(const char *name) return {}; } -bool AsyncWebServerRequest::hasArg(const char *name) { return this->find_query_value_(name).has_value(); } +bool AsyncWebServerRequest::hasArg(const char *name) { + if (query_has_key(this->post_query_.c_str(), this->post_query_.size(), name)) { + return true; + } + auto url_query = request_get_url_query(*this); + if (url_query.has_value()) { + return query_has_key(url_query.value().c_str(), url_query.value().size(), name); + } + return false; +} std::string AsyncWebServerRequest::arg(const char *name) { auto val = this->find_query_value_(name); From f0828928b44dd12fb6af87a696d865acdc0b74e0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 11 Feb 2026 18:26:13 -0600 Subject: [PATCH 2/5] Extract search_query_sources to deduplicate hasArg/find_query_value_ Both methods iterated post_query_ then url_query with the same pattern. Extracted a file-local template that takes a callback, avoiding duplicated request_get_url_query heap allocation logic. --- .../web_server_idf/web_server_idf.cpp | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 1c33df97103..e59263e409c 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -412,27 +412,29 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const char *name) { return param; } -optional AsyncWebServerRequest::find_query_value_(const char *name) const { - auto val = query_key_value(this->post_query_.c_str(), this->post_query_.size(), name); - if (val.has_value()) { - return val; +/// Search post_query then URL query with a callback. +/// Returns first truthy result, or value-initialized default. +template +static auto search_query_sources(const AsyncWebServerRequest &req, const std::string &post_query, const char *name, + Func func) -> decltype(func(nullptr, size_t{0}, name)) { + auto result = func(post_query.c_str(), post_query.size(), name); + if (result) { + return result; } - auto url_query = request_get_url_query(*this); + auto url_query = request_get_url_query(req); if (url_query.has_value()) { - return query_key_value(url_query.value().c_str(), url_query.value().size(), name); + return func(url_query.value().c_str(), url_query.value().size(), name); } return {}; } +optional AsyncWebServerRequest::find_query_value_(const char *name) const { + return search_query_sources(*this, this->post_query_, name, + [](const char *q, size_t len, const char *k) { return query_key_value(q, len, k); }); +} + bool AsyncWebServerRequest::hasArg(const char *name) { - if (query_has_key(this->post_query_.c_str(), this->post_query_.size(), name)) { - return true; - } - auto url_query = request_get_url_query(*this); - if (url_query.has_value()) { - return query_has_key(url_query.value().c_str(), url_query.value().size(), name); - } - return false; + return search_query_sources(*this, this->post_query_, name, query_has_key); } std::string AsyncWebServerRequest::arg(const char *name) { From e57612d52283529da08ba931b3f800835ee30f0a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 11 Feb 2026 18:27:55 -0600 Subject: [PATCH 3/5] Avoid heap allocation for URL query in hasArg/arg Replace request_get_url_query (returns std::string) with inline httpd_req_get_url_query_str into a SmallBufferWithHeapFallback stack buffer. Typical query strings (<256 bytes) now use zero heap allocations for parameter lookups. --- .../web_server_idf/web_server_idf.cpp | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index e59263e409c..b6cbdb9f012 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -414,27 +414,32 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const char *name) { /// Search post_query then URL query with a callback. /// Returns first truthy result, or value-initialized default. +/// Uses stack buffer for URL query to avoid heap allocation. template -static auto search_query_sources(const AsyncWebServerRequest &req, const std::string &post_query, const char *name, - Func func) -> decltype(func(nullptr, size_t{0}, name)) { +static auto search_query_sources(httpd_req_t *req, const std::string &post_query, const char *name, Func func) + -> decltype(func(nullptr, size_t{0}, name)) { auto result = func(post_query.c_str(), post_query.size(), name); if (result) { return result; } - auto url_query = request_get_url_query(req); - if (url_query.has_value()) { - return func(url_query.value().c_str(), url_query.value().size(), name); + auto len = httpd_req_get_url_query_len(req); + if (len == 0) { + return {}; } - return {}; + SmallBufferWithHeapFallback<256, char> buf(len + 1); + if (httpd_req_get_url_query_str(req, buf.get(), len + 1) != ESP_OK) { + return {}; + } + return func(buf.get(), len, name); } optional AsyncWebServerRequest::find_query_value_(const char *name) const { - return search_query_sources(*this, this->post_query_, name, + return search_query_sources(this->req_, this->post_query_, name, [](const char *q, size_t len, const char *k) { return query_key_value(q, len, k); }); } bool AsyncWebServerRequest::hasArg(const char *name) { - return search_query_sources(*this, this->post_query_, name, query_has_key); + return search_query_sources(this->req_, this->post_query_, name, query_has_key); } std::string AsyncWebServerRequest::arg(const char *name) { From 92d800412a21484ff8887f358cbaa2fbb1349c56 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 11 Feb 2026 18:35:10 -0600 Subject: [PATCH 4/5] Skip empty post_query in search_query_sources; reuse find_query_value_ in getParam - Add early return for empty post_query (common GET request path) - Refactor getParam to use find_query_value_ instead of duplicating search logic - Remove now-unused request_get_url_query (and its heap allocation) - Remove unused std::string overload of query_key_value --- esphome/components/web_server_idf/utils.cpp | 21 ------------------- esphome/components/web_server_idf/utils.h | 4 ---- .../web_server_idf/web_server_idf.cpp | 16 ++++++-------- 3 files changed, 6 insertions(+), 35 deletions(-) diff --git a/esphome/components/web_server_idf/utils.cpp b/esphome/components/web_server_idf/utils.cpp index 542a7fc0c0a..5bdd219944f 100644 --- a/esphome/components/web_server_idf/utils.cpp +++ b/esphome/components/web_server_idf/utils.cpp @@ -3,15 +3,12 @@ #include #include #include "esphome/core/helpers.h" -#include "esphome/core/log.h" #include "http_parser.h" #include "utils.h" namespace esphome::web_server_idf { -static const char *const TAG = "web_server_idf_utils"; - size_t url_decode(char *str) { char *start = str; char *ptr = str, buf; @@ -54,24 +51,6 @@ optional request_get_header(httpd_req_t *req, const char *name) { return {str}; } -optional request_get_url_query(httpd_req_t *req) { - auto len = httpd_req_get_url_query_len(req); - if (len == 0) { - return {}; - } - - std::string str; - str.resize(len); - - auto res = httpd_req_get_url_query_str(req, &str[0], len + 1); - if (res != ESP_OK) { - ESP_LOGW(TAG, "Can't get query for request: %s", esp_err_to_name(res)); - return {}; - } - - return {str}; -} - optional query_key_value(const char *query_url, size_t query_len, const char *key) { if (query_url == nullptr || query_len == 0) { return {}; diff --git a/esphome/components/web_server_idf/utils.h b/esphome/components/web_server_idf/utils.h index 8eee82696fd..027a2f7b6cb 100644 --- a/esphome/components/web_server_idf/utils.h +++ b/esphome/components/web_server_idf/utils.h @@ -13,11 +13,7 @@ size_t url_decode(char *str); bool request_has_header(httpd_req_t *req, const char *name); optional request_get_header(httpd_req_t *req, const char *name); -optional request_get_url_query(httpd_req_t *req); optional query_key_value(const char *query_url, size_t query_len, const char *key); -inline optional query_key_value(const std::string &query_url, const std::string &key) { - return query_key_value(query_url.c_str(), query_url.size(), key.c_str()); -} bool query_has_key(const char *query_url, size_t query_len, const char *key); // Helper function for case-insensitive character comparison diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index b6cbdb9f012..9ce4b3673d5 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -393,13 +393,7 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const char *name) { } // Look up value from query strings - optional val = query_key_value(this->post_query_.c_str(), this->post_query_.size(), name); - if (!val.has_value()) { - auto url_query = request_get_url_query(*this); - if (url_query.has_value()) { - val = query_key_value(url_query.value().c_str(), url_query.value().size(), name); - } - } + auto val = this->find_query_value_(name); // Don't cache misses to avoid wasting memory when handlers check for // optional parameters that don't exist in the request @@ -418,9 +412,11 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const char *name) { template static auto search_query_sources(httpd_req_t *req, const std::string &post_query, const char *name, Func func) -> decltype(func(nullptr, size_t{0}, name)) { - auto result = func(post_query.c_str(), post_query.size(), name); - if (result) { - return result; + if (!post_query.empty()) { + auto result = func(post_query.c_str(), post_query.size(), name); + if (result) { + return result; + } } auto len = httpd_req_get_url_query_len(req); if (len == 0) { From 53345724f2f5f1fd8f6c31080e8c296555190da2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 11 Feb 2026 18:38:25 -0600 Subject: [PATCH 5/5] Use fixed stack buffer for query strings bounded by CONFIG_HTTPD_MAX_URI_LEN Query strings cannot exceed the max URI length, so SmallBufferWithHeapFallback is unnecessary. Use a plain stack array instead for zero heap allocation. --- esphome/components/web_server_idf/utils.cpp | 11 ++++------- esphome/components/web_server_idf/web_server_idf.cpp | 6 +++--- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/esphome/components/web_server_idf/utils.cpp b/esphome/components/web_server_idf/utils.cpp index 5bdd219944f..d9c43bd6c7a 100644 --- a/esphome/components/web_server_idf/utils.cpp +++ b/esphome/components/web_server_idf/utils.cpp @@ -1,5 +1,4 @@ #ifdef USE_ESP32 -#include #include #include #include "esphome/core/helpers.h" @@ -56,15 +55,13 @@ optional query_key_value(const char *query_url, size_t query_len, c return {}; } - // Use stack buffer for typical query strings, heap fallback for large ones - SmallBufferWithHeapFallback<256, char> val(query_len); - - if (httpd_query_key_value(query_url, key, val.get(), query_len) != ESP_OK) { + char val[CONFIG_HTTPD_MAX_URI_LEN + 1]; + if (httpd_query_key_value(query_url, key, val, query_len) != ESP_OK) { return {}; } - url_decode(val.get()); - return {val.get()}; + url_decode(val); + return {val}; } bool query_has_key(const char *query_url, size_t query_len, const char *key) { diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 9ce4b3673d5..8e1623f0998 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -422,11 +422,11 @@ static auto search_query_sources(httpd_req_t *req, const std::string &post_query if (len == 0) { return {}; } - SmallBufferWithHeapFallback<256, char> buf(len + 1); - if (httpd_req_get_url_query_str(req, buf.get(), len + 1) != ESP_OK) { + char buf[AsyncWebServerRequest::URL_BUF_SIZE]; + if (httpd_req_get_url_query_str(req, buf, len + 1) != ESP_OK) { return {}; } - return func(buf.get(), len, name); + return func(buf, len, name); } optional AsyncWebServerRequest::find_query_value_(const char *name) const {