From dace5b3bfc39defd927d6349a691980ab8db535c Mon Sep 17 00:00:00 2001 From: abo3losh1 Date: Sun, 27 Sep 2026 03:40:55 +0200 Subject: [PATCH 1/2] Test that a resumed download rejects an ignored Range request A server can advertise byte ranges, then return the full file with status 200 to a resumed request. The downloader previously appended that body to the partial file and reported success. The regression test checks the response is rejected and the destination is not published. --- tests/utils/test_download.py | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/tests/utils/test_download.py b/tests/utils/test_download.py index 6a96d65915f..f2dd884492f 100644 --- a/tests/utils/test_download.py +++ b/tests/utils/test_download.py @@ -92,6 +92,27 @@ def handle_request(request: PreparedRequest) -> HttpResponse: assert http.calls[-1].request.headers["Range"] == f"bytes={file_length // 2}-" +def test_download_file_fails_if_server_ignores_resume_range( + http: responses.RequestsMock, tmp_path: Path +) -> None: + body = b"abcdefgh" + url = "https://foo.com/archive.tar.gz" + + def handle_request(request: PreparedRequest) -> HttpResponse: + if "Range" not in request.headers: + return 200, {"Content-Length": "8", "Accept-Ranges": "bytes"}, body[:4] + return 200, {"Content-Length": "8"}, body + + http.add_callback(responses.GET, url, callback=handle_request) + dest = tmp_path / "archive.tar.gz" + + with pytest.raises(ChunkedEncodingError, match=r"ignored.*Range"): + download_file(url, dest, chunk_size=4, max_retries=1) + + assert http.calls[-1].request.headers["Range"] == "bytes=4-" + assert not dest.exists() + + def test_download_file_fail_when_no_range( http: responses.RequestsMock, fixture_dir: FixtureDirGetter, tmp_path: Path ) -> None: @@ -205,6 +226,9 @@ class _Resp: "Content-Length": "8", } + def __init__(self, status_code: int) -> None: + self.status_code = status_code + def raise_for_status(self) -> None: ... def close(self) -> None: ... def __enter__(self) -> _Resp: @@ -220,8 +244,8 @@ def iter_content(self, chunk_size: int = 1) -> Iterator[bytes]: raise exc("stub failure") class _Session: - def get(self, *_: object, **__: object) -> _Resp: - return _Resp() + def get(self, *_: object, headers: dict[str, str], **__: object) -> _Resp: + return _Resp(status_code=206 if "Range" in headers else 200) downloader = Downloader( "https://example.invalid/x", @@ -252,6 +276,9 @@ class _Resp: "Content-Length": "8", } + def __init__(self, status_code: int) -> None: + self.status_code = status_code + def raise_for_status(self) -> None: ... def close(self) -> None: ... def __enter__(self) -> _Resp: @@ -266,8 +293,8 @@ def iter_content(self, chunk_size: int = 1) -> Iterator[bytes]: raise requests.exceptions.ConnectionError("stub failure") class _Session: - def get(self, *_: object, **__: object) -> _Resp: - return _Resp() + def get(self, *_: object, headers: dict[str, str], **__: object) -> _Resp: + return _Resp(status_code=206 if "Range" in headers else 200) downloader = Downloader( "https://example.invalid/x", From 104a1a77fca15c3a7a82efa8829794ac76f13fa3 Mon Sep 17 00:00:00 2001 From: abo3losh1 Date: Sun, 27 Sep 2026 03:40:55 +0200 Subject: [PATCH 2/2] Reject full responses to resumed download requests A resumed request must return HTTP 206. If a server ignores Range and returns 200, close the response and raise ChunkedEncodingError so the partial data cannot be combined with a full file. --- src/poetry/utils/download.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/poetry/utils/download.py b/src/poetry/utils/download.py index 2da6c360c00..7c841b0c53a 100644 --- a/src/poetry/utils/download.py +++ b/src/poetry/utils/download.py @@ -100,6 +100,10 @@ def _get(self, start: int = 0) -> Response: ) try: response.raise_for_status() + if start > 0 and response.status_code != 206: + raise ChunkedEncodingError( + f"Server ignored the Range request while resuming {self._url}." + ) return response except BaseException: response.close()