[web_server] Switch from getParam to arg API to eliminate heap allocations (#13942)

This commit is contained in:
J. Nick Koston
2026-02-12 11:04:41 -06:00
committed by GitHub
parent 0e1433329d
commit cde8b66719
7 changed files with 134 additions and 130 deletions

View File

@@ -47,8 +47,8 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) {
request->send(stream);
}
void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
std::string ssid = request->arg("ssid").c_str(); // NOLINT(readability-redundant-string-cstr)
std::string psk = request->arg("psk").c_str(); // NOLINT(readability-redundant-string-cstr)
const auto &ssid = request->arg("ssid");
const auto &psk = request->arg("psk");
ESP_LOGI(TAG,
"Requested WiFi Settings Change:\n"
" SSID='%s'\n"
@@ -56,10 +56,10 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
ssid.c_str(), psk.c_str());
#ifdef USE_ESP8266
// ESP8266 is single-threaded, call directly
wifi::global_wifi_component->save_wifi_sta(ssid, psk);
wifi::global_wifi_component->save_wifi_sta(ssid.c_str(), psk.c_str());
#else
// Defer save to main loop thread to avoid NVS operations from HTTP thread
this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid, psk); });
this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid.c_str(), psk.c_str()); });
#endif
request->redirect(ESPHOME_F("/?save"));
}

View File

@@ -585,8 +585,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,10 +862,10 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
}
auto call = is_on ? obj->turn_on() : obj->turn_off();
parse_int_param_(request, ESPHOME_F("speed_level"), call, &decltype(call)::set_speed);
parse_num_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:
@@ -1042,14 +1041,14 @@ 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;
}
parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position);
parse_float_param_(request, ESPHOME_F("tilt"), call, &decltype(call)::set_tilt);
parse_num_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position);
parse_num_param_(request, ESPHOME_F("tilt"), call, &decltype(call)::set_tilt);
DEFER_ACTION(call, call.perform());
request->send(200);
@@ -1108,7 +1107,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM
}
auto call = obj->make_call();
parse_float_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value);
parse_num_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value);
DEFER_ACTION(call, call.perform());
request->send(200);
@@ -1176,12 +1175,13 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat
auto call = obj->make_call();
if (!request->hasParam(ESPHOME_F("value"))) {
const auto &value = request->arg(ESPHOME_F("value"));
// Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
if (value.length() == 0) { // NOLINT(readability-container-size-empty)
request->send(409);
return;
}
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_date);
call.set_date(value.c_str(), value.length());
DEFER_ACTION(call, call.perform());
request->send(200);
@@ -1236,12 +1236,13 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat
auto call = obj->make_call();
if (!request->hasParam(ESPHOME_F("value"))) {
const auto &value = request->arg(ESPHOME_F("value"));
// Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
if (value.length() == 0) { // NOLINT(readability-container-size-empty)
request->send(409);
return;
}
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_time);
call.set_time(value.c_str(), value.length());
DEFER_ACTION(call, call.perform());
request->send(200);
@@ -1295,12 +1296,13 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur
auto call = obj->make_call();
if (!request->hasParam(ESPHOME_F("value"))) {
const auto &value = request->arg(ESPHOME_F("value"));
// Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
if (value.length() == 0) { // NOLINT(readability-container-size-empty)
request->send(409);
return;
}
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_datetime);
call.set_datetime(value.c_str(), value.length());
DEFER_ACTION(call, call.perform());
request->send(200);
@@ -1479,10 +1481,14 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
parse_string_param_(request, ESPHOME_F("swing_mode"), call, &decltype(call)::set_swing_mode);
// Parse temperature parameters
parse_float_param_(request, ESPHOME_F("target_temperature_high"), call,
&decltype(call)::set_target_temperature_high);
parse_float_param_(request, ESPHOME_F("target_temperature_low"), call, &decltype(call)::set_target_temperature_low);
parse_float_param_(request, ESPHOME_F("target_temperature"), call, &decltype(call)::set_target_temperature);
// static_cast needed to disambiguate overloaded setters (float vs optional<float>)
using ClimateCall = decltype(call);
parse_num_param_(request, ESPHOME_F("target_temperature_high"), call,
static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature_high));
parse_num_param_(request, ESPHOME_F("target_temperature_low"), call,
static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature_low));
parse_num_param_(request, ESPHOME_F("target_temperature"), call,
static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature));
DEFER_ACTION(call, call.perform());
request->send(200);
@@ -1723,12 +1729,12 @@ 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;
}
parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position);
parse_num_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position);
DEFER_ACTION(call, call.perform());
request->send(200);
@@ -1872,12 +1878,12 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons
parse_string_param_(request, ESPHOME_F("mode"), base_call, &water_heater::WaterHeaterCall::set_mode);
// Parse temperature parameters
parse_float_param_(request, ESPHOME_F("target_temperature"), base_call,
&water_heater::WaterHeaterCall::set_target_temperature);
parse_float_param_(request, ESPHOME_F("target_temperature_low"), base_call,
&water_heater::WaterHeaterCall::set_target_temperature_low);
parse_float_param_(request, ESPHOME_F("target_temperature_high"), base_call,
&water_heater::WaterHeaterCall::set_target_temperature_high);
parse_num_param_(request, ESPHOME_F("target_temperature"), base_call,
&water_heater::WaterHeaterCall::set_target_temperature);
parse_num_param_(request, ESPHOME_F("target_temperature_low"), base_call,
&water_heater::WaterHeaterCall::set_target_temperature_low);
parse_num_param_(request, ESPHOME_F("target_temperature_high"), base_call,
&water_heater::WaterHeaterCall::set_target_temperature_high);
// Parse away mode parameter
parse_bool_param_(request, ESPHOME_F("away"), base_call, &water_heater::WaterHeaterCall::set_away);
@@ -1981,16 +1987,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());
{
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());
{
auto value = parse_number<uint32_t>(request->arg(ESPHOME_F("repeat_count")).c_str());
if (value.has_value()) {
call.set_repeat_count(*value);
}
@@ -1998,18 +2004,12 @@ 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"))) {
request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Missing 'data' parameter"));
return;
}
const auto &data_arg = request->arg(ESPHOME_F("data"));
// .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)
// Validate base64url is not empty
if (encoded.empty()) {
request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Empty 'data' parameter"));
// Validate base64url is not empty (also catches missing parameter since arg() returns empty string)
// Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
if (data_arg.length() == 0) { // NOLINT(readability-container-size-empty)
request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Missing or empty 'data' parameter"));
return;
}
@@ -2017,7 +2017,7 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur
// it outlives the call - set_raw_timings_base64url stores a pointer, so the string
// must remain valid until perform() completes.
// ESP8266 also needs this because ESPAsyncWebServer callbacks run in "sys" context.
this->defer([call, encoded = std::move(encoded)]() mutable {
this->defer([call, encoded = std::string(data_arg.c_str(), data_arg.length())]() mutable {
call.set_raw_timings_base64url(encoded);
call.perform();
});

View File

@@ -513,11 +513,9 @@ 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 (value.has_value()) {
(call.*setter)(*value / scale);
}
auto value = parse_number<float>(request->arg(param_name).c_str());
if (value.has_value()) {
(call.*setter)(*value / scale);
}
}
@@ -525,34 +523,19 @@ 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 (value.has_value()) {
(call.*setter)(*value * scale);
}
auto value = parse_number<uint32_t>(request->arg(param_name).c_str());
if (value.has_value()) {
(call.*setter)(*value * scale);
}
}
#endif
// 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 (value.has_value()) {
(call.*setter)(*value);
}
}
}
// 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 (value.has_value()) {
(call.*setter)(*value);
}
// Generic helper to parse and apply a numeric parameter
template<typename NumT, typename T, typename Ret>
void parse_num_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(NumT)) {
auto value = parse_number<NumT>(request->arg(param_name).c_str());
if (value.has_value()) {
(call.*setter)(*value);
}
}
@@ -560,10 +543,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)
(call.*setter)(value);
if (request->hasArg(param_name)) {
const auto &value = request->arg(param_name);
(call.*setter)(std::string(value.c_str(), value.length()));
}
}
@@ -573,8 +555,9 @@ 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();
const auto &param_value = request->arg(param_name);
// Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
if (param_value.length() > 0) { // NOLINT(readability-container-size-empty)
// First check on/off (default), then true/false (custom)
auto val = parse_on_off(param_value.c_str());
if (val == PARSE_NONE) {

View File

@@ -1,17 +1,13 @@
#ifdef USE_ESP32
#include <memory>
#include <cstring>
#include <cctype>
#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,32 +50,15 @@ optional<std::string> request_get_header(httpd_req_t *req, const char *name) {
return {str};
}
optional<std::string> 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<std::string> query_key_value(const char *query_url, size_t query_len, const char *key) {
if (query_url == nullptr || query_len == 0) {
return {};
}
// Use stack buffer for typical query strings, heap fallback for large ones
SmallBufferWithHeapFallback<256, char> val(query_len);
// Value can't exceed query_len. Use small stack buffer for typical values,
// heap fallback for long ones (e.g. base64 IR data) to limit stack usage
// since callers may also have stack buffers for the query string.
SmallBufferWithHeapFallback<128, char> val(query_len);
if (httpd_query_key_value(query_url, key, val.get(), query_len) != ESP_OK) {
return {};
}
@@ -88,6 +67,18 @@ optional<std::string> 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 found, ESP_ERR_HTTPD_RESULT_TRUNC if found
// but value truncated (expected with 1-byte buffer), or other errors for invalid input
auto err = httpd_query_key_value(query_url, key, buf, sizeof(buf));
return err == ESP_OK || err == ESP_ERR_HTTPD_RESULT_TRUNC;
}
// 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++) {

View File

@@ -13,11 +13,8 @@ size_t url_decode(char *str);
bool request_has_header(httpd_req_t *req, const char *name);
optional<std::string> request_get_header(httpd_req_t *req, const char *name);
optional<std::string> request_get_url_query(httpd_req_t *req);
optional<std::string> query_key_value(const char *query_url, size_t query_len, const char *key);
inline optional<std::string> 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); }

View File

@@ -393,13 +393,7 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const char *name) {
}
// Look up value from query strings
optional<std::string> 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
@@ -412,6 +406,50 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const char *name) {
return param;
}
/// Search post_query then URL query with a callback.
/// Returns first truthy result, or value-initialized default.
/// URL query is accessed directly from req->uri (same pattern as url_to()).
template<typename Func>
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)) {
if (!post_query.empty()) {
auto result = func(post_query.c_str(), post_query.size(), name);
if (result) {
return result;
}
}
// Use httpd API for query length, then access string directly from URI.
// http_parser identifies components by offset/length without modifying the URI string.
// This is the same pattern used by url_to().
auto len = httpd_req_get_url_query_len(req);
if (len == 0) {
return {};
}
const char *query = strchr(req->uri, '?');
if (query == nullptr) {
return {};
}
query++; // skip '?'
return func(query, len, name);
}
optional<std::string> AsyncWebServerRequest::find_query_value_(const char *name) const {
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->req_, this->post_query_, name, query_has_key);
}
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) const;
std::vector<AsyncWebParameter *> params_;
std::string post_query_;
AsyncWebServerRequest(httpd_req_t *req) : req_(req) {}