From b13fcd8939b543ba6937aa29d84907e771a4557e Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 18 Apr 2026 17:32:54 +0200 Subject: [PATCH] single static-content handler serving all assets --- src/ESP32React/ESP32React.cpp | 114 ++++++++++++++++++++-------------- src/ESP32React/ESP32React.h | 1 + 2 files changed, 69 insertions(+), 46 deletions(-) diff --git a/src/ESP32React/ESP32React.cpp b/src/ESP32React/ESP32React.cpp index 41536153f..73575178c 100644 --- a/src/ESP32React/ESP32React.cpp +++ b/src/ESP32React/ESP32React.cpp @@ -2,10 +2,75 @@ #include "WWWData.h" // include auto-generated static web resources +#include + static constexpr const char CACHE_CONTROL[] = "public,max-age=60"; +// Single static-content handler serving all assets embedded in WWWData.h. +class StaticContentHandler : public AsyncWebHandler { + public: + bool canHandle(AsyncWebServerRequest * request) const override { + const auto method = request->method(); + return method == HTTP_GET || method == HTTP_HEAD || method == HTTP_OPTIONS; + } + + void handleRequest(AsyncWebServerRequest * request) override { + // OPTIONS is handled generically - the server-level CORS headers are + // attached via DefaultHeaders in ESP32React::begin(). + if (request->method() == HTTP_OPTIONS) { + request->send(200); + return; + } + + const char * url = request->url().c_str(); + const WWWAsset * found = lookup(url); + const WWWAsset * asset = found ? found : index_asset(); + + if (asset == nullptr) { + request->send(404); + return; + } + + // If the client already has this exact ETag, respond 304 Not Modified without sending the body. + const String & inm = request->header(asyncsrv::T_INM); + if (inm.length() != 0 && strcmp(inm.c_str(), asset->etag) == 0) { + request->send(304); + return; + } + + AsyncWebServerResponse * response = request->beginResponse(200, asset->contentType, asset->content, asset->len); + response->addHeader(asyncsrv::T_Content_Encoding, asyncsrv::T_gzip, false); + response->addHeader(asyncsrv::T_ETag, asset->etag, false); + response->addHeader(asyncsrv::T_Cache_Control, CACHE_CONTROL, false); + request->send(response); + } + + private: + // Exact-match lookup in the asset table + static const WWWAsset * lookup(const char * url) { + for (size_t i = 0; i < WWW_ASSETS_COUNT; i++) { + if (strcmp(WWW_ASSETS[i].uri, url) == 0) { + return &WWW_ASSETS[i]; + } + } + return nullptr; + } + + // Returns the /index.html asset, used as the SPA fallback for any GET + // that didn't match an embedded asset (React Router handles routing on + // the client side). + static const WWWAsset * index_asset() { + static const WWWAsset * cached = nullptr; + if (cached == nullptr) { + cached = lookup("/index.html"); + } + return cached; + } +}; + ESP32React::ESP32React(AsyncWebServer * server, FS * fs) - : _securitySettingsService(server, fs) + : _server(server) + , _securitySettingsService(server, fs) , _networkSettingsService(server, fs, &_securitySettingsService) , _wifiScanner(server, &_securitySettingsService) , _networkStatus(server, &_securitySettingsService) @@ -17,50 +82,6 @@ ESP32React::ESP32React(AsyncWebServer * server, FS * fs) , _mqttSettingsService(server, fs, &_securitySettingsService) , _mqttStatus(server, &_mqttSettingsService, &_securitySettingsService) , _authenticationService(server, &_securitySettingsService) { - // - // Serve static web resources - // - - ArRequestHandlerFunction indexHtmlHandler = nullptr; - - WWWData::registerRoutes([server, &indexHtmlHandler](const char * uri, const String & contentType, const uint8_t * content, size_t len, const String & hash) { - String etag = "\"" + hash + "\""; // RFC9110: ETag must be enclosed in double quotes - - ArRequestHandlerFunction requestHandler = [contentType, content, len, etag](AsyncWebServerRequest * request) { - if (request->header(asyncsrv::T_INM) == etag) { - request->send(304); - return; - } - - AsyncWebServerResponse * response = request->beginResponse(200, contentType, content, len); - response->addHeader(asyncsrv::T_Content_Encoding, asyncsrv::T_gzip, false); - response->addHeader(asyncsrv::T_ETag, etag, false); - response->addHeader(asyncsrv::T_Cache_Control, CACHE_CONTROL, false); - request->send(response); - }; - - server->on(uri, HTTP_GET, requestHandler); - - // Capture index.html handler to set onNotFound once after all routes are registered - if (strcmp(uri, "/index.html") == 0) { - indexHtmlHandler = requestHandler; - } - }); - - // Set onNotFound handler once after all routes are registered - // Serving non matching get requests with "/index.html" - // OPTIONS get a straight up 200 response - if (indexHtmlHandler != nullptr) { - server->onNotFound([indexHtmlHandler](AsyncWebServerRequest * request) { - if (request->method() == HTTP_GET) { - indexHtmlHandler(request); - } else if (request->method() == HTTP_OPTIONS) { - request->send(200); - } else { - request->send(404); // not found - } - }); - } } void ESP32React::begin() { @@ -78,10 +99,11 @@ void ESP32React::begin() { _ntpSettingsService.begin(); _mqttSettingsService.begin(); _securitySettingsService.begin(); + _server->addHandler(new StaticContentHandler()); } void ESP32React::loop() { _networkSettingsService.loop(); _apSettingsService.loop(); _mqttSettingsService.loop(); -} \ No newline at end of file +} diff --git a/src/ESP32React/ESP32React.h b/src/ESP32React/ESP32React.h index f5af50a8a..cbcea487e 100644 --- a/src/ESP32React/ESP32React.h +++ b/src/ESP32React/ESP32React.h @@ -68,6 +68,7 @@ class ESP32React { } private: + AsyncWebServer * _server; SecuritySettingsService _securitySettingsService; NetworkSettingsService _networkSettingsService; WiFiScanner _wifiScanner;