diff --git a/CHANGELOG.md b/CHANGELOG.md index 0497ac3508..cb164b7218 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 + +- Reject nested object or array access through a JSON `null` field with a type error instead of silently replacing the request body. Plain `null` values, direct value overrides, and filling sparse array entries remain supported. + ## [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)) diff --git a/httpie/cli/nested_json/interpret.py b/httpie/cli/nested_json/interpret.py index 71fad98acd..0bef6544dc 100644 --- a/httpie/cli/nested_json/interpret.py +++ b/httpie/cli/nested_json/interpret.py @@ -65,10 +65,11 @@ def object_for(kind: PathAction) -> Any: else: assert_cant_happen() + # Initialize the root only; nested null values must pass type checking. + if context is None: + context = cursor = object_for(paths[0].kind) + for index, (path, next_path) in enumerate(zip(paths, paths[1:])): - # If there is no context yet, set it. - if cursor is None: - context = cursor = object_for(path.kind) if path.kind is PathAction.KEY: type_check(index, path, dict) if next_path.kind is PathAction.SET: diff --git a/tests/test_json.py b/tests/test_json.py index e758ebe7f4..334f994909 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -578,6 +578,41 @@ def test_nested_json_errors(input_json, expected_error, httpbin): assert expected_lines == exc_lines +@pytest.mark.parametrize('field', ['foo', 'foo[bar]', '[0][foo]']) +@pytest.mark.parametrize( + 'access, kind, required_type', + [('[key]', 'key', 'object'), ('[0]', 'index', 'array'), ('[]', 'append', 'array')], +) +def test_nested_json_null_container(field, access, kind, required_type): + with pytest.raises(NestedJSONSyntaxError) as exc: + http('--offline', DUMMY_URL, f'{field}:=null', f'{field}{access}=value') + + assert str(exc.value).splitlines()[0] == ( + f"HTTPie Type Error: Cannot perform {kind!r} based access on {field!r} " + "which has a type of 'NoneType' but this operation " + f"requires a type of {required_type!r}." + ) + + +@pytest.mark.parametrize( + 'input_json, expected_json', + [ + (['keep=value', 'foo:=null'], {'keep': 'value', 'foo': None}), + ( + ['keep=value', 'foo:=null', 'foo=value'], + {'keep': 'value', 'foo': 'value'}, + ), + ( + ['keep=value', 'foo[1]=last', 'foo[0][bar]=first'], + {'keep': 'value', 'foo': [{'bar': 'first'}, 'last']}, + ), + ], +) +def test_nested_json_null_values(input_json, expected_json): + r = http('--offline', DUMMY_URL, *input_json) + assert r.json == expected_json + + def test_nested_json_sparse_array(httpbin_both): r = http(httpbin_both + '/post', 'test[0]:=1', 'test[100]:=1') assert len(r.json['json']['test']) == 101