[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.
This commit is contained in:
J. Nick Koston
2026-02-11 17:33:11 -06:00
parent e12ed08487
commit 4f3c95ced2
4 changed files with 54 additions and 39 deletions

View File

@@ -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<uint32_t>(request->getParam(ESPHOME_F("carrier_frequency"))->value().c_str());
if (request->hasArg(ESPHOME_F("carrier_frequency"))) {
auto value = parse_number<uint32_t>(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<uint32_t>(request->getParam(ESPHOME_F("repeat_count"))->value().c_str());
if (request->hasArg(ESPHOME_F("repeat_count"))) {
auto value = parse_number<uint32_t>(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()) {

View File

@@ -513,8 +513,8 @@ class WebServer : public Controller,
template<typename T, typename Ret>
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<float>(request->getParam(param_name)->value().c_str());
if (request->hasArg(param_name)) {
auto value = parse_number<float>(request->arg(param_name).c_str());
if (value.has_value()) {
(call.*setter)(*value / scale);
}
@@ -525,8 +525,8 @@ class WebServer : public Controller,
template<typename T, typename Ret>
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<uint32_t>(request->getParam(param_name)->value().c_str());
if (request->hasArg(param_name)) {
auto value = parse_number<uint32_t>(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<typename T, typename Ret>
void parse_float_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(float)) {
if (request->hasParam(param_name)) {
auto value = parse_number<float>(request->getParam(param_name)->value().c_str());
if (request->hasArg(param_name)) {
auto value = parse_number<float>(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<typename T, typename Ret>
void parse_int_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(int)) {
if (request->hasParam(param_name)) {
auto value = parse_number<int>(request->getParam(param_name)->value().c_str());
if (request->hasArg(param_name)) {
auto value = parse_number<int>(request->arg(param_name).c_str());
if (value.has_value()) {
(call.*setter)(*value);
}
@@ -560,9 +560,9 @@ class WebServer : public Controller,
template<typename T, typename Ret>
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<typename T, typename Ret>
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) {

View File

@@ -411,6 +411,28 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const char *name) {
return param;
}
optional<std::string> 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);
}

View File

@@ -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<std::string> find_query_value_(const char *name);
std::vector<AsyncWebParameter *> params_;
std::string post_query_;
AsyncWebServerRequest(httpd_req_t *req) : req_(req) {}