From a936c47573c65e904bb788370b91f25038611fbf Mon Sep 17 00:00:00 2001 From: Varun Chawla Date: Sat, 7 Feb 2026 17:48:26 -0800 Subject: [PATCH 1/3] Fix confusing error message for invalid data parameter When AsyncClient receives invalid data like data=[{"a": "b"}] (list of dicts), it was raising "Attempted to send a sync request with an AsyncClient instance" which is misleading. The actual issue is invalid data format. This fix adds early validation in encode_request() to check if data is a list/tuple containing non-bytes objects, and raises a clear TypeError with helpful guidance (use json= or data={...} instead). Fixes #3471 --- httpx/_content.py | 15 +++++++++++++++ tests/test_content.py | 21 +++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/httpx/_content.py b/httpx/_content.py index 6f479a0885..ef0e9f736e 100644 --- a/httpx/_content.py +++ b/httpx/_content.py @@ -202,6 +202,21 @@ 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 + try: + for item in data: + if not isinstance(item, (bytes, bytearray, memoryview)): + raise TypeError( + f"Expected bytes-like object in 'data' sequence, got {type(item).__name__}. " + f"Use 'json=' for JSON data or 'data={{...}}' for form data." + ) + except TypeError: + # Re-raise our custom error, not the iteration error + raise + 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 = [ From 51cb6bd6d612a7330ef4c2a477579e854bc452b4 Mon Sep 17 00:00:00 2001 From: Varun Chawla Date: Sat, 7 Feb 2026 19:21:45 -0800 Subject: [PATCH 2/3] Simplify error handling in data validation Remove unnecessary try-except block that was catching TypeError during iteration. The validation logic doesn't need exception handling since we're only checking types of items in a list/tuple that we've already confirmed exists. --- httpx/_content.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/httpx/_content.py b/httpx/_content.py index ef0e9f736e..e3bdae94d4 100644 --- a/httpx/_content.py +++ b/httpx/_content.py @@ -206,16 +206,12 @@ def encode_request( # 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 - try: - for item in data: - if not isinstance(item, (bytes, bytearray, memoryview)): - raise TypeError( - f"Expected bytes-like object in 'data' sequence, got {type(item).__name__}. " - f"Use 'json=' for JSON data or 'data={{...}}' for form data." - ) - except TypeError: - # Re-raise our custom error, not the iteration error - raise + for item in data: + if not isinstance(item, (bytes, bytearray, memoryview)): + raise TypeError( + f"Expected bytes-like object in 'data' sequence, got {type(item).__name__}. " + f"Use 'json=' for JSON data or 'data={{...}}' for form data." + ) message = "Use 'content=<...>' to upload raw bytes/text content." warnings.warn(message, DeprecationWarning, stacklevel=2) From 5a2b1884d078e2714451e4fd0f4d2535b9da79eb Mon Sep 17 00:00:00 2001 From: Varun Chawla Date: Sat, 7 Feb 2026 19:40:10 -0800 Subject: [PATCH 3/3] Fix line length in error message --- httpx/_content.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/httpx/_content.py b/httpx/_content.py index e3bdae94d4..0c80ab9d90 100644 --- a/httpx/_content.py +++ b/httpx/_content.py @@ -208,9 +208,11 @@ def encode_request( # 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, got {type(item).__name__}. " - f"Use 'json=' for JSON data or 'data={{...}}' for form data." + 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."