honor entity-tag If-Range in FileResponse range requests - #13480
honor entity-tag If-Range in FileResponse range requests#13480arshsmith1 wants to merge 3 commits into
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #13480 +/- ##
==========================================
+ Coverage 99.00% 99.02% +0.01%
==========================================
Files 132 135 +3
Lines 49626 50496 +870
Branches 2575 2652 +77
==========================================
+ Hits 49132 50003 +871
Misses 370 370
+ Partials 124 123 -1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. |
Merging this PR will not alter performance
Comparing Footnotes
|
| # entity-tag form is handled here with the strong comparison If-Range | ||
| # requires, so a stale ETag no longer yields a partial from a changed | ||
| # file. | ||
| if_range = request.headers.get(hdrs.IF_RANGE) |
There was a problem hiding this comment.
Why has this changed from request.if_range?
There was a problem hiding this comment.
request.if_range needs to be updated to return an etag.
There was a problem hiding this comment.
Good call, moved the parsing into if_range instead of reading the raw header. It now returns the ETag for the entity-tag form, so the property is datetime | ETag | None and _prepare_open_file just switches on the type.
| if if_range is None: | ||
| range_applies = True | ||
| elif (if_range_date := parse_http_date(if_range)) is not None: | ||
| range_applies = file_mtime <= if_range_date.timestamp() |
There was a problem hiding this comment.
| range_applies = file_mtime <= if_range_date.timestamp() | |
| # https://www.rfc-editor.org/info/rfc9110/#section-13.1.5-10.2 | |
| range_applies = file_mtime == if_range_date.timestamp() |
There was a problem hiding this comment.
Done, switched to equality. One wrinkle worth flagging: Last-Modified goes out as math.ceil(mtime), so the strong compare has to be against that rounded value (math.ceil(file_mtime) == if_range.timestamp()), otherwise the sub-second mtime never matches the whole-second date the client echoes back and the range would never apply. Added a matching-date test that round-trips the Last-Modified header to cover it.
| elif (if_range_date := parse_http_date(if_range)) is not None: | ||
| range_applies = file_mtime <= if_range_date.timestamp() | ||
| else: | ||
| range_applies = if_range.strip() == f'"{etag_value}"' |
There was a problem hiding this comment.
| range_applies = if_range.strip() == f'"{etag_value}"' | |
| # https://www.rfc-editor.org/info/rfc9110/#section-13.1.5-12.1 | |
| range_applies = if_range.strip() == f'"{etag_value}"' |
There was a problem hiding this comment.
Applied, with the RFC anchor. Since if_range now hands back an ETag, the compare is if_range.value == etag_value and it also drops a weak validator (not if_range.is_weak), which If-Range requires a strong match for.
| # If-Range: only honor the Range when the validator still matches the | ||
| # current representation, otherwise serve the whole 200 (RFC 9110 | ||
| # section 13.1.5). request.if_range parses the HTTP-date form; the | ||
| # entity-tag form is handled here with the strong comparison If-Range | ||
| # requires, so a stale ETag no longer yields a partial from a changed | ||
| # file. |
There was a problem hiding this comment.
| # If-Range: only honor the Range when the validator still matches the | |
| # current representation, otherwise serve the whole 200 (RFC 9110 | |
| # section 13.1.5). request.if_range parses the HTTP-date form; the | |
| # entity-tag form is handled here with the strong comparison If-Range | |
| # requires, so a stale ETag no longer yields a partial from a changed | |
| # file. | |
| # https://www.rfc-editor.org/info/rfc9110/#name-if-range |
There was a problem hiding this comment.
Trimmed it down to the RFC link, thanks.
What do these changes do?
FileResponseonly looked at the HTTP-date form ofIf-Range.request.if_rangeruns the value throughparse_http_dateand returnsNonefor anything that is not a date, so anIf-Rangecarrying an entity-tag was treated as absent and theRangewas honored unconditionally. A client resuming a download withIf-Range: "<etag>"after the file had changed therefore received a206partial that it stitched onto the stale bytes it already held, producing a corrupt file. :rfc:9110#section-13.1.5wants theRangehonored only when the validator still matches, otherwise the full200. The strong entity-tag comparison now happens inside_prepare_open_file, next to where the date form was already handled, so a stale (or weak) validator falls back to a full200.Are there changes in behavior for the user?
Only for the entity-tag
If-Rangecase. Requests with noIf-Range, and the date form, behave exactly as before; a matching strong ETag still serves the206.Is it a substantial burden for the maintainers to support this?
No. It reads the raw
If-Rangeheader where the date form was already parsed and adds one strong comparison, with two regression tests beside the existingIf-Rangedate tests.Related issue number
None.
Checklist
CONTRIBUTORS.txtCHANGES/folder