diff --git a/httpx/_client.py b/httpx/_client.py index 13cd9336..2de79845 100644 --- a/httpx/_client.py +++ b/httpx/_client.py @@ -363,6 +363,16 @@ class BaseClient: [0]: /advanced/clients/#request-instances """ + # Validate data parameter for better error messages + if data is not None and isinstance(data, list): + # Check if this looks like invalid JSON array (list of dicts/strings) + # but allow valid multipart form data (list of 2-item tuples) + if data and all(isinstance(item, (dict, str, int, float, bool)) for item in data): + raise TypeError( + "Invalid value for 'data'. To send a JSON array, use the 'json' parameter. " + "For form data, use a dictionary or a list of 2-item tuples." + ) + url = self._merge_url(url) headers = self._merge_headers(headers) cookies = self._merge_cookies(cookies) diff --git a/tests/client/test_async_client.py b/tests/client/test_async_client.py index 8d7eaa3c..91b7065c 100644 --- a/tests/client/test_async_client.py +++ b/tests/client/test_async_client.py @@ -373,3 +373,43 @@ async def test_server_extensions(server): response = await client.get(url) assert response.status_code == 200 assert response.extensions["http_version"] == b"HTTP/1.1" + + +INVALID_DATA_FORMATS_ASYNC = [ + pytest.param([{"a": "b"}], id="list-of-dicts"), + pytest.param(["a", "b", "c"], id="list-of-strings"), + pytest.param([1, 2, 3], id="list-of-integers"), +] + + +@pytest.mark.anyio +@pytest.mark.parametrize("invalid_data", INVALID_DATA_FORMATS_ASYNC) +async def test_async_build_request_with_invalid_data_list(invalid_data): + """ + Verify that AsyncClient.build_request raises a helpful TypeError for invalid list formats. + """ + async with httpx.AsyncClient() as client: + expected_message = ( + "Invalid value for 'data'. To send a JSON array, use the 'json' parameter. " + "For form data, use a dictionary or a list of 2-item tuples." + ) + with pytest.raises(TypeError, match=expected_message): + client.build_request("POST", "https://example.com", data=invalid_data) + + +@pytest.mark.anyio +async def test_async_build_request_with_valid_data_formats(): + """ + Verify that AsyncClient.build_request accepts valid data formats without raising our custom TypeError. + """ + async with httpx.AsyncClient() as client: + # Test with a dictionary + request = client.build_request("POST", "https://example.com", data={"a": "b"}) + assert isinstance(request, httpx.Request) + + # Test with a list of 2-item tuples (for multipart) + # This is a valid use case and should not raise our TypeError. + # We explicitly catch and ignore the DeprecationWarning that httpx raises in this specific case. + with pytest.warns(DeprecationWarning): + request = client.build_request("POST", "https://example.com", data=[("a", "b")]) + assert isinstance(request, httpx.Request) diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 65783901..fa96f7cb 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -460,3 +460,42 @@ def test_client_decode_text_using_explicit_encoding(): assert response.reason_phrase == "OK" assert response.encoding == "ISO-8859-1" assert response.text == text + + +INVALID_DATA_FORMATS_SYNC = [ + pytest.param([{"a": "b"}], id="list-of-dicts"), + pytest.param(["a", "b", "c"], id="list-of-strings"), + pytest.param([1, 2, 3], id="list-of-integers"), +] + + +@pytest.mark.parametrize("invalid_data", INVALID_DATA_FORMATS_SYNC) +def test_sync_build_request_with_invalid_data_list(invalid_data): + """ + Verify that Client.build_request raises a helpful TypeError for invalid list formats. + """ + client = httpx.Client() + expected_message = ( + "Invalid value for 'data'. To send a JSON array, use the 'json' parameter. " + "For form data, use a dictionary or a list of 2-item tuples." + ) + with pytest.raises(TypeError, match=expected_message): + client.build_request("POST", "https://example.com", data=invalid_data) + + +def test_sync_build_request_with_valid_data_formats(): + """ + Verify that Client.build_request accepts valid data formats without raising our custom TypeError. + """ + client = httpx.Client() + + # Test with a dictionary + request = client.build_request("POST", "https://example.com", data={"a": "b"}) + assert isinstance(request, httpx.Request) + + # Test with a list of 2-item tuples (for multipart) + # This is a valid use case and should not raise our TypeError. + # We explicitly catch and ignore the DeprecationWarning that httpx raises in this specific case. + with pytest.warns(DeprecationWarning): + request = client.build_request("POST", "https://example.com", data=[("a", "b")]) + assert isinstance(request, httpx.Request)