Skip to content

Commit 4c86b44

Browse files
Merge pull request #367 from StefanOberhumer/ETag_RFC9110
Fix: Process ETag header value to be RFC 9110 compliant.
2 parents 05566ca + a8f976c commit 4c86b44

File tree

4 files changed

+21
-17
lines changed

4 files changed

+21
-17
lines changed

examples/ChunkResponse/ChunkResponse.ino

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ void setup() {
9898
// curl -N -v -H "if-none-match: 4272" http://192.168.4.1/ --output -
9999
//
100100
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
101-
String etag = String(htmlContentLength);
101+
String etag = "\"" + String(htmlContentLength) + "\""; // RFC9110: ETag must be enclosed in double quotes
102102

103103
if (request->header(asyncsrv::T_INM) == etag) {
104104
request->send(304);

src/AsyncWebServerRequest.cpp

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ void AsyncWebServerRequest::send(FS &fs, const String &path, const char *content
3535
// ETag validation
3636
if (this->hasHeader(asyncsrv::T_INM)) {
3737
// Generate server ETag from CRC in gzip trailer
38-
char serverETag[9];
38+
char serverETag[11];
3939
if (!_getEtag(gzFile, serverETag)) {
4040
// Compressed file not found or invalid
4141
send(404);
@@ -58,14 +58,15 @@ void AsyncWebServerRequest::send(FS &fs, const String &path, const char *content
5858
}
5959

6060
/**
61-
* @brief Generates an ETag string from the CRC32 trailer of a GZIP file.
61+
* @brief Generates an ETag string (enclosed into quotes) from the CRC32 trailer of a GZIP file.
6262
*
6363
* This function reads the CRC32 checksum (4 bytes) located at the end of a GZIP-compressed file
64-
* and converts it into an 8-character hexadecimal ETag string (null-terminated).
64+
* and converts it into an 8-character hexadecimal ETag string (enclosed in double quotes and null-terminated).
65+
* Double quotes for ETag value are required by RFC9110 section 8.8.3.
6566
*
6667
* @param gzFile Opened file handle pointing to the GZIP file.
6768
* @param eTag Output buffer to store the generated ETag.
68-
* Must be pre-allocated with at least 9 bytes (8 for hex digits + 1 for null terminator).
69+
* Must be pre-allocated with at least 11 bytes (8 for hex digits + 2 for quotes + 1 for null terminator).
6970
*
7071
* @return true if the ETag was successfully generated, false otherwise (e.g., file too short or seek failed).
7172
*/
@@ -79,15 +80,17 @@ bool AsyncWebServerRequest::_getEtag(File gzFile, char *etag) {
7980
uint32_t crc;
8081
gzFile.read(reinterpret_cast<uint8_t *>(&crc), sizeof(crc));
8182

82-
etag[0] = hexChars[(crc >> 4) & 0x0F];
83-
etag[1] = hexChars[crc & 0x0F];
84-
etag[2] = hexChars[(crc >> 12) & 0x0F];
85-
etag[3] = hexChars[(crc >> 8) & 0x0F];
86-
etag[4] = hexChars[(crc >> 20) & 0x0F];
87-
etag[5] = hexChars[(crc >> 16) & 0x0F];
88-
etag[6] = hexChars[(crc >> 28)];
89-
etag[7] = hexChars[(crc >> 24) & 0x0F];
90-
etag[8] = '\0';
83+
etag[0] = '"';
84+
etag[1] = hexChars[(crc >> 4) & 0x0F];
85+
etag[2] = hexChars[crc & 0x0F];
86+
etag[3] = hexChars[(crc >> 12) & 0x0F];
87+
etag[4] = hexChars[(crc >> 8) & 0x0F];
88+
etag[5] = hexChars[(crc >> 20) & 0x0F];
89+
etag[6] = hexChars[(crc >> 16) & 0x0F];
90+
etag[7] = hexChars[(crc >> 28)];
91+
etag[8] = hexChars[(crc >> 24) & 0x0F];
92+
etag[9] = '"';
93+
etag[10] = '\0';
9194

9295
return true;
9396
}

src/WebHandlers.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ void AsyncStaticWebHandler::handleRequest(AsyncWebServerRequest *request) {
210210
}
211211

212212
// Get server ETag. If file is not GZ and we have a Template Processor, ETag is set to an empty string
213-
char etag[9];
213+
char etag[11];
214214
const char *tempFileName = request->_tempFile.name();
215215
const size_t lenFilename = strlen(tempFileName);
216216

@@ -237,7 +237,8 @@ void AsyncStaticWebHandler::handleRequest(AsyncWebServerRequest *request) {
237237
size_t fileSize = request->_tempFile.size();
238238
etagValue = static_cast<uint32_t>(fileSize);
239239
}
240-
snprintf(etag, sizeof(etag), "%08" PRIx32, etagValue);
240+
// RFC9110 Section-8.8.3: Value of the ETag response must be enclosed in double quotes
241+
snprintf(etag, sizeof(etag), "\"%08" PRIx32 "\"", etagValue);
241242
} else {
242243
etag[0] = '\0';
243244
}

src/WebResponses.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -732,7 +732,7 @@ AsyncFileResponse::AsyncFileResponse(FS &fs, const String &path, const char *con
732732
gzPath.concat(asyncsrv::T__gz);
733733
_content = fs.open(gzPath, fs::FileOpenMode::read);
734734

735-
char serverETag[9];
735+
char serverETag[11];
736736
if (AsyncWebServerRequest::_getEtag(_content, serverETag)) {
737737
addHeader(T_Content_Encoding, T_gzip, false);
738738
_callback = nullptr; // Unable to process zipped templates

0 commit comments

Comments
 (0)