Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
105 changes: 56 additions & 49 deletions src/WebHandlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,15 @@ 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);
Expand All @@ -198,66 +207,64 @@ void AsyncStaticWebHandler::handleRequest(AsyncWebServerRequest *request) {
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
} 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
}

bool not_modified = false;
// 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;
}

// 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);
// Reset file position to the beginning after reading the gz trailer for ETag,
// 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 {
etag[0] = '\0';
}

AsyncWebServerResponse *response;

if (not_modified) {
// Check if client already has the file (ETag match)
if (*etag != '\0' && request->header(T_INM).equals(etag)) {
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;
}

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

if (_last_modified.length()) {
response->addHeader(T_Last_Modified, _last_modified.c_str());
}
if (_cache_control.length()) {
response->addHeader(T_Cache_Control, _cache_control.c_str());
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");
#endif
request->abort();
return;
}
if (*etag != '\0') {
response->addHeader(T_ETag, etag, true);
response->addHeader(T_Cache_Control, T_no_cache, true);
}
else if (_cache_control.length()) {
response->addHeader(T_Cache_Control, _cache_control.c_str(), false);
Copy link
Preview

Copilot AI Aug 25, 2025

Choose a reason for hiding this comment

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

[nitpick] The else-if condition creates an asymmetric caching behavior where files with ETags always get 'no-cache' directive, while files without ETags might get custom cache control. This logic should be clarified or the cache control handling should be more consistent.

Suggested change
response->addHeader(T_Cache_Control, T_no_cache, true);
}
else if (_cache_control.length()) {
response->addHeader(T_Cache_Control, _cache_control.c_str(), false);
}
if (_cache_control.length()) {
response->addHeader(T_Cache_Control, _cache_control.c_str(), false);
} else {
response->addHeader(T_Cache_Control, T_no_cache, true);

Copilot uses AI. Check for mistakes.

}
}

request->send(response);
}

Expand Down
3 changes: 1 addition & 2 deletions src/literals.h
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,6 @@ static constexpr const char *T__jpg = ".jpg"; // JPEG/JPG: Photos. Legacy s
static constexpr const char *T__js = ".js"; // JavaScript: Interactive functionality
static constexpr const char *T__json = ".json"; // JSON: Data exchange format
static constexpr const char *T__mp4 = ".mp4"; // MP4: Proprietary format. Worse compression than WEBM.
static constexpr const char *T__mjs = ".mjs"; // MJS: JavaScript module format
static constexpr const char *T__opus = ".opus"; // OPUS: High compression audio format
static constexpr const char *T__pdf = ".pdf"; // PDF: Universal document format
static constexpr const char *T__png = ".png"; // PNG: Icons, logos, transparency. Legacy support
Expand Down Expand Up @@ -204,5 +203,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