[http_request] Keep action request headers in a shared flash table (#20225)

This commit is contained in:
J. Nick Koston
2026-10-11 14:57:06 -10:00
committed by GitHub
parent 40425610d5
commit da0c59a57c
4 changed files with 146 additions and 12 deletions
+22 -6
View File
@@ -39,6 +39,7 @@ HttpRequestIDF = http_request_ns.class_("HttpRequestIDF", HttpRequestComponent)
HttpRequestHost = http_request_ns.class_("HttpRequestHost", HttpRequestComponent)
HttpContainer = http_request_ns.class_("HttpContainer")
RequestHeader = http_request_ns.struct("RequestHeader")
HttpRequestSendAction = http_request_ns.class_(
"HttpRequestSendAction", automation.Action
@@ -360,12 +361,27 @@ async def http_request_action_to_code(
for key in json_:
template_ = await cg.templatable(json_[key], args, cg.std_string)
cg.add(var.add_json(key, template_))
request_headers = config.get(CONF_REQUEST_HEADERS, {})
if request_headers:
cg.add(var.init_request_headers(len(request_headers)))
for key, value in request_headers.items():
template_ = await cg.templatable(value, args, cg.const_char_ptr)
cg.add(var.add_request_header(key, template_))
if request_headers := config.get(CONF_REQUEST_HEADERS):
# A user lambda may keep static state, so only all-constant lists are shared
share = not any(map(cg.is_template, request_headers.values()))
# Constant headers ignore their arguments, so drop the names: identical lists
# from scripts with different parameter names then render the same table.
header_args = [(arg_type, "") for arg_type, _ in args] if share else args
headers = [
cg.StructInitializer(
"",
("name", key),
("value", await cg.templatable(value, header_args, cg.const_char_ptr)),
)
for key, value in request_headers.items()
]
table = cg.shared_progmem_array(
"http_request_headers",
RequestHeader.template(*[arg_type for arg_type, _ in args]),
headers,
share=share,
)
cg.add(var.set_request_headers(table, len(headers)))
for value in config.get(CONF_COLLECT_HEADERS, []):
cg.add(var.add_collect_header(value.lower()))
+15 -6
View File
@@ -22,6 +22,12 @@ struct Header {
std::string value;
};
/// One request header of an action; codegen puts these in a shared flash table.
template<typename... Ts> struct RequestHeader {
const char *name;
const char *(*value)(Ts...);
};
// Some common HTTP status codes
enum HttpStatus {
HTTP_STATUS_OK = 200,
@@ -492,9 +498,12 @@ template<typename... Ts> class HttpRequestSendAction final : public Action<Ts...
TEMPLATABLE_VALUE(bool, capture_response)
#endif
void init_request_headers(size_t count) { this->request_headers_.init(count); }
void add_request_header(const char *key, TemplatableFn<const char *, Ts...> value) {
this->request_headers_.push_back({key, value});
static_assert(std::is_trivially_copyable_v<RequestHeader<Ts...>> &&
sizeof(RequestHeader<Ts...>) % sizeof(uint32_t) == 0,
"ESP8266 reads request headers from flash in whole words");
/// Codegen only: the table must outlive the action.
void set_request_headers(const RequestHeader<Ts...> *headers, size_t count) {
this->request_headers_ = ConstVector<RequestHeader<Ts...>>(headers, count);
}
void add_collect_header(const char *value) { this->lower_case_collect_headers_.emplace_back(value); }
@@ -530,8 +539,8 @@ template<typename... Ts> class HttpRequestSendAction final : public Action<Ts...
}
std::vector<Header> request_headers;
request_headers.reserve(this->request_headers_.size());
for (const auto &[key, val] : this->request_headers_) {
request_headers.push_back({key, val.value(x...)});
for (const auto &header : this->request_headers_) {
request_headers.push_back({header.name, header.value(x...)});
}
auto container = this->parent_->start(this->url_.value(x...).c_str(), this->method_.value(x...), body,
@@ -596,7 +605,7 @@ template<typename... Ts> class HttpRequestSendAction final : public Action<Ts...
}
}
HttpRequestComponent *parent_;
FixedVector<std::pair<const char *, TemplatableFn<const char *, Ts...>>> request_headers_{};
ConstVector<RequestHeader<Ts...>> request_headers_{};
std::vector<std::string> lower_case_collect_headers_{"content-type", "content-length"};
FixedVector<std::pair<const char *, TemplatableValue<std::string, Ts...>>> json_{};
std::function<void(Ts..., JsonObject)> json_func_{nullptr};
@@ -0,0 +1,72 @@
esphome:
name: test
esp32:
board: esp32dev
wifi:
ssid: test
password: testtest
http_request:
button:
- platform: template
id: shared_a
on_press:
- http_request.get:
url: http://example.com
request_headers:
Accept: application/json
X-Mode: a
- platform: template
id: shared_b
on_press:
- http_request.get:
url: http://example.com
request_headers:
Accept: application/json
X-Mode: a
- platform: template
id: other
on_press:
- http_request.get:
url: http://example.com
request_headers:
Accept: text/plain
- platform: template
id: lambda_a
on_press:
- http_request.get:
url: http://example.com
request_headers:
Accept: application/json
X-Mode: !lambda return "a";
- platform: template
id: lambda_b
on_press:
- http_request.get:
url: http://example.com
request_headers:
Accept: application/json
X-Mode: !lambda return "a";
script:
- id: param_a
parameters:
level: int
then:
- http_request.get:
url: http://example.com
request_headers:
Accept: application/json
X-Mode: a
- id: param_b
parameters:
value: int
then:
- http_request.get:
url: http://example.com
request_headers:
Accept: application/json
X-Mode: a
@@ -0,0 +1,37 @@
"""Request headers are PROGMEM tables, shared only between all-constant lists."""
from collections.abc import Callable
from pathlib import Path
import re
def test_request_headers_share_constant_tables(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
main_cpp = generate_main(component_config_path("request_headers.yaml"))
# Actions in config order: the five buttons, then the two scripts
tables = [
(table, int(count))
for table, count in re.findall(
r"->set_request_headers\((\w+), (\d+)\);", main_cpp
)
]
assert len(tables) == 7
shared_a, shared_b, other, lambda_a, lambda_b, param_a, param_b = tables
assert shared_a == shared_b
assert shared_a[1] == 2
assert other[1] == 1
# A list with a lambda may keep static state, so it never shares even with an identical one
assert lambda_a[0] != lambda_b[0]
assert len({shared_a[0], other[0], lambda_a[0], lambda_b[0]}) == 4
for table, _ in tables:
assert re.search(rf"\b{table}\[\] PROGMEM = \{{", main_cpp)
# Constant headers ignore their arguments, so scripts whose parameters differ
# only by name share a table, and it is the only one of its argument type
assert param_a == param_b
assert param_a[0] not in {shared_a[0], other[0], lambda_a[0], lambda_b[0]}
assert re.findall(r"RequestHeader<[^>]+> (\w+)\[\] PROGMEM = \{", main_cpp) == [
param_a[0]
]