Compare commits

...
Author SHA1 Message Date
J. Nick Koston e14a918218 [web_server_base] Drop the heap list of web handlers
Only web OTA's handler ever waits for the server, and only when no web_server or prometheus creates it first, so it waits in a single pointer compiled in for those configs. A handler with nowhere to wait creates the server early, keeping registration order for external components.
2026-10-26 17:59:37 -10:00
9 changed files with 120 additions and 24 deletions

No files matched your search

@@ -92,5 +92,9 @@ async def to_code(config: ConfigType) -> None:
await ota_to_code(var, config)
await cg.register_component(var, config)
cg.add_define("USE_WEBSERVER_OTA")
# web_server and prometheus create the server in an earlier setup(); without them
# the handler waits until captive_portal starts it
if not {"web_server", "prometheus"} & CORE.config.keys():
cg.add_define("USE_WEBSERVER_BASE_PENDING_HANDLER")
if CORE.is_esp32:
add_idf_component(name="zorxx/multipart-parser", ref="1.0.1")
@@ -72,7 +72,7 @@ async def to_code(config: ConfigType) -> None:
cg.add(cg.RawExpression(f"{web_server_base_ns}::global_web_server_base = {var}"))
if CORE.is_esp32:
# Count for StaticVector in web_server_idf - matches headers added in init()
# Count for StaticVector in web_server_idf - matches headers added in create_server_()
cg.add_define("WEB_SERVER_DEFAULT_HEADERS_COUNT", 1)
return
@@ -15,10 +15,29 @@ void WebServerBase::add_handler(AsyncWebHandler *handler) {
}
void WebServerBase::add_handler_without_auth(AsyncWebHandler *handler) {
this->handlers_.push_back(handler);
if (this->server_ != nullptr) {
this->server_->addHandler(handler);
if (this->server_ == nullptr) {
#ifdef USE_WEBSERVER_BASE_PENDING_HANDLER
if (this->pending_handler_ == nullptr) {
this->pending_handler_ = handler;
return;
}
#endif
// Nowhere for it to wait: create the server now, after the waiting handler to keep the order
this->create_server_();
}
this->server_->addHandler(handler);
}
void WebServerBase::create_server_() {
this->server_ = new AsyncWebServer(this->port_);
// All content is controlled and created by user - so allowing all origins is fine here.
// NOTE: Currently 1 header. If more are added, update in __init__.py:
// cg.add_define("WEB_SERVER_DEFAULT_HEADERS_COUNT", 1)
DefaultHeaders::Instance().addHeader(ESPHOME_F("Access-Control-Allow-Origin"), ESPHOME_F("*"));
#ifdef USE_WEBSERVER_BASE_PENDING_HANDLER
if (this->pending_handler_ != nullptr)
this->server_->addHandler(this->pending_handler_);
#endif
}
} // namespace esphome::web_server_base
@@ -1,8 +1,6 @@
#pragma once
#include "esphome/core/defines.h"
#if defined(USE_NETWORK) && !defined(USE_ZEPHYR) && !defined(USE_HOST)
#include <vector>
#include "esphome/core/progmem.h"
#if USE_ESP32
@@ -115,26 +113,14 @@ class WebServerBase final {
// The AsyncWebServer is created once and intentionally never deleted: on Arduino
// platforms ESPAsyncWebServer owns its registered handlers, so destroying it would
// also destroy live components (e.g. the captive portal) out from under us.
// init()/deinit() refcount users and start/stop the listener; handlers are
// registered once at creation and survive listener restarts.
// init()/deinit() refcount users and start/stop the listener; handlers stay
// registered across listener restarts.
void init() {
this->initialized_++;
if (this->server_ != nullptr) {
if (this->initialized_ == 1) {
// Restart the listener after a previous deinit()
this->server_->begin();
}
if (++this->initialized_ != 1)
return;
}
this->server_ = new AsyncWebServer(this->port_);
// All content is controlled and created by user - so allowing all origins is fine here.
// NOTE: Currently 1 header. If more are added, update in __init__.py:
// cg.add_define("WEB_SERVER_DEFAULT_HEADERS_COUNT", 1)
DefaultHeaders::Instance().addHeader(ESPHOME_F("Access-Control-Allow-Origin"), ESPHOME_F("*"));
if (this->server_ == nullptr)
this->create_server_();
this->server_->begin();
for (auto *handler : this->handlers_)
this->server_->addHandler(handler);
}
void deinit() {
if (this->initialized_ == 0)
@@ -169,10 +155,15 @@ class WebServerBase final {
uint16_t get_port() const { return port_; }
protected:
void create_server_();
uint8_t initialized_{0};
uint16_t port_{80}; // Keep in sync with DEFAULT_PORT in web_server/__init__.py
AsyncWebServer *server_{nullptr};
std::vector<AsyncWebHandler *> handlers_;
#ifdef USE_WEBSERVER_BASE_PENDING_HANDLER
// Web OTA's handler when it is added before any component created the server
AsyncWebHandler *pending_handler_{nullptr};
#endif
#ifdef USE_WEBSERVER_AUTH
internal::Credentials credentials_;
#endif
+2
View File
@@ -561,6 +561,7 @@
#define USE_WEBSERVER_PRIVATE_NETWORK_ACCESS
#define USE_WEBSERVER_VERSION 3
#define WEB_SERVER_DEFAULT_HEADERS_COUNT 1
#define USE_WEBSERVER_BASE_PENDING_HANDLER
#define USE_CAPTIVE_PORTAL_GZIP
#endif
#define USE_WIFI_11KV_SUPPORT
@@ -740,6 +741,7 @@
#define USE_WEBSERVER_VERSION 2
#define USE_WEBSERVER_LOCAL
#define USE_WEBSERVER_OTA
#define USE_WEBSERVER_BASE_PENDING_HANDLER
#define USE_WEBSERVER_OTA_DISABLED
#endif
@@ -0,0 +1,14 @@
---
esphome:
name: test
esp8266:
board: d1_mini
wifi:
ssid: test
password: testtest
ap:
password: testtest
captive_portal:
@@ -0,0 +1,16 @@
---
esphome:
name: test
esp8266:
board: d1_mini
wifi:
ssid: test
password: testtest
ap:
password: testtest
captive_portal:
prometheus:
@@ -0,0 +1,16 @@
---
esphome:
name: test
esp8266:
board: d1_mini
wifi:
ssid: test
password: testtest
ap:
password: testtest
captive_portal:
web_server:
@@ -0,0 +1,34 @@
"""Tests for when web OTA's handler waits in WebServerBase for the server to be created."""
from collections.abc import Callable
from pathlib import Path
import pytest
from esphome.core import CORE
_PENDING = "USE_WEBSERVER_BASE_PENDING_HANDLER"
@pytest.mark.parametrize(
("config_file", "pending"),
[
# captive_portal loads web OTA, and with it the web_server package, but only
# creates the server when the AP starts
("pending_handler_captive_portal.yaml", True),
("pending_handler_web_server.yaml", False),
("pending_handler_prometheus.yaml", False),
],
)
def test_pending_handler_define(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
config_file: str,
pending: bool,
) -> None:
"""Only configs where nothing creates the server before web OTA's setup() keep the pointer."""
generate_main(component_config_path(config_file))
defines = {define.name for define in CORE.defines}
assert "USE_WEBSERVER_OTA" in defines
assert (_PENDING in defines) is pending