Repository: encode/httpx

Issue:
AsyncClient.request runtime error: "Attempted to send an sync request with an AsyncClient instance"
Ran into this issue as well. It was surprising to end up with a `RuntimeError: Attempted to send an sync request with an AsyncClient instance` error, when my actual issue had nothing to do with sync/async (in my case submitting the wrong type by accident through `data` kwarg). It feels like it might be a bug / that the type being submitted could be checked before trying to send the request?

### Discussed in https://github.com/encode/httpx/discussions/2339

<div type='discussions-op-text'>

<sup>Originally posted by **mdlincoln** August 16, 2022</sup>
I need to interact with an API that accepts `DELETE` requests requiring JSON content in the body. Since `AsyncClient`'s shortcut `delete()` method does not allow this, I followed the docs to instead use the `request()` method, but I received an unexpected error. Minimal example:

``` py
import asyncio
from httpx import AsyncClient

async_client = AsyncClient(
    base_url="http://localhost",
)

async def test_request_method():
    async with async_client as ac:
        response = await ac.request(method="DELETE", url="/", content={"foo": "bar"})

asyncio.run(test_request_method())
```

this raises a `RuntimeError: Attempted to send an sync request with an AsyncClient instance.` (full traceback in the `<details>` below.)

Am I missing the right way for calling the `request()` method with an `AsyncClient`?

<details>
<pre>
Traceback (most recent call last):
  File "*******/minimal.py", line 14, in <module>
    asyncio.run(test_request_method())
  File "/opt/homebrew/Cellar/python@3.9/3.9.13_3/Frameworks/Python.framework/Versions/3.9/lib/python3.9/asyncio/runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "/opt/homebrew/Cellar/python@3.9/3.9.13_3/Frameworks/Python.framework/Versions/3.9/lib/python3.9/asyncio/base_events.py", line 647, in run_until_complete
    return future.result()
  File "*********minimal.py", line 11, in test_request_method
    response = await ac.request(method="DELETE", url="/", content={"foo": "bar"})
  File "*********/lib/python3.9/site-packages/httpx/_client.py", line 1527, in request
    return await self.send(request, auth=auth, follow_redirects=follow_redirects)
  File "*********/lib/python3.9/site-packages/httpx/_client.py", line 1614, in send
    response = await self._send_handling_auth(
  File "*********/lib/python3.9/site-packages/httpx/_client.py", line 1642, in _send_handling_auth
    response = await self._send_handling_redirects(
  File "*********/lib/python3.9/site-packages/httpx/_client.py", line 1679, in _send_handling_redirects
    response = await self._send_single_request(request)
  File "*********/lib/python3.9/site-packages/httpx/_client.py", line 1711, in _send_single_request
    raise RuntimeError(
RuntimeError: Attempted to send an sync request with an AsyncClient instance.
</pre>
</details>
</div>

Discussion:

Patch:
diff --git a/httpx/_content.py b/httpx/_content.py
index 3cbca7a..1c450b7 100644
--- a/httpx/_content.py
+++ b/httpx/_content.py
@@ -114,7 +114,11 @@ def encode_content(
         headers = {"Content-Length": str(content_length)} if body else {}
         return headers, ByteStream(body)
 
-    elif isinstance(content, Iterable):
+    elif isinstance(content, Iterable) and not isinstance(content, dict):
+        # `not isinstance(content, dict)` is a bit oddly specific, but it
+        # catches a case that's easy for users to make in error, and would
+        # otherwise pass through here, like any other bytes-iterable,
+        # because `dict` happens to be iterable. See issue #2491.
         content_length_or_none = peek_filelike_length(content)
 
         if content_length_or_none is None:


Test patch:
diff --git a/tests/test_content.py b/tests/test_content.py
index d9b4ed8..6a1303c 100644
--- a/tests/test_content.py
+++ b/tests/test_content.py
@@ -360,6 +360,9 @@ def test_invalid_argument():
     with pytest.raises(TypeError):
         httpx.Request(method, url, content=123)  # type: ignore
 
+    with pytest.raises(TypeError):
+        httpx.Request(method, url, content={"a": "b"})  # type: ignore
+
 
 @pytest.mark.asyncio
 async def test_multipart_multiple_files_single_input_content():