Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
120 changes: 76 additions & 44 deletions src/WebHandlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -187,75 +187,107 @@ 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);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ESP32 logging call must be #ifdef'd on ESP32.

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) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should respect _last_modified if the user has set it even if they're running the template processor. While this feature could be misused, it provides a mechanism to explicitly allow caching behaviour for templates if the user calls setLastModified() when the template variables are updated.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see bug #237.
This PR attempts to fix that bug. The behavior you suggest is precisely what's causing problems for the user.
If you think the bug is incorrect, please let me know, and I'll discard this PR.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proposed code still fixes that bug. You have removed the call to setLastModified() in the generation path, so unless the user has explicitly called setLastModified(), no "Last-Modified" header will be produced in non-static cases. The default behaviour would be correct for #237, without defeating an advanced user's ability to enable caching based on external knowledge of template state.

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) {
if (!response) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a regression here -- this check must also applied to the AsyncBasicResponse above.

#ifdef ESP32
log_e("Failed to allocate");
log_e("Failed to allocate");
#endif
request->abort();
return;
}

response->addHeader(T_ETag, etag.c_str());
request->abort();
return;
}

if (_last_modified.length()) {
response->addHeader(T_Last_Modified, _last_modified.c_str());
// 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);
}
}

// 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
Loading