Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6665b47
Fix ETag handling for dynamic template responses
JosePineiro Aug 22, 2025
a16b756
Update src/WebHandlers.cpp
JosePineiro Aug 22, 2025
dcf0491
Update src/WebHandlers.cpp
JosePineiro Aug 22, 2025
d1b0868
update mathieucarbou suggestions
JosePineiro Aug 22, 2025
d41b258
Merge branch 'ESP32Async:main' into fix/dynamic-template-etag
JosePineiro Aug 23, 2025
bc2a00c
Add files via upload
JosePineiro Aug 23, 2025
d7e91bc
ci(pre-commit): Apply automatic fixes
pre-commit-ci-lite[bot] Aug 25, 2025
44e9fde
Merge branch 'ESP32Async:main' into fix/dynamic-template-etag
JosePineiro Aug 25, 2025
22cc69d
Add Last-Modified header
JosePineiro Aug 26, 2025
aabdb77
Merge branch 'ESP32Async:main' into fix/dynamic-template-etag
JosePineiro Sep 1, 2025
72ab521
fix handleRequest(AsyncWebServerRequest *request)
JosePineiro Sep 2, 2025
9e83287
Merge branch 'main' into fix/dynamic-template-etag
mathieucarbou Sep 9, 2025
04fd5e5
ci(pre-commit): Apply automatic fixes
pre-commit-ci-lite[bot] Sep 9, 2025
25e138e
Update Templates.ino with complete set of examples
willmmiles Sep 27, 2025
f1c8d03
Eliminate some naked pointer usage
willmmiles Sep 27, 2025
3771fdb
Respect Last-Modified if set by user
willmmiles Sep 27, 2025
0c0e958
Merge remote-tracking branch 'origin/main' into pr/JosePineiro/271
willmmiles Sep 28, 2025
40dca4c
ci(pre-commit): Apply automatic fixes
pre-commit-ci-lite[bot] Sep 28, 2025
5a6e9f6
Check allocation failure on Not Modified
willmmiles Sep 28, 2025
0a5913b
Fix template example date
willmmiles Sep 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/ESPAsyncWebServer.h
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ class AsyncWebServerRequest {
friend class AsyncWebServer;
friend class AsyncCallbackWebHandler;
friend class AsyncFileResponse;
friend class AsyncStaticWebHandler;

private:
AsyncClient *_client;
Expand Down
123 changes: 78 additions & 45 deletions src/WebHandlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -187,75 +187,108 @@ bool AsyncStaticWebHandler::_searchFile(AsyncWebServerRequest *request, const St
return found;
}

/**
* @brief Handles an incoming HTTP request for a static file.
*
* This method processes a request for serving static files asynchronously.
* It determines the correct ETag (entity tag) for caching, checks if the file
* has been modified, and prepares the appropriate response (file response or 304 Not Modified).
*
* @param request Pointer to the incoming AsyncWebServerRequest object.
*/
void AsyncStaticWebHandler::handleRequest(AsyncWebServerRequest *request) {
// Get the filename from request->_tempObject and free it
String filename((char *)request->_tempObject);
free(request->_tempObject);
request->_tempObject = NULL;
request->_tempObject = nullptr;

if (request->_tempFile != true) {
request->send(404);
return;
}

time_t lw = request->_tempFile.getLastWrite(); // get last file mod time (if supported by FS)
// set etag to lastmod timestamp if available, otherwise to size
String etag;
if (lw) {
setLastModified(lw);
#if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350)
// time_t == long long int
constexpr size_t len = 1 + 8 * sizeof(time_t);
char buf[len];
char *ret = lltoa(lw ^ request->_tempFile.size(), buf, len, 10);
etag = ret ? String(ret) : String(request->_tempFile.size());
#elif defined(LIBRETINY)
long val = lw ^ request->_tempFile.size();
etag = String(val);
#else
etag = lw ^ request->_tempFile.size(); // etag combines file size and lastmod timestamp
#endif
// Get server ETag. If file is not GZ and we have a Template Processor, ETag=0
char etag[9];
const char* tempFileName = request->_tempFile.name();
const size_t lenFilename = strlen(tempFileName);

if (lenFilename > T__GZ_LEN && memcmp(tempFileName + lenFilename - T__GZ_LEN, T__gz, T__GZ_LEN) == 0) {
//File is a gz, get etag from CRC in trailer
if (!AsyncWebServerRequest::_getEtag(request->_tempFile, etag)) {
// File is corrupted or invalid
log_e("File is corrupted or invalid: %s", tempFileName);
request->send(404);
return;
}

// Reset file position to the beginning so the file can be served from the start.
request->_tempFile.seek(0);
} else if (_callback == nullptr) {
// We don't have a Template processor
uint32_t etagValue;
time_t lastWrite = request->_tempFile.getLastWrite();
if (lastWrite > 0) {
// Use timestamp-based ETag
etagValue = static_cast<uint32_t>(lastWrite);
} else {
// No timestamp available, use filesize-based ETag
size_t fileSize = request->_tempFile.size();
etagValue = static_cast<uint32_t>(fileSize);
}
snprintf(etag, sizeof(etag), "%08x", etagValue);
} else {
#if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(LIBRETINY)
etag = String(request->_tempFile.size());
#else
etag = request->_tempFile.size();
#endif
etag[0] = '\0';
}

bool not_modified = false;
AsyncWebServerResponse *response;

// if-none-match has precedence over if-modified-since
if (request->hasHeader(T_INM)) {
not_modified = request->header(T_INM).equals(etag);
} else if (_last_modified.length()) {
not_modified = request->header(T_IMS).equals(_last_modified);
}
// Get raw header pointers to avoid creating temporary String objects
const char* inm = request->header(T_INM).c_str(); // If-None-Match
const char* ims = request->header(T_IMS).c_str(); // If-Modified-Since

AsyncWebServerResponse *response;
bool notModified = false;
// 1. If the client sent If-None-Match and we have an ETag → compare
if (*etag != '\0' && inm && *inm) {
if (strcmp(inm, etag) == 0) {
notModified = true;
}
}
// 2. Otherwise, if there is no ETag and no Template processor but we have Last-Modified and Last-Modified matches
else if (*etag == '\0' && _callback == nullptr && _last_modified.length() > 0 && ims && *ims && strcmp(ims, _last_modified.c_str()) == 0) {
log_d("_last_modified: %s", _last_modified.c_str());
log_d("ims: %s", ims);
notModified = true;
}

if (not_modified) {
if (notModified) {
request->_tempFile.close();
response = new AsyncBasicResponse(304); // Not modified
} else {
response = new AsyncFileResponse(request->_tempFile, filename, emptyString, false, _callback);
}
if (!response) {
#ifdef ESP32
log_e("Failed to allocate");
#endif
request->abort();
return;
}

if (!response) {
#ifdef ESP32
log_e("Failed to allocate");
#endif
request->abort();
return;
// Set ETag header
if (*etag != '\0') {
response->addHeader(T_ETag, etag, true);
}
// Set Last-Modified header
if (_last_modified.length()) {
response->addHeader(T_Last_Modified, _last_modified.c_str(), true);
}
}

response->addHeader(T_ETag, etag.c_str());

if (_last_modified.length()) {
response->addHeader(T_Last_Modified, _last_modified.c_str());
}
// Set cache control
if (_cache_control.length()) {
response->addHeader(T_Cache_Control, _cache_control.c_str());
response->addHeader(T_Cache_Control, _cache_control.c_str(), false);
}
else {
response->addHeader(T_Cache_Control, T_no_cache, false);
}

request->send(response);
Expand Down
2 changes: 1 addition & 1 deletion src/literals.h
Original file line number Diff line number Diff line change
Expand Up @@ -204,5 +204,5 @@ static constexpr const char *T_only_once_headers[] = {
T_Transfer_Encoding, T_Content_Location, T_Server, T_WWW_AUTH
};
static constexpr size_t T_only_once_headers_len = sizeof(T_only_once_headers) / sizeof(T_only_once_headers[0]);

static constexpr size_t T__GZ_LEN = strlen(T__gz);
} // namespace asyncsrv