[http_request] Replace heavy STL containers with std::vector for headers (#14024)

This commit is contained in:
J. Nick Koston
2026-02-18 16:49:19 -06:00
committed by GitHub
parent 387f615dae
commit d90754dc0a
9 changed files with 65 additions and 62 deletions

View File

@@ -310,7 +310,7 @@ async def http_request_action_to_code(config, action_id, template_arg, args):
cg.add(var.add_request_header(key, template_))
for value in config.get(CONF_COLLECT_HEADERS, []):
cg.add(var.add_collect_header(value))
cg.add(var.add_collect_header(value.lower()))
if response_conf := config.get(CONF_ON_RESPONSE):
if capture_response:

View File

@@ -22,23 +22,15 @@ void HttpRequestComponent::dump_config() {
}
std::string HttpContainer::get_response_header(const std::string &header_name) {
auto response_headers = this->get_response_headers();
auto header_name_lower_case = str_lower_case(header_name);
if (response_headers.count(header_name_lower_case) == 0) {
ESP_LOGW(TAG, "No header with name %s found", header_name_lower_case.c_str());
return "";
} else {
auto values = response_headers[header_name_lower_case];
if (values.empty()) {
ESP_LOGE(TAG, "header with name %s returned an empty list, this shouldn't happen",
header_name_lower_case.c_str());
return "";
} else {
auto header_value = values.front();
ESP_LOGD(TAG, "Header with name %s found with value %s", header_name_lower_case.c_str(), header_value.c_str());
return header_value;
auto lower = str_lower_case(header_name);
for (const auto &entry : this->response_headers_) {
if (entry.name == lower) {
ESP_LOGD(TAG, "Header with name %s found with value %s", lower.c_str(), entry.value.c_str());
return entry.value;
}
}
ESP_LOGW(TAG, "No header with name %s found", lower.c_str());
return "";
}
} // namespace esphome::http_request

View File

@@ -80,6 +80,16 @@ inline bool is_redirect(int const status) {
*/
inline bool is_success(int const status) { return status >= HTTP_STATUS_OK && status < HTTP_STATUS_MULTIPLE_CHOICES; }
/// Check if a header name should be collected (linear scan, fine for small lists)
inline bool should_collect_header(const std::vector<std::string> &lower_case_collect_headers,
const std::string &lower_header_name) {
for (const auto &h : lower_case_collect_headers) {
if (h == lower_header_name)
return true;
}
return false;
}
/*
* HTTP Container Read Semantics
* =============================
@@ -258,20 +268,13 @@ class HttpContainer : public Parented<HttpRequestComponent> {
return !this->is_chunked_ && this->bytes_read_ >= this->content_length;
}
/**
* @brief Get response headers.
*
* @return The key is the lower case response header name, the value is the header value.
*/
std::map<std::string, std::list<std::string>> get_response_headers() { return this->response_headers_; }
std::string get_response_header(const std::string &header_name);
protected:
size_t bytes_read_{0};
bool secure_{false};
bool is_chunked_{false}; ///< True if response uses chunked transfer encoding
std::map<std::string, std::list<std::string>> response_headers_{};
std::vector<Header> response_headers_{};
};
/// Read data from HTTP container into buffer with timeout handling
@@ -333,8 +336,8 @@ class HttpRequestComponent : public Component {
return this->start(url, "GET", "", request_headers);
}
std::shared_ptr<HttpContainer> get(const std::string &url, const std::list<Header> &request_headers,
const std::set<std::string> &collect_headers) {
return this->start(url, "GET", "", request_headers, collect_headers);
const std::vector<std::string> &lower_case_collect_headers) {
return this->start(url, "GET", "", request_headers, lower_case_collect_headers);
}
std::shared_ptr<HttpContainer> post(const std::string &url, const std::string &body) {
return this->start(url, "POST", body, {});
@@ -345,29 +348,40 @@ class HttpRequestComponent : public Component {
}
std::shared_ptr<HttpContainer> post(const std::string &url, const std::string &body,
const std::list<Header> &request_headers,
const std::set<std::string> &collect_headers) {
return this->start(url, "POST", body, request_headers, collect_headers);
const std::vector<std::string> &lower_case_collect_headers) {
return this->start(url, "POST", body, request_headers, lower_case_collect_headers);
}
std::shared_ptr<HttpContainer> start(const std::string &url, const std::string &method, const std::string &body,
const std::list<Header> &request_headers) {
return this->start(url, method, body, request_headers, {});
// Call perform() directly to avoid ambiguity with the std::set overload
return this->perform(url, method, body, request_headers, {});
}
// Remove before 2027.1.0
ESPDEPRECATED("Pass collect_headers as std::vector<std::string> instead of std::set. Removed in 2027.1.0.",
"2026.7.0")
std::shared_ptr<HttpContainer> start(const std::string &url, const std::string &method, const std::string &body,
const std::list<Header> &request_headers,
const std::set<std::string> &collect_headers) {
std::vector<std::string> lower;
lower.reserve(collect_headers.size());
for (const auto &h : collect_headers) {
lower.push_back(str_lower_case(h));
}
return this->perform(url, method, body, request_headers, lower);
}
std::shared_ptr<HttpContainer> start(const std::string &url, const std::string &method, const std::string &body,
const std::list<Header> &request_headers,
const std::set<std::string> &collect_headers) {
std::set<std::string> lower_case_collect_headers;
for (const std::string &collect_header : collect_headers) {
lower_case_collect_headers.insert(str_lower_case(collect_header));
}
const std::vector<std::string> &lower_case_collect_headers) {
return this->perform(url, method, body, request_headers, lower_case_collect_headers);
}
protected:
virtual std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method,
const std::string &body, const std::list<Header> &request_headers,
const std::set<std::string> &collect_headers) = 0;
const std::vector<std::string> &lower_case_collect_headers) = 0;
const char *useragent_{nullptr};
bool follow_redirects_{};
uint16_t redirect_limit_{};
@@ -389,7 +403,7 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
this->request_headers_.insert({key, value});
}
void add_collect_header(const char *value) { this->collect_headers_.insert(value); }
void add_collect_header(const char *value) { this->lower_case_collect_headers_.push_back(value); }
void add_json(const char *key, TemplatableValue<std::string, Ts...> value) { this->json_.insert({key, value}); }
@@ -431,7 +445,7 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
}
auto container = this->parent_->start(this->url_.value(x...), this->method_.value(x...), body, request_headers,
this->collect_headers_);
this->lower_case_collect_headers_);
auto captured_args = std::make_tuple(x...);
@@ -494,7 +508,7 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
void encode_json_func_(Ts... x, JsonObject root) { this->json_func_(x..., root); }
HttpRequestComponent *parent_;
std::map<const char *, TemplatableValue<const char *, Ts...>> request_headers_{};
std::set<std::string> collect_headers_{"content-type", "content-length"};
std::vector<std::string> lower_case_collect_headers_{"content-type", "content-length"};
std::map<const char *, TemplatableValue<std::string, Ts...>> json_{};
std::function<void(Ts..., JsonObject)> json_func_{nullptr};
#ifdef USE_HTTP_REQUEST_RESPONSE

View File

@@ -27,7 +27,7 @@ static constexpr int ESP8266_SSL_ERR_OOM = -1000;
std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &url, const std::string &method,
const std::string &body,
const std::list<Header> &request_headers,
const std::set<std::string> &collect_headers) {
const std::vector<std::string> &lower_case_collect_headers) {
if (!network::is_connected()) {
this->status_momentary_error("failed", 1000);
ESP_LOGW(TAG, "HTTP Request failed; Not connected to network");
@@ -107,9 +107,9 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
}
// returned needed headers must be collected before the requests
const char *header_keys[collect_headers.size()];
const char *header_keys[lower_case_collect_headers.size()];
int index = 0;
for (auto const &header_name : collect_headers) {
for (auto const &header_name : lower_case_collect_headers) {
header_keys[index++] = header_name.c_str();
}
container->client_.collectHeaders(header_keys, index);
@@ -160,14 +160,14 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
// Still return the container, so it can be used to get the status code and error message
}
container->response_headers_ = {};
container->response_headers_.clear();
auto header_count = container->client_.headers();
for (int i = 0; i < header_count; i++) {
const std::string header_name = str_lower_case(container->client_.headerName(i).c_str());
if (collect_headers.count(header_name) > 0) {
if (should_collect_header(lower_case_collect_headers, header_name)) {
std::string header_value = container->client_.header(i).c_str();
ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str());
container->response_headers_[header_name].push_back(header_value);
container->response_headers_.push_back({header_name, header_value});
}
}

View File

@@ -50,7 +50,7 @@ class HttpRequestArduino : public HttpRequestComponent {
protected:
std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
const std::list<Header> &request_headers,
const std::set<std::string> &collect_headers) override;
const std::vector<std::string> &lower_case_collect_headers) override;
};
} // namespace esphome::http_request

View File

@@ -19,7 +19,7 @@ static const char *const TAG = "http_request.host";
std::shared_ptr<HttpContainer> HttpRequestHost::perform(const std::string &url, const std::string &method,
const std::string &body,
const std::list<Header> &request_headers,
const std::set<std::string> &response_headers) {
const std::vector<std::string> &lower_case_collect_headers) {
if (!network::is_connected()) {
this->status_momentary_error("failed", 1000);
ESP_LOGW(TAG, "HTTP Request failed; Not connected to network");
@@ -116,8 +116,8 @@ std::shared_ptr<HttpContainer> HttpRequestHost::perform(const std::string &url,
for (auto header : response.headers) {
ESP_LOGD(TAG, "Header: %s: %s", header.first.c_str(), header.second.c_str());
auto lower_name = str_lower_case(header.first);
if (response_headers.find(lower_name) != response_headers.end()) {
container->response_headers_[lower_name].emplace_back(header.second);
if (should_collect_header(lower_case_collect_headers, lower_name)) {
container->response_headers_.push_back({lower_name, header.second});
}
}
container->duration_ms = millis() - start;

View File

@@ -20,7 +20,7 @@ class HttpRequestHost : public HttpRequestComponent {
public:
std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
const std::list<Header> &request_headers,
const std::set<std::string> &response_headers) override;
const std::vector<std::string> &lower_case_collect_headers) override;
void set_ca_path(const char *ca_path) { this->ca_path_ = ca_path; }
protected:

View File

@@ -19,8 +19,8 @@ namespace esphome::http_request {
static const char *const TAG = "http_request.idf";
struct UserData {
const std::set<std::string> &collect_headers;
std::map<std::string, std::list<std::string>> response_headers;
const std::vector<std::string> &lower_case_collect_headers;
std::vector<Header> &response_headers;
};
void HttpRequestIDF::dump_config() {
@@ -38,10 +38,10 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) {
switch (evt->event_id) {
case HTTP_EVENT_ON_HEADER: {
const std::string header_name = str_lower_case(evt->header_key);
if (user_data->collect_headers.count(header_name)) {
if (should_collect_header(user_data->lower_case_collect_headers, header_name)) {
const std::string header_value = evt->header_value;
ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str());
user_data->response_headers[header_name].push_back(header_value);
user_data->response_headers.push_back({header_name, header_value});
}
break;
}
@@ -55,7 +55,7 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) {
std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, const std::string &method,
const std::string &body,
const std::list<Header> &request_headers,
const std::set<std::string> &collect_headers) {
const std::vector<std::string> &lower_case_collect_headers) {
if (!network::is_connected()) {
this->status_momentary_error("failed", 1000);
ESP_LOGE(TAG, "HTTP Request failed; Not connected to network");
@@ -110,8 +110,6 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
watchdog::WatchdogManager wdm(this->get_watchdog_timeout());
config.event_handler = http_event_handler;
auto user_data = UserData{collect_headers, {}};
config.user_data = static_cast<void *>(&user_data);
esp_http_client_handle_t client = esp_http_client_init(&config);
@@ -120,6 +118,9 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
container->set_secure(secure);
auto user_data = UserData{lower_case_collect_headers, container->response_headers_};
esp_http_client_set_user_data(client, static_cast<void *>(&user_data));
for (const auto &header : request_headers) {
esp_http_client_set_header(client, header.name.c_str(), header.value.c_str());
}
@@ -164,7 +165,6 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
container->feed_wdt();
container->status_code = esp_http_client_get_status_code(client);
container->feed_wdt();
container->set_response_headers(user_data.response_headers);
container->duration_ms = millis() - start;
if (is_success(container->status_code)) {
return container;

View File

@@ -21,11 +21,8 @@ class HttpContainerIDF : public HttpContainer {
/// @brief Feeds the watchdog timer if the executing task has one attached
void feed_wdt();
void set_response_headers(std::map<std::string, std::list<std::string>> &response_headers) {
this->response_headers_ = std::move(response_headers);
}
protected:
friend class HttpRequestIDF;
esp_http_client_handle_t client_;
};
@@ -41,7 +38,7 @@ class HttpRequestIDF : public HttpRequestComponent {
protected:
std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
const std::list<Header> &request_headers,
const std::set<std::string> &collect_headers) override;
const std::vector<std::string> &lower_case_collect_headers) override;
// if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE
uint16_t buffer_size_rx_{};
uint16_t buffer_size_tx_{};