diff --git a/httpie/downloads.py b/httpie/downloads.py index 9c4b895e6f..16c1501528 100644 --- a/httpie/downloads.py +++ b/httpie/downloads.py @@ -216,12 +216,23 @@ def start( """ assert not self.status.time_started - # FIXME: some servers still might sent Content-Encoding: gzip + # When Content-Encoding is present (e.g., gzip), requests automatically + # decompresses the response. In this case, Content-Length represents + # the compressed size, but we receive uncompressed bytes. This causes + # a mismatch where downloaded bytes > Content-Length, triggering false + # "Incomplete download" errors. Skip using Content-Length in this case. + # # - try: - total_size = int(final_response.headers['Content-Length']) - except (KeyError, ValueError, TypeError): + content_encoding = final_response.headers.get('Content-Encoding') + if content_encoding: + # Can't use Content-Length for progress tracking when response + # is encoded, as the decoded size will differ. 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..b74377d793 100644 --- a/tests/test_downloads.py +++ b/tests/test_downloads.py @@ -159,6 +159,36 @@ def test_download_no_Content_Length(self, mock_env, httpbin_both): downloader.finish() assert not downloader.interrupted + def test_download_with_Content_Encoding_gzip(self, mock_env, httpbin_both): + """ + Test that when Content-Encoding is present, downloads are not flagged + as interrupted even if the decompressed bytes exceed Content-Length. + + This verifies the fix for https://github.com/httpie/cli/issues/1642 + where gzipped responses incorrectly triggered "Incomplete download" errors + because Content-Length is the compressed size but requests auto-decompresses. + """ + 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={ + # Compressed size in Content-Length + 'Content-Length': 100, + 'Content-Encoding': 'gzip', + } + ) + ) + # Simulate receiving decompressed data (more than Content-Length) + downloader.chunk_downloaded(b'x' * 500) + downloader.chunk_downloaded(b'y' * 500) + downloader.finish() + # Should NOT be interrupted since Content-Encoding was present + # and Content-Length was correctly ignored + assert not downloader.interrupted + def test_download_output_from_content_disposition(self, mock_env, httpbin_both): with tempfile.TemporaryDirectory() as tmp_dirname: orig_cwd = os.getcwd()