From 260f26dadbaf458b8c50e0a0a861c99fb8b06d71 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Nov 2025 08:08:43 -0500 Subject: [PATCH] Fix stale UI after firmware updates (#5120) Add WEB_BUILD_TIME to html_ui.h and use it for ETag generation Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com> Co-authored-by: Aircoookie <21045690+Aircoookie@users.noreply.github.com> --- tools/cdata.js | 13 ++++++- wled00/wled_server.cpp | 79 ++++++++++++++++++++++++------------------ 2 files changed, 58 insertions(+), 34 deletions(-) diff --git a/tools/cdata.js b/tools/cdata.js index d9664551..3cc6f8ec 100644 --- a/tools/cdata.js +++ b/tools/cdata.js @@ -113,6 +113,11 @@ function filter(str, type) { } } +// Generate build timestamp as UNIX timestamp (seconds since epoch) +function generateBuildTime() { + return Math.floor(Date.now() / 1000); +} + function writeHtmlGzipped(sourceFile, resultFile, page) { console.info("Reading " + sourceFile); new inliner(sourceFile, function (error, html) { @@ -141,7 +146,13 @@ function writeHtmlGzipped(sourceFile, resultFile, page) { * Please see https://mm.kno.wled.ge/advanced/custom-features/#changing-web-ui * to find out how to easily modify the web UI source! */ - + +// Automatically generated build time for cache busting (UNIX timestamp) +#ifdef WEB_BUILD_TIME // avoid duplicate defintions +#undef WEB_BUILD_TIME +#endif +#define WEB_BUILD_TIME ${generateBuildTime()} + // Autogenerated from ${sourceFile}, do not edit!! const uint16_t PAGE_${page}_L = ${result.length}; const uint8_t PAGE_${page}[] PROGMEM = { diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 0a0dd50f..361b5b58 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -11,18 +11,56 @@ #endif #include "html_cpal.h" -/* - * Integrated HTTP web server page declarations - */ - -bool handleIfNoneMatchCacheHeader(AsyncWebServerRequest* request); -void setStaticContentCacheHeaders(AsyncWebServerResponse *response); - // define flash strings once (saves flash memory) static const char s_redirecting[] PROGMEM = "Redirecting..."; static const char s_content_enc[] PROGMEM = "Content-Encoding"; static const char s_unlock_ota [] PROGMEM = "Please unlock OTA in security settings!"; static const char s_unlock_cfg [] PROGMEM = "Please unlock settings using PIN code!"; +static const char s_cache_control[] PROGMEM = "Cache-Control"; +static const char s_no_store[] PROGMEM = "no-store"; +static const char s_expires[] PROGMEM = "Expires"; + +/* + * Integrated HTTP web server page declarations + */ + +static void generateEtag(char *etag, uint16_t eTagSuffix) { + sprintf_P(etag, PSTR("%u-%02x-%04x"), WEB_BUILD_TIME, cacheInvalidate, eTagSuffix); +} + +static void setStaticContentCacheHeaders(AsyncWebServerResponse *response, int code=200, uint16_t eTagSuffix = 0) { + // Only send ETag for 200 (OK) responses + if (code != 200) return; + + // https://medium.com/@codebyamir/a-web-developers-guide-to-browser-caching-cc41f3b73e7c + #ifndef WLED_DEBUG + // this header name is misleading, "no-cache" will not disable cache, + // it just revalidates on every load using the "If-None-Match" header with the last ETag value + response->addHeader(FPSTR(s_cache_control), F("no-cache")); + #else + response->addHeader(FPSTR(s_cache_control), F("no-store,max-age=0")); // prevent caching if debug build + #endif + char etag[32] = {'\0'}; + generateEtag(etag, eTagSuffix); + response->addHeader(F("ETag"), etag); +} + +static bool handleIfNoneMatchCacheHeader(AsyncWebServerRequest *request, int code=200, uint16_t eTagSuffix = 0) { + // Only send 304 (Not Modified) if response code is 200 (OK) + if (code != 200) return false; + + AsyncWebHeader *header = request->getHeader(F("If-None-Match")); + char etag[32] = {'\0'}; + generateEtag(etag, eTagSuffix); + if (header && header->value() == etag) { + AsyncWebServerResponse *response = request->beginResponse(304); + setStaticContentCacheHeaders(response, code, eTagSuffix); + request->send(response); + return true; + } + return false; +} + //Is this an IP? bool isIp(String str) { @@ -451,7 +489,7 @@ void initServer() AsyncWebServerResponse *response = request->beginResponse_P(404, "text/html", PAGE_404, PAGE_404_length); #endif response->addHeader(FPSTR(s_content_enc),"gzip"); - setStaticContentCacheHeaders(response); + setStaticContentCacheHeaders(response, 404); request->send(response); //request->send_P(404, "text/html", PAGE_404); }); @@ -467,31 +505,6 @@ void serveIndexOrWelcome(AsyncWebServerRequest *request) } } -bool handleIfNoneMatchCacheHeader(AsyncWebServerRequest* request) -{ - AsyncWebHeader* header = request->getHeader("If-None-Match"); - if (header && header->value() == String(VERSION)) { - request->send(304); - return true; - } - return false; -} - -void setStaticContentCacheHeaders(AsyncWebServerResponse *response) -{ - char tmp[12]; - // https://medium.com/@codebyamir/a-web-developers-guide-to-browser-caching-cc41f3b73e7c - #ifndef WLED_DEBUG - //this header name is misleading, "no-cache" will not disable cache, - //it just revalidates on every load using the "If-None-Match" header with the last ETag value - response->addHeader(F("Cache-Control"),"no-cache"); - #else - response->addHeader(F("Cache-Control"),"no-store,max-age=0"); // prevent caching if debug build - #endif - sprintf_P(tmp, PSTR("%8d-%02x"), VERSION, cacheInvalidate); - response->addHeader(F("ETag"), tmp); -} - void serveIndex(AsyncWebServerRequest* request) { if (handleFileRead(request, "/index.htm")) return;