Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions src/anthropic/_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,8 @@ def validate_async_middleware(middleware: Iterable[MiddlewareInput]) -> None:
)
elif not callable(entry):
raise TypeError(f"middleware {_middleware_name(entry)} is not callable")
elif not _is_async_callable(entry):
raise TypeError(
f"middleware {_middleware_name(entry)} is not an async function; "
"the asynchronous client requires async middleware functions"
)
# Function-style async middleware is typed as Callable[..., Awaitable],
# not specifically as a coroutine function. A synchronous wrapper may
# therefore validly return an awaitable; the async middleware chain
# awaits that result at invocation time. Do not reject that supported
# shape based only on inspect.iscoroutinefunction().
74 changes: 74 additions & 0 deletions tests/test_async_middleware_awaitable_callable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from __future__ import annotations

from typing import Any, Awaitable

import anyio
import httpx
import pytest

from anthropic import AsyncAnthropic
from anthropic._middleware import AsyncCallNext
from anthropic._request import APIRequest


def test_async_client_accepts_sync_wrapper_returning_awaitable() -> None:
seen: list[str] = []

def middleware(request: APIRequest, call_next: AsyncCallNext) -> Awaitable[Any]:
seen.append(request.url)
return call_next(request)

async def run() -> None:
async def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(200, json={"ok": True}, request=request)

client = AsyncAnthropic(
api_key="test",
base_url="https://example.test",
middleware=[middleware],
http_client=httpx.AsyncClient(transport=httpx.MockTransport(handler)),
)
try:
result = await client.get("/probe", cast_to=object)
finally:
await client.close()

assert result == {"ok": True}

anyio.run(run)
assert seen == ["/probe"]


def test_async_client_accepts_sync_callable_object_returning_awaitable() -> None:
class MiddlewareWrapper:
def __init__(self) -> None:
self.calls = 0

def __call__(self, request: APIRequest, call_next: AsyncCallNext) -> Awaitable[Any]:
self.calls += 1
return call_next(request)

wrapper = MiddlewareWrapper()

async def run() -> None:
async def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(200, json={"ok": True}, request=request)

client = AsyncAnthropic(
api_key="test",
base_url="https://example.test",
middleware=[wrapper],
http_client=httpx.AsyncClient(transport=httpx.MockTransport(handler)),
)
try:
assert await client.get("/probe", cast_to=object) == {"ok": True}
finally:
await client.close()

anyio.run(run)
assert wrapper.calls == 1


def test_async_client_still_rejects_non_callable_middleware() -> None:
with pytest.raises(TypeError, match="is not callable"):
AsyncAnthropic(api_key="test", middleware=[object()]) # type: ignore[list-item]