From b12fc156ceef37024e47bd7e40e6152c28295cdd Mon Sep 17 00:00:00 2001 From: Bryce Yung Date: Tue, 9 Dec 2025 18:43:01 -0500 Subject: [PATCH 1/2] Fix: Correctly unescape key/value parts in KeyValueArgType --- httpie/cli/argtypes.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/httpie/cli/argtypes.py b/httpie/cli/argtypes.py index 8f19c3c51e..dced7ce397 100644 --- a/httpie/cli/argtypes.py +++ b/httpie/cli/argtypes.py @@ -92,13 +92,20 @@ def __call__(self, s: str) -> KeyValueArg: # Starting first, longest separator found. sep = found[min(found.keys())] - key, value = token.split(sep, 1) + key_part, value_part = token.split(sep, 1) - # Any preceding tokens are part of the key. - key = ''.join(tokens[:i]) + key + # The key is composed of: + # 1. Any preceding tokens (unescaped by str(t) in join) + # 2. The key part of the current token (re-tokenize to + # handle internal escapes correctly) + key_tokens = tokens[:i] + self.tokenize(key_part) + key = ''.join(str(t) for t in key_tokens) - # Any following tokens are part of the value. - value += ''.join(tokens[i + 1:]) + # The value is composed of: + # 1. The value part of the current token (re-tokenize) + # 2. Any succeeding tokens (unescaped by str(t) in join) + value_tokens = self.tokenize(value_part) + tokens[i + 1:] + value = ''.join(str(t) for t in value_tokens) break @@ -272,4 +279,4 @@ def response_mime_type(mime_type: str) -> str: if mime_type.count('/') != 1: raise argparse.ArgumentTypeError( f'{mime_type!r} doesn’t look like a mime type; use type/subtype') - return mime_type + return mime_type \ No newline at end of file From 2403d88fc9c8a37e88bbabd5ebd6a52841e5d6b5 Mon Sep 17 00:00:00 2001 From: Bryce Yung Date: Tue, 9 Dec 2025 19:10:24 -0500 Subject: [PATCH 2/2] Fix: Correctly handle Content-Length for compressed downloads (--download) --- httpie/cli/argtypes.py | 17 +++++++++-------- httpie/core.py | 16 ++++++++++++++++ 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/httpie/cli/argtypes.py b/httpie/cli/argtypes.py index dced7ce397..d38a883cf1 100644 --- a/httpie/cli/argtypes.py +++ b/httpie/cli/argtypes.py @@ -95,16 +95,17 @@ def __call__(self, s: str) -> KeyValueArg: key_part, value_part = token.split(sep, 1) # The key is composed of: - # 1. Any preceding tokens (unescaped by str(t) in join) - # 2. The key part of the current token (re-tokenize to - # handle internal escapes correctly) - key_tokens = tokens[:i] + self.tokenize(key_part) + # 1. Any preceding tokens (already Escaped or regular strings) + # 2. The key part of the current token (re-tokenize to catch internal escapes) + key_tokens = tokens[:i] + key_tokens.extend(self.tokenize(key_part)) key = ''.join(str(t) for t in key_tokens) # The value is composed of: - # 1. The value part of the current token (re-tokenize) - # 2. Any succeeding tokens (unescaped by str(t) in join) - value_tokens = self.tokenize(value_part) + tokens[i + 1:] + # 1. The value part of the current token (re-tokenize to catch internal escapes) + # 2. Any succeeding tokens + value_tokens = self.tokenize(value_part) + value_tokens.extend(tokens[i + 1:]) value = ''.join(str(t) for t in value_tokens) break @@ -279,4 +280,4 @@ def response_mime_type(mime_type: str) -> str: if mime_type.count('/') != 1: raise argparse.ArgumentTypeError( f'{mime_type!r} doesn’t look like a mime type; use type/subtype') - return mime_type \ No newline at end of file + return mime_type diff --git a/httpie/core.py b/httpie/core.py index d0c26dcbcc..dcf3484460 100644 --- a/httpie/core.py +++ b/httpie/core.py @@ -225,8 +225,24 @@ def request_body_read_callback(chunk: bytes): is_streamed_upload = not isinstance(message.body, (str, bytes)) do_write_body = not is_streamed_upload force_separator = is_streamed_upload and env.stdout_isatty + # In httpie/core.py, inside the 'program' function: + else: final_response = message + + # --- BUG FIX: Correctly handle Content-Length for encoded downloads --- + if downloader and 'Content-Encoding' in final_response.headers: + # When in download mode, we assume the user intends to download + # the raw, encoded content (e.g., a .gz file). We must delete + # the 'Content-Encoding' header from the response to prevent + # the 'requests' library from auto-decompressing the stream. + # This ensures the raw compressed data is streamed and the + # 'Content-Length' (which refers to the compressed size per spec) + # is correctly verified against the streamed bytes, resolving + # the "Incomplete download" error. + del final_response.headers['Content-Encoding'] + # --- END BUG FIX --- + if args.check_status or downloader: exit_status = http_status_to_exit_status(http_status=message.status_code, follow=args.follow) if exit_status != ExitStatus.SUCCESS and (not env.stdout_isatty or args.quiet == 1):