1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-12 02:32:15 +00:00

Compare commits

...

24 Commits

Author SHA1 Message Date
J. Nick Koston
220c2acf0e Check for ESP_OK or ESP_ERR_HTTPD_RESULT_TRUNC explicitly in query_has_key
Instead of treating any non-ESP_ERR_NOT_FOUND result as key-present,
explicitly check for the two success codes. This avoids false positives
from unexpected error codes like ESP_ERR_INVALID_ARG.
2026-02-11 19:15:43 -06:00
J. Nick Koston
52461f10e7 Use httpd_req_get_url_query_len instead of strlen for query length
The parsed URL already has the query length available via the httpd API.
Avoids redundant strlen over the query string.
2026-02-11 19:06:28 -06:00
J. Nick Koston
2348ad2a03 Access URL query string directly from req->uri instead of stack copy
The query string already lives in req->uri. Access it via strchr('?')
instead of copying into a 513-byte stack buffer via httpd_req_get_url_query_str.
This is the same pattern url_to() uses — http_parser identifies URI
components by offset/length without modifying the source string.

Eliminates 513 bytes of stack usage on the httpd task, leaving only
the 128-byte value extraction buffer in query_key_value.
2026-02-11 19:06:02 -06:00
J. Nick Koston
2b5bd961ef Fix stack overflow: use small stack buffer with heap fallback in query_key_value
Revert direct req->uri access (unsafe — ESP-IDF uses parsed offsets,
not strchr). Instead fix the root cause: query_key_value used a full
CONFIG_HTTPD_MAX_URI_LEN+1 (513 byte) stack buffer while
search_query_sources had another 513 byte buffer on the stack
simultaneously, totaling ~1KB on the httpd thread's limited stack.

Use SmallBufferWithHeapFallback<128> for the value extraction buffer.
128 bytes covers typical parameter values on stack; longer values
(e.g. base64 IR data) fall back to heap.
2026-02-11 18:53:58 -06:00
J. Nick Koston
5a88bb6d8a Fix stack overflow: access URL query directly from req->uri
search_query_sources was copying the URL query string into a 513-byte
stack buffer, then query_key_value added another 513-byte buffer for
the extracted value — 1026 bytes simultaneously on the httpd thread's
limited stack, causing a crash in lwip_select.

The query string already lives in req->uri after the '?'. Access it
directly via pointer instead of copying, eliminating one buffer entirely.
2026-02-11 18:51:45 -06:00
J. Nick Koston
53345724f2 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.
2026-02-11 18:38:25 -06:00
J. Nick Koston
92d800412a 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
2026-02-11 18:35:10 -06:00
J. Nick Koston
e57612d522 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.
2026-02-11 18:27:55 -06:00
J. Nick Koston
f0828928b4 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.
2026-02-11 18:26:13 -06:00
J. Nick Koston
e42cc2e394 Add query_has_key for efficient hasArg without string allocation
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.
2026-02-11 18:23:38 -06:00
J. Nick Koston
592d5ec24c Restore hasArg guard in parse_string_param_
Empty string is a valid value for string params (e.g. select
options). Must use hasArg to distinguish missing from empty.
2026-02-11 18:17:24 -06:00
J. Nick Koston
91a0b0989e Simplify captive_portal lambda capture
Capture auto type directly and use c_str() overload of
save_wifi_sta. Works on both std::string and Arduino String.
2026-02-11 18:14:46 -06:00
J. Nick Koston
f35dfefdf3 Remove readability-redundant-string-cstr NOLINTs from captive_portal
Use const auto& to bind arg() result directly. Construct
std::string only in deferred lambda capture where ownership
is needed.
2026-02-11 18:13:32 -06:00
J. Nick Koston
892804e02e Remove remaining readability-redundant-string-cstr NOLINTs
Use const auto& to bind arg() result directly, avoiding
unnecessary std::string intermediate copies. Construct
std::string only where needed (lambda capture, setter call).
2026-02-11 18:12:47 -06:00
J. Nick Koston
5585b5967e Avoid std::string copy in date/time/datetime handlers
Use const auto& to bind directly to arg() result (std::string on
IDF, Arduino String on Arduino) and pass c_str()/length() to the
setter. No intermediate std::string copy needed.
2026-02-11 18:11:49 -06:00
J. Nick Koston
73867c62be Pass c_str() and size() directly to date/time/datetime setters
These setters have (const char*, size_t) overloads that do the
actual work. Skip the std::string& overload indirection.
2026-02-11 18:10:43 -06:00
J. Nick Koston
920f84fa1d Eliminate double lookups in parse_string_param_ and date/time/datetime handlers
- parse_string_param_: use arg() and check empty instead of hasArg+arg
- date/time/datetime: inline arg() call to avoid redundant hasArg+parse_string_param_ triple lookup
2026-02-11 18:08:23 -06:00
J. Nick Koston
c2bb55ff5d Add NOLINT for length() > 0 cross-platform check
Arduino String has isEmpty() not empty(). Using length() > 0
works on both std::string and Arduino String.
2026-02-11 18:03:32 -06:00
J. Nick Koston
c6b51d3434 Fix clang-tidy: disambiguate overloaded climate setters
ClimateCall has overloaded set_target_temperature*(float) and
set_target_temperature*(optional<float>), so the compiler can't
infer NumT. Use static_cast to select the float overload.
2026-02-11 18:02:56 -06:00
J. Nick Koston
f638b65f1e Fix Arduino build: use length() instead of empty()
Arduino String has isEmpty() not empty(). Use length() > 0
which works on both std::string and Arduino String.
2026-02-11 17:57:49 -06:00
J. Nick Koston
f92725f76e Eliminate double query lookups and unify numeric parse helpers
- Mark find_query_value_ as const
- Remove redundant hasArg() guards where parse_number() already
  handles empty strings (returns nullopt)
- Use !empty() instead of hasArg() for parse_bool_param_
- Merge parse_float_param_ and parse_int_param_ into single
  parse_num_param_ template
- Combine missing/empty checks for IR data parameter
2026-02-11 17:54:56 -06:00
J. Nick Koston
2a89088bc3 Merge branch 'dev' into web_server_use_arg_api 2026-02-11 17:43:10 -06:00
J. Nick Koston
db831ebee0 Fix clang-tidy: use const auto& for arg() return value
On Arduino, arg() returns const String&, so auto copies unnecessarily.
const auto& binds to the reference on Arduino and extends the temporary
lifetime on IDF.
2026-02-11 17:37:09 -06:00
J. Nick Koston
4f3c95ced2 [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.
2026-02-11 17:33:11 -06:00
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

@@ -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
@@ -861,10 +860,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:
@@ -1040,14 +1039,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);
@@ -1106,7 +1105,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);
@@ -1174,12 +1173,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);
@@ -1234,12 +1234,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);
@@ -1293,12 +1294,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);
@@ -1477,10 +1479,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);
@@ -1721,12 +1727,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);
@@ -1870,12 +1876,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);
@@ -1979,16 +1985,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);
}
@@ -1996,18 +2002,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;
}
@@ -2015,7 +2015,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) {}