Skip to content

honor entity-tag If-Range in FileResponse range requests - #13480

Draft
arshsmith1 wants to merge 3 commits into
aio-libs:masterfrom
arshsmith1:fileresponse-ifrange-etag
Draft

honor entity-tag If-Range in FileResponse range requests#13480
arshsmith1 wants to merge 3 commits into
aio-libs:masterfrom
arshsmith1:fileresponse-ifrange-etag

Conversation

@arshsmith1

Copy link
Copy Markdown
Contributor

What do these changes do?

FileResponse only looked at the HTTP-date form of If-Range. request.if_range runs the value through parse_http_date and returns None for anything that is not a date, so an If-Range carrying an entity-tag was treated as absent and the Range was honored unconditionally. A client resuming a download with If-Range: "<etag>" after the file had changed therefore received a 206 partial that it stitched onto the stale bytes it already held, producing a corrupt file. :rfc:9110#section-13.1.5 wants the Range honored only when the validator still matches, otherwise the full 200. 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 full 200.

Are there changes in behavior for the user?

Only for the entity-tag If-Range case. Requests with no If-Range, and the date form, behave exactly as before; a matching strong ETag still serves the 206.

Is it a substantial burden for the maintainers to support this?

No. It reads the raw If-Range header where the date form was already parsed and adds one strong comparison, with two regression tests beside the existing If-Range date tests.

Related issue number

None.

Checklist

  • I think the code is well written
  • Unit tests for the changes exist
  • Documentation reflects the changes — N/A, no public API change
  • If you provide code modification, please add yourself to CONTRIBUTORS.txt
  • Add a new news fragment into the CHANGES/ folder

@psf-chronographer psf-chronographer Bot added the bot:chronographer:provided There is a change note present in this PR label Aug 18, 2026
@codecov

codecov Bot commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.02%. Comparing base (79b5f5f) to head (9f608d3).
⚠️ Report is 30 commits behind head on master.
✅ All tests successful. No failed tests found.

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     
Flag Coverage Δ
Autobahn 22.02% <13.33%> (-0.02%) ⬇️
CI-GHA 98.91% <100.00%> (+<0.01%) ⬆️
OS-Linux 98.69% <100.00%> (+<0.01%) ⬆️
OS-Windows 97.09% <100.00%> (+0.07%) ⬆️
OS-macOS 97.97% <100.00%> (+0.03%) ⬆️
Py-3.10 98.12% <100.00%> (-0.02%) ⬇️
Py-3.11 98.35% <100.00%> (-0.02%) ⬇️
Py-3.12 98.44% <100.00%> (-0.02%) ⬇️
Py-3.13 98.42% <100.00%> (-0.03%) ⬇️
Py-3.14 98.45% <100.00%> (-0.02%) ⬇️
Py-3.14t 97.61% <100.00%> (+0.06%) ⬆️
Py-pypy-3.11 97.41% <100.00%> (-0.01%) ⬇️
VM-macos 97.97% <100.00%> (+0.03%) ⬆️
VM-ubuntu 98.69% <100.00%> (+<0.01%) ⬆️
VM-windows 97.09% <100.00%> (+0.07%) ⬆️
cython-coverage 83.05% <52.94%> (+0.80%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@codspeed-hq

codspeed-hq Bot commented Aug 18, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 94 untouched benchmarks
⏩ 83 skipped benchmarks1


Comparing arshsmith1:fileresponse-ifrange-etag (9f608d3) with master (eb38b3c)

Open in CodSpeed

Footnotes

  1. 83 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Comment thread aiohttp/web_fileresponse.py Outdated
# 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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why has this changed from request.if_range?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

request.if_range needs to be updated to return an etag.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment thread aiohttp/web_fileresponse.py Outdated
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()

@Dreamsorcerer Dreamsorcerer Aug 25, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
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()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment thread aiohttp/web_fileresponse.py Outdated
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}"'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
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}"'

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment thread aiohttp/web_fileresponse.py Outdated
Comment on lines +316 to +321
# 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
# 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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Trimmed it down to the RFC link, thanks.

@Dreamsorcerer Dreamsorcerer added the backport-3.15 Trigger automatic backporting to the 3.15 release branch by Patchback robot label Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport-3.15 Trigger automatic backporting to the 3.15 release branch by Patchback robot bot:chronographer:provided There is a change note present in this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants