diff --git a/examples/ChunkResponse/ChunkResponse.ino b/examples/ChunkResponse/ChunkResponse.ino index c888cd06..2877e68d 100644 --- a/examples/ChunkResponse/ChunkResponse.ino +++ b/examples/ChunkResponse/ChunkResponse.ino @@ -98,7 +98,7 @@ void setup() { // curl -N -v -H "if-none-match: 4272" http://192.168.4.1/ --output - // server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { - String etag = String(htmlContentLength); + String etag = "\"" + String(htmlContentLength) + "\""; // RFC9110: ETag must be enclosed in double quotes if (request->header(asyncsrv::T_INM) == etag) { request->send(304); diff --git a/src/AsyncWebServerRequest.cpp b/src/AsyncWebServerRequest.cpp index 15d42e90..09fcee86 100644 --- a/src/AsyncWebServerRequest.cpp +++ b/src/AsyncWebServerRequest.cpp @@ -35,7 +35,7 @@ void AsyncWebServerRequest::send(FS &fs, const String &path, const char *content // ETag validation if (this->hasHeader(asyncsrv::T_INM)) { // Generate server ETag from CRC in gzip trailer - char serverETag[9]; + char serverETag[11]; if (!_getEtag(gzFile, serverETag)) { // Compressed file not found or invalid send(404); @@ -58,14 +58,15 @@ void AsyncWebServerRequest::send(FS &fs, const String &path, const char *content } /** - * @brief Generates an ETag string from the CRC32 trailer of a GZIP file. + * @brief Generates an ETag string (enclosed into quotes) from the CRC32 trailer of a GZIP file. * * This function reads the CRC32 checksum (4 bytes) located at the end of a GZIP-compressed file - * and converts it into an 8-character hexadecimal ETag string (null-terminated). + * and converts it into an 8-character hexadecimal ETag string (enclosed in double quotes and null-terminated). + * Double quotes for ETag value are required by RFC9110 section 8.8.3. * * @param gzFile Opened file handle pointing to the GZIP file. * @param eTag Output buffer to store the generated ETag. - * Must be pre-allocated with at least 9 bytes (8 for hex digits + 1 for null terminator). + * Must be pre-allocated with at least 11 bytes (8 for hex digits + 2 for quotes + 1 for null terminator). * * @return true if the ETag was successfully generated, false otherwise (e.g., file too short or seek failed). */ @@ -79,15 +80,17 @@ bool AsyncWebServerRequest::_getEtag(File gzFile, char *etag) { uint32_t crc; gzFile.read(reinterpret_cast(&crc), sizeof(crc)); - etag[0] = hexChars[(crc >> 4) & 0x0F]; - etag[1] = hexChars[crc & 0x0F]; - etag[2] = hexChars[(crc >> 12) & 0x0F]; - etag[3] = hexChars[(crc >> 8) & 0x0F]; - etag[4] = hexChars[(crc >> 20) & 0x0F]; - etag[5] = hexChars[(crc >> 16) & 0x0F]; - etag[6] = hexChars[(crc >> 28)]; - etag[7] = hexChars[(crc >> 24) & 0x0F]; - etag[8] = '\0'; + etag[0] = '"'; + etag[1] = hexChars[(crc >> 4) & 0x0F]; + etag[2] = hexChars[crc & 0x0F]; + etag[3] = hexChars[(crc >> 12) & 0x0F]; + etag[4] = hexChars[(crc >> 8) & 0x0F]; + etag[5] = hexChars[(crc >> 20) & 0x0F]; + etag[6] = hexChars[(crc >> 16) & 0x0F]; + etag[7] = hexChars[(crc >> 28)]; + etag[8] = hexChars[(crc >> 24) & 0x0F]; + etag[9] = '"'; + etag[10] = '\0'; return true; } diff --git a/src/WebHandlers.cpp b/src/WebHandlers.cpp index 97bb4101..cb0860e0 100644 --- a/src/WebHandlers.cpp +++ b/src/WebHandlers.cpp @@ -210,7 +210,7 @@ void AsyncStaticWebHandler::handleRequest(AsyncWebServerRequest *request) { } // Get server ETag. If file is not GZ and we have a Template Processor, ETag is set to an empty string - char etag[9]; + char etag[11]; const char *tempFileName = request->_tempFile.name(); const size_t lenFilename = strlen(tempFileName); @@ -237,7 +237,8 @@ void AsyncStaticWebHandler::handleRequest(AsyncWebServerRequest *request) { size_t fileSize = request->_tempFile.size(); etagValue = static_cast(fileSize); } - snprintf(etag, sizeof(etag), "%08" PRIx32, etagValue); + // RFC9110 Section-8.8.3: Value of the ETag response must be enclosed in double quotes + snprintf(etag, sizeof(etag), "\"%08" PRIx32 "\"", etagValue); } else { etag[0] = '\0'; } diff --git a/src/WebResponses.cpp b/src/WebResponses.cpp index 15ddb6c2..19487ea9 100644 --- a/src/WebResponses.cpp +++ b/src/WebResponses.cpp @@ -732,7 +732,7 @@ AsyncFileResponse::AsyncFileResponse(FS &fs, const String &path, const char *con gzPath.concat(asyncsrv::T__gz); _content = fs.open(gzPath, fs::FileOpenMode::read); - char serverETag[9]; + char serverETag[11]; if (AsyncWebServerRequest::_getEtag(_content, serverETag)) { addHeader(T_Content_Encoding, T_gzip, false); _callback = nullptr; // Unable to process zipped templates