Skip to content

Avoid mutating FileResponse headers on range requests#3144

Merged
Kludex merged 2 commits intomainfrom
fix/file-response-reusable
Feb 14, 2026
Merged

Avoid mutating FileResponse headers on range requests#3144
Kludex merged 2 commits intomainfrom
fix/file-response-reusable

Conversation

@Kludex
Copy link
Owner

@Kludex Kludex commented Feb 14, 2026

FileResponse mutates self.headers when handling range requests, making the instance unusable for subsequent calls. Copy raw_headers into a local MutableHeaders before setting content-range, content-type, and content-length.

Credit to @viccie30.

FileResponse mutates self.headers when handling range requests,
making the instance unusable for subsequent calls. Copy raw_headers
into a local MutableHeaders before setting content-range,
content-type, and content-length.
@Kludex Kludex self-assigned this Feb 14, 2026
Replace confusing parametrized test that calls other test functions
with a straightforward range-then-full-request assertion.
@Kludex Kludex changed the title Copy headers before mutating in range responses Avoid mutating FileResponse headers on range requests Feb 14, 2026
@Kludex Kludex merged commit 3bddf51 into main Feb 14, 2026
6 checks passed
@Kludex Kludex deleted the fix/file-response-reusable branch February 14, 2026 13:19
@viccie30
Copy link
Contributor

I added the parameterized test, because some combinations already worked and some didn't. It exercises all 9 combinations of all three possible code paths (no range, simple range, multiple ranges).

I agree that the simplified test is easier to follow, but it doesn't provide the coverage of the parameterized test.

@Kludex
Copy link
Owner Author

Kludex commented Feb 15, 2026

Hmm... Which ones didn't work? Can we add only the ones that were failing in main?

@viccie30
Copy link
Contributor

viccie30 commented Feb 15, 2026

Checking out my original commit, then discarding my changes to starlette/responses.py and running the test gives 5 failures (times 2, because all tests are run with both asyncio and trio):

  • Simple range -> no range
  • Multiple ranges -> no range
  • Multiple ranges -> simple range
  • Simple range -> multiple ranges
  • Multiple ranges -> multiple ranges

(The order is backwards in the name of the test, because I should have switched the order of the decorators.)

$ git checkout 6bf294ba80174145ba459f6a336641516a279838
$ git checkout HEAD^ -- starlette/responses.py
$ uv run pytest -k test_file_response_multiple_calls -rf
...
=========================== short test summary info ============================
FAILED tests/test_responses.py::test_file_response_multiple_calls[asyncio-test_file_response_without_range-test_file_response_range] - assert 'content-range' not in Headers({'content-type': 'text/plain; charset...
FAILED tests/test_responses.py::test_file_response_multiple_calls[asyncio-test_file_response_without_range-test_file_response_range_multi] - AssertionError: assert '448' == '526'
FAILED tests/test_responses.py::test_file_response_multiple_calls[asyncio-test_file_response_range-test_file_response_range_multi] - AssertionError: assert 'multipart/by...ce7954c8c33f9' == 'text/plain; chars...
FAILED tests/test_responses.py::test_file_response_multiple_calls[asyncio-test_file_response_range_multi-test_file_response_range] - assert 'content-range' not in Headers({'content-type': 'multipart/byterange...
FAILED tests/test_responses.py::test_file_response_multiple_calls[asyncio-test_file_response_range_multi-test_file_response_range_multi] - AssertionError: assert '512' == '448'
FAILED tests/test_responses.py::test_file_response_multiple_calls[trio-test_file_response_without_range-test_file_response_range] - assert 'content-range' not in Headers({'content-type': 'text/plain; charset...
FAILED tests/test_responses.py::test_file_response_multiple_calls[trio-test_file_response_without_range-test_file_response_range_multi] - AssertionError: assert '448' == '526'
FAILED tests/test_responses.py::test_file_response_multiple_calls[trio-test_file_response_range-test_file_response_range_multi] - AssertionError: assert 'multipart/by...fc75ee3b0533e' == 'text/plain; chars...
FAILED tests/test_responses.py::test_file_response_multiple_calls[trio-test_file_response_range_multi-test_file_response_range] - assert 'content-range' not in Headers({'content-type': 'multipart/byterange...
FAILED tests/test_responses.py::test_file_response_multiple_calls[trio-test_file_response_range_multi-test_file_response_range_multi] - AssertionError: assert '512' == '448'
================= 10 failed, 8 passed, 945 deselected in 0.49s =================

It's possible, of course, to just add those 5 cases. Alternatively, I've attached a patch on top of main that would restore the parametrized test, but with clearer ids and comments. It should apply cleanly with patch -Np1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants