Skip to content

Commit

Permalink
Use http header ETag caching for all static content.
Browse files Browse the repository at this point in the history
Using the md5sum as ETag http header value should enable caching on all static http content.
  • Loading branch information
StefanOberhumer committed Jan 29, 2024
1 parent 21ec72f commit e752c43
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 55 deletions.
1 change: 1 addition & 0 deletions include/WebApi_webapp.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ class WebApiWebappClass {

private:
AsyncWebServer* _server;
void responseBinaryDataWithETagCache(AsyncWebServerRequest* request, const String &contentType, const String &contentEncoding, const uint8_t *content, size_t len);
};
112 changes: 57 additions & 55 deletions src/WebApi_webapp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* Copyright (C) 2022-2024 Thomas Basler and others
*/
#include "WebApi_webapp.h"
#include <MD5Builder.h>

extern const uint8_t file_index_html_start[] asm("_binary_webapp_dist_index_html_gz_start");
extern const uint8_t file_favicon_ico_start[] asm("_binary_webapp_dist_favicon_ico_start");
Expand All @@ -18,79 +19,80 @@ extern const uint8_t file_zones_json_end[] asm("_binary_webapp_dist_zones_json_g
extern const uint8_t file_app_js_end[] asm("_binary_webapp_dist_js_app_js_gz_end");
extern const uint8_t file_site_webmanifest_end[] asm("_binary_webapp_dist_site_webmanifest_end");

#ifdef AUTO_GIT_HASH
#define ETAG_HTTP_HEADER_VAL "\"" AUTO_GIT_HASH "\"" // ETag value must be between quotes
#endif
void WebApiWebappClass::responseBinaryDataWithETagCache(AsyncWebServerRequest *request, const String &contentType, const String &contentEncoding, const uint8_t *content, size_t len)
{
auto _md5 = MD5Builder();
_md5.begin();
_md5.add(const_cast<uint8_t *>(content), len);
_md5.calculate();

String expectedEtag;
expectedEtag = "\"";
expectedEtag += _md5.toString();
expectedEtag += "\"";

bool eTagMatch = false;
if (request->hasHeader("If-None-Match")) {
const AsyncWebHeader* h = request->getHeader("If-None-Match");
eTagMatch = h->value().equals(expectedEtag);
}

// begin response 200 or 304
AsyncWebServerResponse* response;
if (eTagMatch) {
response = request->beginResponse(304);
} else {
response = request->beginResponse_P(200, contentType, content, len);
if (contentEncoding.length() > 0) {
response->addHeader("Content-Encoding", contentEncoding);
}
}

// HTTP requires cache headers in 200 and 304 to be identical
response->addHeader("Cache-Control", "public, must-revalidate");
response->addHeader("ETag", expectedEtag);

request->send(response);
}

void WebApiWebappClass::init(AsyncWebServer& server, Scheduler& scheduler)
{
_server = &server;

_server->on("/", HTTP_GET, [](AsyncWebServerRequest* request) {
AsyncWebServerResponse* response = request->beginResponse_P(200, "text/html", file_index_html_start, file_index_html_end - file_index_html_start);
response->addHeader("Content-Encoding", "gzip");
request->send(response);
});
/*
We don't validate the request header "Accept-Encoding" if gzip compression is supported!
We just have the gzipped data available - so we ship them!
*/

_server->onNotFound([](AsyncWebServerRequest* request) {
AsyncWebServerResponse* response = request->beginResponse_P(200, "text/html", file_index_html_start, file_index_html_end - file_index_html_start);
response->addHeader("Content-Encoding", "gzip");
request->send(response);
_server->on("/", HTTP_GET, [&](AsyncWebServerRequest* request) {
responseBinaryDataWithETagCache(request, "text/html", "gzip", file_index_html_start, file_index_html_end - file_index_html_start);
});

_server->on("/index.html", HTTP_GET, [](AsyncWebServerRequest* request) {
AsyncWebServerResponse* response = request->beginResponse_P(200, "text/html", file_index_html_start, file_index_html_end - file_index_html_start);
response->addHeader("Content-Encoding", "gzip");
request->send(response);
_server->onNotFound([&](AsyncWebServerRequest* request) {
responseBinaryDataWithETagCache(request, "text/html", "gzip", file_index_html_start, file_index_html_end - file_index_html_start);
});

_server->on("/favicon.ico", HTTP_GET, [](AsyncWebServerRequest* request) {
AsyncWebServerResponse* response = request->beginResponse_P(200, "image/x-icon", file_favicon_ico_start, file_favicon_ico_end - file_favicon_ico_start);
request->send(response);
_server->on("/index.html", HTTP_GET, [&](AsyncWebServerRequest* request) {
responseBinaryDataWithETagCache(request, "text/html", "gzip", file_index_html_start, file_index_html_end - file_index_html_start);
});

_server->on("/favicon.png", HTTP_GET, [](AsyncWebServerRequest* request) {
AsyncWebServerResponse* response = request->beginResponse_P(200, "image/png", file_favicon_png_start, file_favicon_png_end - file_favicon_png_start);
request->send(response);
_server->on("/favicon.ico", HTTP_GET, [&](AsyncWebServerRequest* request) {
responseBinaryDataWithETagCache(request, "image/x-icon", "", file_favicon_ico_start, file_favicon_ico_end - file_favicon_ico_start);
});

_server->on("/zones.json", HTTP_GET, [](AsyncWebServerRequest* request) {
AsyncWebServerResponse* response = request->beginResponse_P(200, "application/json", file_zones_json_start, file_zones_json_end - file_zones_json_start);
response->addHeader("Content-Encoding", "gzip");
request->send(response);
_server->on("/favicon.png", HTTP_GET, [&](AsyncWebServerRequest* request) {
responseBinaryDataWithETagCache(request, "image/png", "", file_favicon_png_start, file_favicon_png_end - file_favicon_png_start);
});

_server->on("/site.webmanifest", HTTP_GET, [](AsyncWebServerRequest* request) {
AsyncWebServerResponse* response = request->beginResponse_P(200, "application/json", file_site_webmanifest_start, file_site_webmanifest_end - file_site_webmanifest_start);
request->send(response);
_server->on("/zones.json", HTTP_GET, [&](AsyncWebServerRequest* request) {
responseBinaryDataWithETagCache(request, "application/json", "gzip", file_zones_json_start, file_zones_json_end - file_zones_json_start);
});

_server->on("/js/app.js", HTTP_GET, [](AsyncWebServerRequest* request) {
#ifdef ETAG_HTTP_HEADER_VAL
// check client If-None-Match header vs ETag/AUTO_GIT_HASH
bool eTagMatch = false;
if (request->hasHeader("If-None-Match")) {
const AsyncWebHeader* h = request->getHeader("If-None-Match");
if (strncmp(ETAG_HTTP_HEADER_VAL, h->value().c_str(), strlen(ETAG_HTTP_HEADER_VAL)) == 0) {
eTagMatch = true;
}
}
_server->on("/site.webmanifest", HTTP_GET, [&](AsyncWebServerRequest* request) {
responseBinaryDataWithETagCache(request, "application/json", "", file_site_webmanifest_start, file_site_webmanifest_end - file_site_webmanifest_start);
});

// begin response 200 or 304
AsyncWebServerResponse* response;
if (eTagMatch) {
response = request->beginResponse(304);
} else {
response = request->beginResponse_P(200, "text/javascript", file_app_js_start, file_app_js_end - file_app_js_start);
response->addHeader("Content-Encoding", "gzip");
}
// HTTP requires cache headers in 200 and 304 to be identical
response->addHeader("Cache-Control", "public, must-revalidate");
response->addHeader("ETag", ETAG_HTTP_HEADER_VAL);
#else
AsyncWebServerResponse* response = request->beginResponse_P(200, "text/javascript", file_app_js_start, file_app_js_end - file_app_js_start);
response->addHeader("Content-Encoding", "gzip");
#endif
request->send(response);
_server->on("/js/app.js", HTTP_GET, [&](AsyncWebServerRequest* request) {
responseBinaryDataWithETagCache(request, "text/javascript", "gzip", file_app_js_start, file_app_js_end - file_app_js_start);
});
}

0 comments on commit e752c43

Please sign in to comment.