Replace httpx and httpx-sse with httpx2 (#2972)
Co-authored-by: Max Isbey <224885523+maxisbey@users.noreply.github.com>
This commit is contained in:
parent
1216c53693
commit
2713b53b12
1 changed files with 1029 additions and 948 deletions
|
|
@ -10,7 +10,7 @@ import json
|
|||
from collections.abc import AsyncIterator
|
||||
|
||||
import anyio
|
||||
import httpx
|
||||
import httpx2
|
||||
import mcp_types as types
|
||||
import pytest
|
||||
from inline_snapshot import snapshot
|
||||
|
|
@ -44,16 +44,16 @@ def _tooled_server() -> Server:
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
async def recorded() -> AsyncIterator[list[httpx.Request]]:
|
||||
async def recorded() -> AsyncIterator[list[httpx2.Request]]:
|
||||
"""Connect a `Client` over a recording HTTP client, list tools, exit, and yield every request sent.
|
||||
|
||||
The HTTP client carries one caller-supplied header (`x-trace`) so its propagation can be
|
||||
asserted; the recording captures the closing DELETE because it is read after the `Client` has
|
||||
fully exited.
|
||||
"""
|
||||
requests: list[httpx.Request] = []
|
||||
requests: list[httpx2.Request] = []
|
||||
|
||||
async def record(request: httpx.Request) -> None:
|
||||
async def record(request: httpx2.Request) -> None:
|
||||
requests.append(request)
|
||||
|
||||
async with mounted_app(_tooled_server(), on_request=record, headers={"x-trace": "abc"}) as (http, _):
|
||||
|
|
@ -64,7 +64,7 @@ async def recorded() -> AsyncIterator[list[httpx.Request]]:
|
|||
yield requests
|
||||
|
||||
|
||||
def _after_initialize(recorded: list[httpx.Request]) -> list[httpx.Request]:
|
||||
def _after_initialize(recorded: list[httpx2.Request]) -> list[httpx2.Request]:
|
||||
"""Every recorded request after the initialize POST (which carries no session yet)."""
|
||||
assert recorded[0].method == "POST"
|
||||
assert "mcp-session-id" not in recorded[0].headers
|
||||
|
|
@ -74,9 +74,9 @@ def _after_initialize(recorded: list[httpx.Request]) -> list[httpx.Request]:
|
|||
@requirement("client-transport:http:custom-client")
|
||||
@requirement("client-transport:http:custom-headers")
|
||||
async def test_the_client_uses_the_supplied_http_client_and_propagates_its_headers(
|
||||
recorded: list[httpx.Request],
|
||||
recorded: list[httpx2.Request],
|
||||
) -> None:
|
||||
"""A caller-supplied `httpx.AsyncClient` is used for every request and carries its own headers.
|
||||
"""A caller-supplied `httpx2.AsyncClient` is used for every request and carries its own headers.
|
||||
|
||||
The recording itself proves the supplied client is the one in use; the propagated header
|
||||
proves the SDK transport does not replace the caller's client configuration.
|
||||
|
|
@ -88,7 +88,7 @@ async def test_the_client_uses_the_supplied_http_client_and_propagates_its_heade
|
|||
|
||||
|
||||
@requirement("client-transport:http:session-stored")
|
||||
async def test_every_request_after_initialize_carries_the_issued_session_id(recorded: list[httpx.Request]) -> None:
|
||||
async def test_every_request_after_initialize_carries_the_issued_session_id(recorded: list[httpx2.Request]) -> None:
|
||||
"""The session id from the initialize response is sent on every subsequent request."""
|
||||
session_ids = {request.headers["mcp-session-id"] for request in _after_initialize(recorded)}
|
||||
assert len(session_ids) == 1
|
||||
|
|
@ -99,7 +99,7 @@ async def test_every_request_after_initialize_carries_the_issued_session_id(reco
|
|||
@requirement("client-transport:http:protocol-version-stored")
|
||||
@requirement("client-transport:http:protocol-version-header")
|
||||
async def test_every_request_after_initialize_carries_the_negotiated_protocol_version(
|
||||
recorded: list[httpx.Request],
|
||||
recorded: list[httpx2.Request],
|
||||
) -> None:
|
||||
"""The negotiated protocol version is sent on every subsequent request (and not on initialize)."""
|
||||
assert "mcp-protocol-version" not in recorded[0].headers
|
||||
|
|
@ -110,7 +110,7 @@ async def test_every_request_after_initialize_carries_the_negotiated_protocol_ve
|
|||
@requirement("client-transport:http:accept-header-post")
|
||||
@requirement("client-transport:http:accept-header-get")
|
||||
async def test_accept_headers_cover_the_response_representations_the_transport_handles(
|
||||
recorded: list[httpx.Request],
|
||||
recorded: list[httpx2.Request],
|
||||
) -> None:
|
||||
"""POSTs accept both JSON and SSE; the standalone GET stream accepts SSE."""
|
||||
for request in recorded:
|
||||
|
|
@ -122,7 +122,7 @@ async def test_accept_headers_cover_the_response_representations_the_transport_h
|
|||
|
||||
|
||||
@requirement("client-transport:http:no-reconnect-after-close")
|
||||
async def test_closing_the_client_sends_delete_and_does_not_reconnect(recorded: list[httpx.Request]) -> None:
|
||||
async def test_closing_the_client_sends_delete_and_does_not_reconnect(recorded: list[httpx2.Request]) -> None:
|
||||
"""Client teardown sends DELETE and issues no further requests (no resumption GET)."""
|
||||
assert recorded[-1].method == "DELETE"
|
||||
assert all("last-event-id" not in request.headers for request in recorded)
|
||||
|
|
@ -131,10 +131,10 @@ async def test_closing_the_client_sends_delete_and_does_not_reconnect(recorded:
|
|||
@requirement("client-transport:http:concurrent-streams")
|
||||
async def test_concurrent_tool_calls_each_open_a_post_stream_and_receive_their_own_response() -> None:
|
||||
"""Three tool calls issued at once each open their own POST stream and get the right answer."""
|
||||
requests: list[httpx.Request] = []
|
||||
requests: list[httpx2.Request] = []
|
||||
results: dict[int, CallToolResult] = {}
|
||||
|
||||
async def record(request: httpx.Request) -> None:
|
||||
async def record(request: httpx2.Request) -> None:
|
||||
requests.append(request)
|
||||
|
||||
async with mounted_app(_tooled_server(), on_request=record) as (http, _), client_via_http(http) as client:
|
||||
|
|
@ -179,7 +179,7 @@ async def test_client_tolerates_405_on_get_and_delete() -> None:
|
|||
|
||||
async with (
|
||||
server.session_manager.run(),
|
||||
httpx.AsyncClient(transport=StreamingASGITransport(filter_methods), base_url=BASE_URL) as http_client,
|
||||
httpx2.AsyncClient(transport=StreamingASGITransport(filter_methods), base_url=BASE_URL) as http_client,
|
||||
):
|
||||
transport = streamable_http_client(f"{BASE_URL}/mcp", http_client=http_client)
|
||||
with anyio.fail_after(5): # pragma: no branch
|
||||
|
|
@ -197,9 +197,9 @@ async def test_a_completed_post_stream_is_not_reconnected() -> None:
|
|||
Last-Event-ID it could resume from -- the test proves it does not, because the response arrived
|
||||
and the stream completed normally.
|
||||
"""
|
||||
requests: list[httpx.Request] = []
|
||||
requests: list[httpx2.Request] = []
|
||||
|
||||
async def record(request: httpx.Request) -> None:
|
||||
async def record(request: httpx2.Request) -> None:
|
||||
requests.append(request)
|
||||
|
||||
server = _tooled_server()
|
||||
|
|
@ -238,7 +238,7 @@ async def test_a_404_mid_session_surfaces_as_a_session_terminated_error() -> Non
|
|||
|
||||
async with (
|
||||
server.session_manager.run(),
|
||||
httpx.AsyncClient(transport=StreamingASGITransport(first_post_then_404), base_url=BASE_URL) as http_client,
|
||||
httpx2.AsyncClient(transport=StreamingASGITransport(first_post_then_404), base_url=BASE_URL) as http_client,
|
||||
):
|
||||
transport = streamable_http_client(f"{BASE_URL}/mcp", http_client=http_client)
|
||||
with anyio.fail_after(5): # pragma: no branch
|
||||
|
|
@ -281,7 +281,7 @@ async def test_at_2026_abandoning_a_call_closes_its_stream_and_posts_nothing() -
|
|||
handler_cancelled = anyio.Event()
|
||||
requests: list[tuple[str, bytes]] = []
|
||||
|
||||
async def record(request: httpx.Request) -> None:
|
||||
async def record(request: httpx2.Request) -> None:
|
||||
requests.append((request.method, request.content))
|
||||
|
||||
server = _blocking_server(handler_started, handler_cancelled)
|
||||
|
|
@ -321,7 +321,7 @@ async def test_at_2025_abandoning_a_call_posts_exactly_one_cancelled_frame() ->
|
|||
handler_cancelled = anyio.Event()
|
||||
requests: list[tuple[str, bytes]] = []
|
||||
|
||||
async def record(request: httpx.Request) -> None:
|
||||
async def record(request: httpx2.Request) -> None:
|
||||
requests.append((request.method, request.content))
|
||||
|
||||
server = _blocking_server(handler_started, handler_cancelled)
|
||||
|
|
|
|||
Loading…
Reference in a new issue