-
Notifications
You must be signed in to change notification settings - Fork 66
Fix ETag handling for dynamic template responses #271
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix ETag handling for dynamic template responses #271
Conversation
This PR updates AsyncStaticWebHandler::handleRequest to correctly manage caching headers when template processing introduces dynamic content. The previous implementation always set ETag and Last-Modified headers based on the underlying filesystem metadata, which was misleading when templates generated non-static responses (e.g., current time). Issue ESP32Async#237 (ESP32Async#237) The new implementation: - Uses file-based ETag when .gz files or template callback is not present. - Ensures gzip files generate consistent ETag values using _getEtag. - For non-gzip files, generate ETag values using the timestamp or file size. - Returns 304 Not Modified only when If-None-Match matches the valid ETag. - Removed use of Variable Length Array and String, ensuring the same implementation works consistently on all platforms. This prevents browsers from incorrectly reusing cached content when template output is dynamic, ensuring correctness while still allowing efficient caching for static resources.
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
@JosePineiro @me-no-dev @willmmiles : FYI the previous behavior was using as an etag value the last modification time or file size to speed up file serving and avoid too many file reads, especially on concurrent requests. The drawback is that this is incorrect as per what an etag should be, and also incorrect in regard to the callback option (templating). This PR fixes that for gz file, keeps same behavior for non gz files and considers the callback value. That's a good fix + improvement, even if at the expense of more file reading for gz files. |
Thanks a lot @JosePineiro 👍 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR fixes incorrect ETag handling in AsyncStaticWebHandler to properly manage caching for dynamic template responses. The previous implementation always generated ETags based on file metadata, causing browsers to incorrectly cache dynamic content when templates were used.
- Implements conditional ETag generation based on file type and template presence
- Updates gzip file handling to extract ETag from CRC trailer data
- Removes platform-specific Variable Length Array usage for better portability
Reviewed Changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
File | Description |
---|---|
src/literals.h | Removes unused MJS file type constant and adds GZ_LEN constant for gzip file detection |
src/WebHandlers.cpp | Refactors handleRequest method to conditionally generate ETags and properly handle caching for template responses |
src/ESPAsyncWebServer.h | Adds AsyncStaticWebHandler as friend class to access private _getEtag method |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
+1 for using internal checksums for GZ files. A future PR could maybe generalize this for other kinds of content with some I'm glad to see the handling of templated content is getting some attention too. The previous behaviour of treating it as static was definitely not ideal! I do have some concerns though:
I think what I'd ask for is:
|
@willmmiles |
@JosePineiro : I think we all agree Etags are better but Will mentioned a good point in the fact that a user could decide to control themselves their caching based on their own rules. And I can say that this is typically what was often happening when serving files because caching management in the lib is quite new. So instead of forcing Etags, a better approach could be to read the user request headers and answer appropriately according to the spec in the best way we can |
In funcion void AsyncStaticWebHandler::handleRequest(AsyncWebServerRequest *request) add Last-Modified header if no GZ file or have template processor. Sugeestion of @willmmiles
I think the ETag system used in this PR is ideal for files without templates or GZIP. I can't think of any circumstances where a user would be harmed, even if they use a different system. In template files, we can respect user input in a generic way. This way, the user would have the best of both worlds: an automatic ETagsystem when possible and a manual system controlled by the user in all other cases. Handling ETag and Last-Modified simultaneously complicates and slows down the code. It also leads to potential conflicts whose resolution can be very obscure for the user. If the current behavior doesn't seem right, please tell me what you think is the best options:
|
Re cache_control: The most critical use case for cache_control is if the user has set "max-age" with the intent to reduce the number of transactions on static content. I think this must continue to be respected for static content. For microcontrollers, re-validation is not always the best policy -- every transaction has a cost in CPU time and memory and can affect the performance of other tasks. I think this decision should be left in the hands of the application author. Or to put it another way: sometimes it is preferable to sacrifice correctness in rare cases (ie. the cache may become stale when you perform a firmware update) for performance in the common case (everyday UI access). So I think the logic should be:
Re Does that make sense? |
I think I understand what you're saying. We would send to the browser: This way, the browser won't request the page for the next hour. If the etag remains, we will send: What to do if Cache-Control contains "no-store" or "immutable"? In both cases, an "ETag" is pointless, but sending it won't cause any problems. I still think that if we can use "ETag," we shouldn't send "Last-Modified". If we can't use "ETag," we should send "Last-Modified" if it's user-defined. Does this seem like an acceptable solution to you? |
You got it! :)
I think we can do better. If we send the user "last modified time" as the ETag, then we don't have to parse "If-Modified-Since". Since ETags have no particular required format, I believe we can get away with this simplification. So code like: char etag_buf[9];
char* etag = etag_buf;
const char* tempFileName = request->_tempFile.name();
const size_t lenFilename = strlen(tempFileName);
if (_last_modified.length()) {
etag = _last_modified.c_str();
} else if (lenFilename > T__GZ_LEN && memcmp(tempFileName + lenFilename - T__GZ_LEN, T__gz, T__GZ_LEN) == 0) {
... (above code is not const-correct, but conveys the idea, I hope) |
If the user has defined them, "Last-Modified" and "Cache-Control" are always sent.
@willmmiles Implemented as specified in RFC 7232, section 2.4:
In RFC 7232, section 3.3: Please note that "entity-tag" is ALWAYS sent when "Last-Modified" is sent. Note for @willmmiles: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A couple more little things. Thanks for your perseverence on this, I'm excited to see this merged.
@JosePineiro : could you please tell us if you are still motivated in finishing your PR ? Or if one of us should take over it ? Thanks. |
Now that templates are detected by serveStatic, update the example with the full set of cases.
The underlying objects are all Arduino Strings; leverage that to simplify the code.
This allows caching and validation to happen if users are manually managing this value, as demonstrated in the example.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the new way and examples.
Also easy to follow and understand!
I just wonder if the function call should even be simplified a bit more ;-)
I do want to point out that accepting the approach in this PR does have a breaking semantic change relevant to existing code, if it wasn't clear from the changes to the examples: Excerpt from the current example code
With this PR, the code above will not generate caching headers anymore. To get those caching headers back, the new logic requires Personally I prefer the approach from this PR -- allow |
It makes total sense to me, and I would even say that this is a bug fix from previous behavior : no cache header should be set by default when we have a template. This can lead to more issues than benefits. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
Co-authored-by: Copilot <[email protected]>
This PR updates AsyncStaticWebHandler::handleRequest to correctly manage caching headers when template processing introduces dynamic content. The previous implementation always set ETag and Last-Modified headers based on the underlying filesystem metadata, which was misleading when templates generated non-static responses (e.g., current time). Issue #237
The new implementation:
This prevents browsers from incorrectly reusing cached content when template output is dynamic, ensuring correctness while still allowing efficient caching for static resources.