Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
This document records all notable changes to [HTTPie](https://httpie.io).
This project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]

- Fix false "Incomplete download" error when downloaded bytes exceed `Content-Length`, which occurs when servers send `Content-Encoding: gzip` and `requests` auto-decompresses. ([#1642](https://github.com/httpie/cli/issues/1642), [#423](https://github.com/httpie/cli/issues/423))

## [3.2.4](https://github.com/httpie/cli/compare/3.2.3...3.2.4) (2024-11-01)

- Fix default certs loading and unpin `requests`. ([#1596](https://github.com/httpie/cli/issues/1596))
Expand Down
15 changes: 12 additions & 3 deletions httpie/downloads.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,8 +216,6 @@ def start(
"""
assert not self.status.time_started

# FIXME: some servers still might sent Content-Encoding: gzip
# <https://github.com/httpie/cli/issues/423>
try:
total_size = int(final_response.headers['Content-Length'])
except (KeyError, ValueError, TypeError):
Expand Down Expand Up @@ -269,10 +267,21 @@ def failed(self):

@property
def interrupted(self) -> bool:
"""
Download is interrupted if we received fewer bytes than expected.

Uses `<` instead of `!=` to handle Content-Encoding (e.g., gzip):
when servers send compressed responses, Content-Length reflects
compressed size, but `requests` auto-decompresses, so downloaded
bytes exceed Content-Length. This is not an interruption.

See https://github.com/httpie/cli/issues/423
See https://github.com/httpie/cli/issues/1642
"""
return (
self.finished
and self.status.total_size
and self.status.total_size != self.status.downloaded
and self.status.downloaded < self.status.total_size
)

def chunk_downloaded(self, chunk: bytes):
Expand Down
33 changes: 33 additions & 0 deletions tests/test_downloads.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,39 @@ def test_download_interrupted(self, mock_env, httpbin_both):
downloader.finish()
assert downloader.interrupted

def test_download_not_interrupted_when_bytes_exceed_content_length(self, mock_env, httpbin_both):
"""
When downloaded bytes exceed Content-Length, this is NOT an interrupted
download. This happens when servers send Content-Encoding (e.g., gzip):
Content-Length reflects compressed size, but requests auto-decompresses,
so we receive more bytes than Content-Length indicates.

The fix uses `downloaded < total_size` instead of `!=` to detect
interruptions, so receiving MORE bytes than expected is considered complete.

Regression test for https://github.com/httpie/cli/issues/1642
See also https://github.com/httpie/cli/issues/423
"""
with open(os.devnull, 'w') as devnull:
downloader = Downloader(mock_env, output_file=devnull)
downloader.start(
initial_url='/',
final_response=Response(
url=httpbin_both.url + '/',
headers={
'Content-Length': 100,
}
)
)
# Simulate receiving MORE bytes than Content-Length
downloader.chunk_downloaded(b'x' * 150)
downloader.finish()
# Should NOT be marked as interrupted (got more than expected)
assert not downloader.interrupted
# Progress tracking should still work (total_size preserved)
assert downloader.status.total_size == 100
assert downloader.status.downloaded == 150

def test_download_resumed(self, mock_env, httpbin_both):
with tempfile.TemporaryDirectory() as tmp_dirname:
file = os.path.join(tmp_dirname, 'file.bin')
Expand Down
Loading