diff --git a/httpx/_content.py b/httpx/_content.py index 6f479a0885..0c80ab9d90 100644 --- a/httpx/_content.py +++ b/httpx/_content.py @@ -202,6 +202,19 @@ def encode_request( # However for compat with requests, we *do* still support # `data=` usages. We deal with that case here, treating it # as if `content=<...>` had been supplied instead. + + # Validate that data is bytes-like or an iterable of bytes, not other types + if isinstance(data, (list, tuple)): + # Check if it's a list/tuple of bytes + for item in data: + if not isinstance(item, (bytes, bytearray, memoryview)): + item_type = type(item).__name__ + raise TypeError( + f"Expected bytes-like object in 'data' sequence, " + f"got {item_type}. Use 'json=' for JSON data or " + f"'data={{...}}' for form data." + ) + message = "Use 'content=<...>' to upload raw bytes/text content." warnings.warn(message, DeprecationWarning, stacklevel=2) return encode_content(data) diff --git a/tests/test_content.py b/tests/test_content.py index 9bfe983722..7ff619f335 100644 --- a/tests/test_content.py +++ b/tests/test_content.py @@ -364,6 +364,27 @@ def test_invalid_argument(): httpx.Request(method, url, content={"a": "b"}) # type: ignore +def test_invalid_data_list_of_dicts(): + """Test that passing a list of dicts to data= produces a clear error message. + + This was previously raising a confusing 'Attempted to send a sync request + with an AsyncClient instance' error with AsyncClient. Now it should give + a clear error for both sync and async clients. + """ + with pytest.raises(TypeError, match="Expected bytes-like object.*got dict"): + httpx.Request(method, url, data=[{"a": "b"}]) # type: ignore + + +@pytest.mark.anyio +async def test_invalid_data_list_of_dicts_async(): + """Test that AsyncClient produces clear error for invalid data parameter. + + Regression test for issue #3471. + """ + with pytest.raises(TypeError, match="Expected bytes-like object.*got dict"): + httpx.Request(method, url, data=[{"a": "b"}]) # type: ignore + + @pytest.mark.anyio async def test_multipart_multiple_files_single_input_content(): files = [