Skip to content

AsyncClient.request runtime error: "Attempted to send an sync request with an AsyncClient instance" #2491

Closed

Description

@zvxr

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 #2339

Originally posted by mdlincoln August 16, 2022
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:

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
Traceback (most recent call last):
  File "*******/minimal.py", line 14, in 
    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.

Activity

  1. lovelydinosaur commented on Dec 6, 2022

    @lovelydinosaur
    Member

    I need to interact with an API that accepts DELETE requests requiring JSON content in the body.

    That's a really confusing error message that you're seeing, which I need to dig into.

    But the short answer to your question is...

    "I need to interact with an API that accepts DELETE requests requiring JSON content in the body."

    Okay, so use json=... to send JSON data...

    await ac.request(method="DELETE", url="/", json={"foo": "bar"})
  2. lovelydinosaur commented on Dec 6, 2022

    @lovelydinosaur
    Member

    To improve our UX here we ought to be raising a TypeError if a dictionary is passed in the content=... argument.
    Because it won't do what you're expecting it to do.

    In the async case you'll end up with this really confusing error message.

    In the sync case, you'll end up with...

    # Not ideal. We've treated the argument as valid, and attempted to send a request with content "foo".
    >>> httpx.post("https://www.example.com", content={"foo": "bar"})
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "/Users/tomchristie/GitHub/encode/httpx/httpx/_api.py", line 304, in post
        return request(
               ^^^^^^^^
      File "/Users/tomchristie/GitHub/encode/httpx/httpx/_api.py", line 100, in request
        return client.request(
               ^^^^^^^^^^^^^^^
      File "/Users/tomchristie/GitHub/encode/httpx/httpx/_client.py", line 821, in request
        return self.send(request, auth=auth, follow_redirects=follow_redirects)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "/Users/tomchristie/GitHub/encode/httpx/httpx/_client.py", line 908, in send
        response = self._send_handling_auth(
                   ^^^^^^^^^^^^^^^^^^^^^^^^^
      File "/Users/tomchristie/GitHub/encode/httpx/httpx/_client.py", line 936, in _send_handling_auth
        response = self._send_handling_redirects(
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "/Users/tomchristie/GitHub/encode/httpx/httpx/_client.py", line 973, in _send_handling_redirects
        response = self._send_single_request(request)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "/Users/tomchristie/GitHub/encode/httpx/httpx/_client.py", line 1009, in _send_single_request
        response = transport.handle_request(request)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "/Users/tomchristie/GitHub/encode/httpx/httpx/_transports/default.py", line 218, in handle_request
        resp = self._pool.handle_request(req)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "/Users/tomchristie/GitHub/encode/httpx/venv/lib/python3.11/site-packages/httpcore/_sync/connection_pool.py", line 253, in handle_request
        raise exc
      File "/Users/tomchristie/GitHub/encode/httpx/venv/lib/python3.11/site-packages/httpcore/_sync/connection_pool.py", line 237, in handle_request
        response = connection.handle_request(request)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "/Users/tomchristie/GitHub/encode/httpx/venv/lib/python3.11/site-packages/httpcore/_sync/connection.py", line 90, in handle_request
        return self._connection.handle_request(request)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "/Users/tomchristie/GitHub/encode/httpx/venv/lib/python3.11/site-packages/httpcore/_sync/http11.py", line 112, in handle_request
        raise exc
      File "/Users/tomchristie/GitHub/encode/httpx/venv/lib/python3.11/site-packages/httpcore/_sync/http11.py", line 82, in handle_request
        self._send_request_body(**kwargs)
      File "/Users/tomchristie/GitHub/encode/httpx/venv/lib/python3.11/site-packages/httpcore/_sync/http11.py", line 135, in _send_request_body
        self._send_event(event, timeout=timeout)
      File "/Users/tomchristie/GitHub/encode/httpx/venv/lib/python3.11/site-packages/httpcore/_sync/http11.py", line 142, in _send_event
        bytes_to_send = self._h11_state.send(event)
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "/Users/tomchristie/GitHub/encode/httpx/venv/lib/python3.11/site-packages/h11/_connection.py", line 516, in send
        return b"".join(data_list)
               ^^^^^^^^^^^^^^^^^^^
    TypeError: sequence item 1: expected a bytes-like object, str found

    This is because content=... should be a str, bytes, or bytes iterator. Because dict instances happen to be iterators, the internals assume a bytes-iterator has been passed, and treat it as a valid argument.

    Really we'd like the same behaviour that we currently have if you pass some other incorrect type to content=..., eg:

    # Look, this error message makes sense...
    >>> httpx.post("https://www.example.com", content=123)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "/Users/tomchristie/GitHub/encode/httpx/httpx/_api.py", line 304, in post
        return request(
               ^^^^^^^^
      File "/Users/tomchristie/GitHub/encode/httpx/httpx/_api.py", line 100, in request
        return client.request(
               ^^^^^^^^^^^^^^^
      File "/Users/tomchristie/GitHub/encode/httpx/httpx/_client.py", line 808, in request
        request = self.build_request(
                  ^^^^^^^^^^^^^^^^^^^
      File "/Users/tomchristie/GitHub/encode/httpx/httpx/_client.py", line 360, in build_request
        return Request(
               ^^^^^^^^
      File "/Users/tomchristie/GitHub/encode/httpx/httpx/_models.py", line 339, in __init__
        headers, stream = encode_request(
                          ^^^^^^^^^^^^^^^
      File "/Users/tomchristie/GitHub/encode/httpx/httpx/_content.py", line 205, in encode_request
        return encode_content(content)
               ^^^^^^^^^^^^^^^^^^^^^^^
      File "/Users/tomchristie/GitHub/encode/httpx/httpx/_content.py", line 130, in encode_content
        raise TypeError(f"Unexpected type for 'content', {type(content)!r}")
    TypeError: Unexpected type for 'content', <class 'int'>

    It would be good if we switch the behaviour for the dict case to match this...

    # This would be a neater behaviour...
    >>> httpx.post("https://www.example.com", content={"a": "b"})
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "/Users/tomchristie/GitHub/encode/httpx/httpx/_api.py", line 304, in post
        return request(
               ^^^^^^^^
      File "/Users/tomchristie/GitHub/encode/httpx/httpx/_api.py", line 100, in request
        return client.request(
               ^^^^^^^^^^^^^^^
      File "/Users/tomchristie/GitHub/encode/httpx/httpx/_client.py", line 808, in request
        request = self.build_request(
                  ^^^^^^^^^^^^^^^^^^^
      File "/Users/tomchristie/GitHub/encode/httpx/httpx/_client.py", line 360, in build_request
        return Request(
               ^^^^^^^^
      File "/Users/tomchristie/GitHub/encode/httpx/httpx/_models.py", line 339, in __init__
        headers, stream = encode_request(
                          ^^^^^^^^^^^^^^^
      File "/Users/tomchristie/GitHub/encode/httpx/httpx/_content.py", line 205, in encode_request
        return encode_content(content)
               ^^^^^^^^^^^^^^^^^^^^^^^
      File "/Users/tomchristie/GitHub/encode/httpx/httpx/_content.py", line 130, in encode_content
        raise TypeError(f"Unexpected type for 'content', {type(content)!r}")
    TypeError: Unexpected type for 'content', <class 'dict'>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    user-experienceEnsuring that users have a good experience using the library

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions