From 1f2aa328536480bd7b489871587e1621d805430d Mon Sep 17 00:00:00 2001 From: Harsh Thakkar Date: Thu, 24 Sep 2026 00:33:50 +0530 Subject: [PATCH 1/2] Treat event-stream media type case-insensitively Media types are case-insensitive per RFC 2045 section 5.1, but the auto-streaming check for Server-Sent Events compared the parsed Content-Type header literally, so a server responding with e.g. 'Text/Event-Stream' silently fell back to buffered output. --- httpie/output/writer.py | 2 +- tests/test_stream.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/httpie/output/writer.py b/httpie/output/writer.py index 4a2949bce2..136f0abac7 100644 --- a/httpie/output/writer.py +++ b/httpie/output/writer.py @@ -167,7 +167,7 @@ def get_stream_type_and_kwargs( raw_content_type_header = headers.get('Content-Type', None) if raw_content_type_header: content_type_header, _ = parse_content_type_header(raw_content_type_header) - is_stream = (content_type_header == 'text/event-stream') + is_stream = (content_type_header.lower() == 'text/event-stream') if not env.stdout_isatty and not prettify_groups: stream_class = RawStream diff --git a/tests/test_stream.py b/tests/test_stream.py index b0b9b8bde8..d9647fca96 100644 --- a/tests/test_stream.py +++ b/tests/test_stream.py @@ -128,6 +128,10 @@ def test_redirected_stream(httpbin): ['Accept:text/event-stream; charset=utf-8'], 3 ), + ( + ['Accept:Text/Event-Stream'], + 3 + ), ( ['Accept:text/plain'], 1 From 09520ff3ea275a5a5a552cbbb276bb104a2ffa37 Mon Sep 17 00:00:00 2001 From: Harsh Thakkar Date: Thu, 24 Sep 2026 00:43:35 +0530 Subject: [PATCH 2/2] Keep leading-zero keys intact in nested JSON items A root literal such as 01=x was tokenized as a NUMBER (int coerces '01' to 1) and emitted as the key '1', silently rewriting the key and colliding with a real 1=x item: $ http pie.dev/post '01=x' '1=y' {1: y} # '01' became '1' and overwrote '1'! Only canonical integers (whose str() form matches, i.e. no leading zeros) are now treated as numbers; JSON keys like '01' are preserved. --- httpie/cli/nested_json/parse.py | 20 ++++++++++++++++++-- tests/test_json.py | 10 ++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/httpie/cli/nested_json/parse.py b/httpie/cli/nested_json/parse.py index 323a22eef1..9792c9c0f9 100644 --- a/httpie/cli/nested_json/parse.py +++ b/httpie/cli/nested_json/parse.py @@ -137,8 +137,8 @@ def send_buffer() -> Iterator[Token]: value = ''.join(buffer) kind = TokenKind.TEXT if not backslashes: - for variation, kind in [ - (int, TokenKind.NUMBER), + for variation, variation_kind in [ + (canonical_int, TokenKind.NUMBER), (check_escaped_int, TokenKind.TEXT), ]: try: @@ -146,6 +146,7 @@ def send_buffer() -> Iterator[Token]: except ValueError: continue else: + kind = variation_kind break yield Token( kind=kind, @@ -189,5 +190,20 @@ def check_escaped_int(value: str) -> str: return value[1:] +def canonical_int(value: str) -> int: + """Convert to int only when the value has no leading zeros. + + A literal such as ``01`` is a valid JSON *key*, not a number - JSON + numbers can't have leading zeros. Coercing it to ``int`` silently + rewrites the key to ``1`` and can collide with a real ``1`` key. + """ + if not value: + raise ValueError('Not an int') + converted = int(value) + if str(converted) != value: + raise ValueError('Not a canonical int') + return converted + + def assert_cant_happen(): raise ValueError('Unexpected value') diff --git a/tests/test_json.py b/tests/test_json.py index e758ebe7f4..656e7e1c92 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -172,6 +172,16 @@ def test_complex_json_arguments_with_non_json(httpbin, request_type, value): ], {'bottle-on-wall': [1, 2, 3]}, ), + ( + # Leading-zero keys stay keys and don't collide with '1' + ['01=x', '1=y'], + {'01': 'x', '1': 'y'}, + ), + ( + # ...but canonical integers still become numeric indices in paths + ['kids[0]=Thelma', 'kids[10]=Ashley'], + {'kids': ['Thelma', None, None, None, None, None, None, None, None, None, 'Ashley']}, + ), ( [ 'pet[species]=Dahut',