From e47f36599321a5aa569421326b97bae7824b61d4 Mon Sep 17 00:00:00 2001 From: Rodion Steshenko Date: Tue, 17 Feb 2026 22:05:51 -0500 Subject: [PATCH] Fix #1642: Don't flag download as interrupted when Content-Encoding is set When Content-Encoding (e.g. gzip) is present, the requests library transparently decompresses the response body. This means the actual bytes written will be larger than Content-Length (which reflects the compressed size). Previously, httpie would compare Content-Length against the decompressed byte count and incorrectly flag the download as 'Incomplete'. Fix: When Content-Encoding is set, skip Content-Length for progress tracking since the sizes are inherently mismatched. Also resolves the longstanding FIXME from #423. Added test: test_download_with_Content_Encoding_gzip_not_interrupted --- httpie/downloads.py | 19 ++++++++++++++----- tests/test_downloads.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/httpie/downloads.py b/httpie/downloads.py index 9c4b895e6f..7baca0ea69 100644 --- a/httpie/downloads.py +++ b/httpie/downloads.py @@ -216,12 +216,21 @@ def start( """ assert not self.status.time_started - # FIXME: some servers still might sent Content-Encoding: gzip - # - try: - total_size = int(final_response.headers['Content-Length']) - except (KeyError, ValueError, TypeError): + # When Content-Encoding is set (e.g. gzip), the requests library + # transparently decompresses the response body. This means the + # actual bytes written will differ from Content-Length (which + # reflects the compressed size). In this case, we cannot rely + # on Content-Length for progress/completeness tracking. + # See: https://github.com/httpie/cli/issues/423 + # See: https://github.com/httpie/cli/issues/1642 + content_encoding = final_response.headers.get('Content-Encoding') + if content_encoding: total_size = None + else: + try: + total_size = int(final_response.headers['Content-Length']) + except (KeyError, ValueError, TypeError): + total_size = None if not self._output_file: self._output_file = self._get_output_file_from_response( diff --git a/tests/test_downloads.py b/tests/test_downloads.py index b646a0e6a5..a949b47837 100644 --- a/tests/test_downloads.py +++ b/tests/test_downloads.py @@ -247,6 +247,36 @@ def test_download_resumed(self, mock_env, httpbin_both): downloader.chunk_downloaded(b'45') downloader.finish() + def test_download_with_Content_Encoding_gzip_not_interrupted(self, mock_env, httpbin_both): + """ + When Content-Encoding is set (e.g. gzip), the requests library + transparently decompresses the body, so the downloaded bytes + will differ from Content-Length (which is the compressed size). + httpie should NOT flag this as an interrupted download. + + Regression test for https://github.com/httpie/cli/issues/1642 + """ + with open(os.devnull, 'w') as devnull: + downloader = Downloader(mock_env, output_file=devnull) + # Simulate a gzip-encoded response where Content-Length is + # the compressed size (100 bytes) but the decompressed body + # is larger (500 bytes). + downloader.start( + initial_url='/', + final_response=Response( + url=httpbin_both.url + '/', + headers={ + 'Content-Length': 100, + 'Content-Encoding': 'gzip', + } + ) + ) + # Simulate receiving decompressed data (larger than Content-Length) + downloader.chunk_downloaded(b'x' * 500) + downloader.finish() + # Should NOT be flagged as interrupted + assert not downloader.interrupted + def test_download_with_redirect_original_url_used_for_filename(self, httpbin): # Redirect from `/redirect/1` to `/get`. expected_filename = '1.json'