encode/httpx · error · TypeError

Cannot use an async handler in a sync Client

Error message

Cannot use an async handler in a sync Client

What it means

Raised by MockTransport.handle_request (the synchronous path) when the handler callable returns a coroutine instead of a Response. MockTransport accepts either a sync or async handler, but the sync handle_request cannot await a coroutine, so it detects the non-Response return and raises TypeError. It indicates a mismatch between the Client type (sync) and the handler (async).

Source

Thrown at httpx/_transports/mock.py:26

SyncHandler = typing.Callable[[Request], Response]
AsyncHandler = typing.Callable[[Request], typing.Coroutine[None, None, Response]]


__all__ = ["MockTransport"]


class MockTransport(AsyncBaseTransport, BaseTransport):
    def __init__(self, handler: SyncHandler | AsyncHandler) -> None:
        self.handler = handler

    def handle_request(
        self,
        request: Request,
    ) -> Response:
        request.read()
        response = self.handler(request)
        if not isinstance(response, Response):  # pragma: no cover
            raise TypeError("Cannot use an async handler in a sync Client")
        return response

    async def handle_async_request(
        self,
        request: Request,
    ) -> Response:
        await request.aread()
        response = self.handler(request)

        # Allow handler to *optionally* be an `async` function.
        # If it is, then the `response` variable need to be awaited to actually
        # return the result.

        if not isinstance(response, Response):
            response = await response

        return response

View on GitHub (pinned to b5addb64f0)

Solutions

  1. Use httpx.AsyncClient with the MockTransport when your handler is 'async def'.
  2. Or rewrite the handler as a plain 'def' returning httpx.Response when using a sync Client.
  3. Keep one MockTransport per Client flavour; do not share between sync and async tests.
  4. Add a unit-level check: if asyncio.iscoroutinefunction(handler) and sync client -> fail fast in test setup.

Example fix

// before
def handler(request):  # actually defined as 'async def' elsewhere
    return httpx.Response(200)
transport = httpx.MockTransport(handler)
client = httpx.Client(transport=transport)  # TypeError at request time

// after
async def handler(request):
    return httpx.Response(200)
transport = httpx.MockTransport(handler)
client = httpx.AsyncClient(transport=transport)
Defensive patterns

Strategy: type-guard

Validate before calling

import asyncio, typing
import httpx

def make_mock_transport(handler):
    is_async = asyncio.iscoroutinefunction(handler)
    transport = httpx.MockTransport(handler)
    return transport, is_async

transport, is_async = make_mock_transport(my_handler)
client = (
    httpx.AsyncClient(transport=transport) if is_async
    else httpx.Client(transport=transport)
)

Type guard

import asyncio, typing

def is_async_handler(handler: typing.Callable) -> bool:
    return asyncio.iscoroutinefunction(handler)

Try / catch

try:
    response = client.get("https://test.local/")
except TypeError as e:
    if "async handler" in str(e):
        raise TypeError("Use httpx.AsyncClient for async MockTransport handlers") from e
    raise

Prevention

When it happens

Trigger: Passing an 'async def handler(request)' to httpx.MockTransport and then using it from a sync httpx.Client (client.get(...)). The sync code path calls handle_request, which calls the handler synchronously and gets back a coroutine object.

Common situations: Writing tests that share one MockTransport between sync and async code; refactoring a sync test to async but forgetting to switch Client to AsyncClient; copy-pasting an async handler example into a sync test suite.

Related errors


AI-assisted analysis of encode/httpx@b5addb64f0 (2026-08-04). Data as JSON: /data/errors/a0ca0899d18106b6.json. Report an issue: GitHub.