Skip to content

Commit c34ba69

Browse files
authored
Add ETag handling and cache for pre-compressed .gz files
Add ETag handling and cache for pre-compressed .gz files in AsyncFileResponse This patch improves AsyncFileResponse by adding ETag and cache support for pre-compressed .gz files. Pre-compressed files (e.g., index.html.gz) were already being served when available. This update introduces automatic generation of an ETag for such files, based on the CRC32 checksum located in the gzip trailer (last 4 bytes). When a .gz file from FS is served: - An `ETag` header is included, derived from the gzip trailer CRC32 - A `Cache-Control: no-cache` header is added to ensure proper validation This enhancement enables better caching and validation mechanisms for clients, improving efficiency while ensuring updated content is properly detected.
1 parent 7ebeb45 commit c34ba69

File tree

2 files changed

+72
-1
lines changed

2 files changed

+72
-1
lines changed

src/ESPAsyncWebServer.h

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,12 +368,52 @@ class AsyncWebServerRequest {
368368
}
369369

370370
void send(FS &fs, const String &path, const char *contentType = asyncsrv::empty, bool download = false, AwsTemplateProcessor callback = nullptr) {
371+
// If-None-Match header
372+
if (this->hasHeader(asyncsrv::T_INM) && !download && fs.exists(path + asyncsrv::T__gz)) {
373+
// CRC32-based ETag of the trailer, bytes 4-7 from the end
374+
File file = fs.open(path + asyncsrv::T__gz, fs::FileOpenMode::read);
375+
if (file && file.size() >= 8) {
376+
file.seek(file.size() - 8);
377+
378+
uint8_t trailer[4];
379+
if (file.read(trailer, sizeof(trailer)) == sizeof(trailer)) {
380+
char serverETag[11];
381+
_getEtag(trailer, serverETag);
382+
383+
// Compare with client's If-None-Match header
384+
const char *clientETag = this->getHeader(asyncsrv::T_INM)->value().c_str();
385+
if (strcmp(clientETag, serverETag) == 0) {
386+
file.close();
387+
this->send(304); // Not Modified
388+
return;
389+
}
390+
}
391+
file.close();
392+
}
393+
}
394+
395+
// If we get here, create and send the normal response
371396
if (fs.exists(path) || (!download && fs.exists(path + asyncsrv::T__gz))) {
372397
send(beginResponse(fs, path, contentType, download, callback));
373398
} else {
374399
send(404);
375400
}
376401
}
402+
403+
void _getEtag(uint8_t trailer[4], char* serverETag) {
404+
serverETag[0] = '"';
405+
for (int i = 0; i < 4; ++i) {
406+
uint8_t byte = trailer[i];
407+
uint8_t highNibble = (byte >> 4) & 0x0F;
408+
uint8_t lowNibble = byte & 0x0F;
409+
410+
serverETag[1 + i * 2] = (highNibble < 10) ? ('0' + highNibble) : ('A' + highNibble - 10);
411+
serverETag[2 + i * 2] = (lowNibble < 10) ? ('0' + lowNibble) : ('A' + lowNibble - 10);
412+
}
413+
serverETag[9] = '"';
414+
serverETag[10] = '\0';
415+
}
416+
377417
void send(FS &fs, const String &path, const String &contentType, bool download = false, AwsTemplateProcessor callback = nullptr) {
378418
send(fs, path, contentType.c_str(), download, callback);
379419
}

src/WebResponses.cpp

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -657,8 +657,25 @@ AsyncFileResponse::AsyncFileResponse(FS &fs, const String &path, const char *con
657657
_callback = nullptr; // Unable to process zipped templates
658658
_sendContentLength = true;
659659
_chunked = false;
660+
_content = fs.open(_path, fs::FileOpenMode::read);
661+
_contentLength = _content.size();
662+
663+
// CRC32-based ETag of the trailer, bytes 4-7 from the end
664+
if (_content && _contentLength >= 8) {
665+
_content.seek(_contentLength - 8);
666+
uint8_t trailer[4];
667+
if (_content.read(trailer, sizeof(trailer)) == sizeof(trailer)) {
668+
char serverETag[11];
669+
_getEtag(trailer, serverETag);
670+
addHeader(T_ETag, serverETag, false);
671+
addHeader(T_Cache_Control, T_no_cache, false);
672+
}
673+
674+
// Return to the beginning of the file
675+
_content.seek(0);
676+
}
660677
}
661-
678+
662679
_content = fs.open(_path, fs::FileOpenMode::read);
663680
_contentLength = _content.size();
664681

@@ -682,6 +699,20 @@ AsyncFileResponse::AsyncFileResponse(FS &fs, const String &path, const char *con
682699
addHeader(T_Content_Disposition, buf, false);
683700
}
684701

702+
void AsyncFileResponse::_getEtag(uint8_t trailer[4], char* serverETag) {
703+
serverETag[0] = '"';
704+
for (int i = 0; i < 4; ++i) {
705+
uint8_t byte = trailer[i];
706+
uint8_t highNibble = (byte >> 4) & 0x0F;
707+
uint8_t lowNibble = byte & 0x0F;
708+
709+
serverETag[1 + i * 2] = (highNibble < 10) ? ('0' + highNibble) : ('A' + highNibble - 10);
710+
serverETag[2 + i * 2] = (lowNibble < 10) ? ('0' + lowNibble) : ('A' + lowNibble - 10);
711+
}
712+
serverETag[9] = '"';
713+
serverETag[10] = '\0';
714+
}
715+
685716
AsyncFileResponse::AsyncFileResponse(File content, const String &path, const char *contentType, bool download, AwsTemplateProcessor callback)
686717
: AsyncAbstractResponse(callback) {
687718
_code = 200;

0 commit comments

Comments
 (0)