From 4f3c95ced2139fb88ddc9d97a50f1507ac2b1862 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 11 Feb 2026 17:33:11 -0600 Subject: [PATCH] [web_server] Switch from getParam to arg API to eliminate heap allocations Switch all web_server callers from getParam()/hasParam() to arg()/hasArg(). Both APIs exist on Arduino ESPAsyncWebServer and our IDF implementation. On the IDF side, getParam() allocated a new AsyncWebParameter on the heap for every successful lookup, cached it in a vector, and required cleanup in the destructor. No caller ever held the pointer or called getParam twice with the same name - every use was just getParam("x")->value() immediately. Rewrite IDF arg()/hasArg() to call query_key_value() directly, bypassing getParam entirely. The linker strips the now-unreferenced getParam, AsyncWebParameter, and cache machinery. Saves ~348 bytes flash on ESP32-IDF, ~272 bytes on ESP8266 Arduino. --- esphome/components/web_server/web_server.cpp | 34 +++++++++---------- esphome/components/web_server/web_server.h | 26 +++++++------- .../web_server_idf/web_server_idf.cpp | 22 ++++++++++++ .../web_server_idf/web_server_idf.h | 11 ++---- 4 files changed, 54 insertions(+), 39 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index dfd602be6b..eced4cddd2 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -583,8 +583,7 @@ static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const c // Helper to get request detail parameter static JsonDetail get_request_detail(AsyncWebServerRequest *request) { - auto *param = request->getParam(ESPHOME_F("detail")); - return (param && param->value() == "all") ? DETAIL_ALL : DETAIL_STATE; + return request->arg(ESPHOME_F("detail")) == "all" ? DETAIL_ALL : DETAIL_STATE; } #ifdef USE_SENSOR @@ -863,8 +862,8 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc parse_int_param_(request, ESPHOME_F("speed_level"), call, &decltype(call)::set_speed); - if (request->hasParam(ESPHOME_F("oscillation"))) { - auto speed = request->getParam(ESPHOME_F("oscillation"))->value(); + if (request->hasArg(ESPHOME_F("oscillation"))) { + auto speed = request->arg(ESPHOME_F("oscillation")); auto val = parse_on_off(speed.c_str()); switch (val) { case PARSE_ON: @@ -1040,8 +1039,8 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa } auto traits = obj->get_traits(); - if ((request->hasParam(ESPHOME_F("position")) && !traits.get_supports_position()) || - (request->hasParam(ESPHOME_F("tilt")) && !traits.get_supports_tilt())) { + if ((request->hasArg(ESPHOME_F("position")) && !traits.get_supports_position()) || + (request->hasArg(ESPHOME_F("tilt")) && !traits.get_supports_tilt())) { request->send(409); return; } @@ -1174,7 +1173,7 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat auto call = obj->make_call(); - if (!request->hasParam(ESPHOME_F("value"))) { + if (!request->hasArg(ESPHOME_F("value"))) { request->send(409); return; } @@ -1234,7 +1233,7 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat auto call = obj->make_call(); - if (!request->hasParam(ESPHOME_F("value"))) { + if (!request->hasArg(ESPHOME_F("value"))) { request->send(409); return; } @@ -1293,7 +1292,7 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur auto call = obj->make_call(); - if (!request->hasParam(ESPHOME_F("value"))) { + if (!request->hasArg(ESPHOME_F("value"))) { request->send(409); return; } @@ -1721,7 +1720,7 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa } auto traits = obj->get_traits(); - if (request->hasParam(ESPHOME_F("position")) && !traits.get_supports_position()) { + if (request->hasArg(ESPHOME_F("position")) && !traits.get_supports_position()) { request->send(409); return; } @@ -1979,16 +1978,16 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur auto call = obj->make_call(); // Parse carrier frequency (optional) - if (request->hasParam(ESPHOME_F("carrier_frequency"))) { - auto value = parse_number(request->getParam(ESPHOME_F("carrier_frequency"))->value().c_str()); + if (request->hasArg(ESPHOME_F("carrier_frequency"))) { + auto value = parse_number(request->arg(ESPHOME_F("carrier_frequency")).c_str()); if (value.has_value()) { call.set_carrier_frequency(*value); } } // Parse repeat count (optional, defaults to 1) - if (request->hasParam(ESPHOME_F("repeat_count"))) { - auto value = parse_number(request->getParam(ESPHOME_F("repeat_count"))->value().c_str()); + if (request->hasArg(ESPHOME_F("repeat_count"))) { + auto value = parse_number(request->arg(ESPHOME_F("repeat_count")).c_str()); if (value.has_value()) { call.set_repeat_count(*value); } @@ -1996,14 +1995,13 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur // Parse base64url-encoded raw timings (required) // Base64url is URL-safe: uses A-Za-z0-9-_ (no special characters needing escaping) - if (!request->hasParam(ESPHOME_F("data"))) { + if (!request->hasArg(ESPHOME_F("data"))) { request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Missing 'data' parameter")); return; } - // .c_str() is required for Arduino framework where value() returns Arduino String instead of std::string - std::string encoded = - request->getParam(ESPHOME_F("data"))->value().c_str(); // NOLINT(readability-redundant-string-cstr) + // .c_str() is required for Arduino framework where arg() returns Arduino String instead of std::string + std::string encoded = request->arg(ESPHOME_F("data")).c_str(); // NOLINT(readability-redundant-string-cstr) // Validate base64url is not empty if (encoded.empty()) { diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index ce09ebf7a9..081bf75aa7 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -513,8 +513,8 @@ class WebServer : public Controller, template void parse_light_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(float), float scale = 1.0f) { - if (request->hasParam(param_name)) { - auto value = parse_number(request->getParam(param_name)->value().c_str()); + if (request->hasArg(param_name)) { + auto value = parse_number(request->arg(param_name).c_str()); if (value.has_value()) { (call.*setter)(*value / scale); } @@ -525,8 +525,8 @@ class WebServer : public Controller, template void parse_light_param_uint_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(uint32_t), uint32_t scale = 1) { - if (request->hasParam(param_name)) { - auto value = parse_number(request->getParam(param_name)->value().c_str()); + if (request->hasArg(param_name)) { + auto value = parse_number(request->arg(param_name).c_str()); if (value.has_value()) { (call.*setter)(*value * scale); } @@ -537,8 +537,8 @@ class WebServer : public Controller, // Generic helper to parse and apply a float parameter template void parse_float_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(float)) { - if (request->hasParam(param_name)) { - auto value = parse_number(request->getParam(param_name)->value().c_str()); + if (request->hasArg(param_name)) { + auto value = parse_number(request->arg(param_name).c_str()); if (value.has_value()) { (call.*setter)(*value); } @@ -548,8 +548,8 @@ class WebServer : public Controller, // Generic helper to parse and apply an int parameter template void parse_int_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(int)) { - if (request->hasParam(param_name)) { - auto value = parse_number(request->getParam(param_name)->value().c_str()); + if (request->hasArg(param_name)) { + auto value = parse_number(request->arg(param_name).c_str()); if (value.has_value()) { (call.*setter)(*value); } @@ -560,9 +560,9 @@ class WebServer : public Controller, template void parse_string_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(const std::string &)) { - if (request->hasParam(param_name)) { - // .c_str() is required for Arduino framework where value() returns Arduino String instead of std::string - std::string value = request->getParam(param_name)->value().c_str(); // NOLINT(readability-redundant-string-cstr) + if (request->hasArg(param_name)) { + // .c_str() is required for Arduino framework where arg() returns Arduino String instead of std::string + std::string value = request->arg(param_name).c_str(); // NOLINT(readability-redundant-string-cstr) (call.*setter)(value); } } @@ -573,8 +573,8 @@ class WebServer : public Controller, // Invalid values are ignored (setter not called) template void parse_bool_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(bool)) { - if (request->hasParam(param_name)) { - auto param_value = request->getParam(param_name)->value(); + if (request->hasArg(param_name)) { + auto param_value = request->arg(param_name); // First check on/off (default), then true/false (custom) auto val = parse_on_off(param_value.c_str()); if (val == PARSE_NONE) { diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 2e07fb6e0a..2754363c96 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -411,6 +411,28 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const char *name) { return param; } +optional AsyncWebServerRequest::find_query_value_(const char *name) { + auto val = query_key_value(this->post_query_.c_str(), this->post_query_.size(), name); + if (val.has_value()) { + return val; + } + auto url_query = request_get_url_query(*this); + if (url_query.has_value()) { + return query_key_value(url_query.value().c_str(), url_query.value().size(), name); + } + return {}; +} + +bool AsyncWebServerRequest::hasArg(const char *name) { return this->find_query_value_(name).has_value(); } + +std::string AsyncWebServerRequest::arg(const char *name) { + auto val = this->find_query_value_(name); + if (val.has_value()) { + return std::move(val.value()); + } + return {}; +} + void AsyncWebServerResponse::addHeader(const char *name, const char *value) { httpd_resp_set_hdr(*this->req_, name, value); } diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 6a409de74e..f4c92bb8c4 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -170,14 +170,8 @@ class AsyncWebServerRequest { AsyncWebParameter *getParam(const std::string &name) { return this->getParam(name.c_str()); } // NOLINTNEXTLINE(readability-identifier-naming) - bool hasArg(const char *name) { return this->hasParam(name); } - std::string arg(const char *name) { - auto *param = this->getParam(name); - if (param) { - return param->value(); - } - return {}; - } + bool hasArg(const char *name); + std::string arg(const char *name); std::string arg(const std::string &name) { return this->arg(name.c_str()); } operator httpd_req_t *() const { return this->req_; } @@ -192,6 +186,7 @@ class AsyncWebServerRequest { // is faster than tree/hash overhead. AsyncWebParameter stores both name and value to avoid // duplicate storage. Only successful lookups are cached to prevent cache pollution when // handlers check for optional parameters that don't exist. + optional find_query_value_(const char *name); std::vector params_; std::string post_query_; AsyncWebServerRequest(httpd_req_t *req) : req_(req) {}