fix: repair httpx2/params/transport upgrade fallout

This commit is contained in:
lda
2026-09-06 11:34:01 +07:00 Verified
parent b628a5aa11
commit 28017648f6
6 changed files with 52 additions and 50 deletions
+2 -2
View File
@@ -7,7 +7,7 @@ from dataclasses import dataclass
from json import JSONDecodeError
from typing import Any, Literal, TypeVar
import httpx
import httpx2
from wf_api.models import (
CapabilityCallResult,
@@ -99,7 +99,7 @@ class PublicErrorWorkflowClientPort:
if known is not None:
raise known from exc
raise ProtocolError(exc.code, exc.message, exc.data) from exc
except (httpx.TransportError, httpx.HTTPStatusError, JSONDecodeError) as exc:
except (httpx2.HTTPError, JSONDecodeError) as exc:
raise TransportError(f"{operation} transport failed: {exc}") from exc
except RuntimeError as exc:
# The RPC transport uses RuntimeError only when a decoded JSON-RPC
+7 -7
View File
@@ -4,7 +4,7 @@ from dataclasses import dataclass
from json import JSONDecodeError
from typing import Any
import httpx
import httpx2
from openapi_core import OpenAPI
from pydantic import BaseModel, ConfigDict
@@ -44,7 +44,7 @@ async def call_openapi_operation(
config: OpenApiExecutionConfig,
payload: dict[str, Any],
*,
client: httpx.AsyncClient | None = None,
client: httpx2.AsyncClient | None = None,
) -> NodeReturn[OpenApiOperationOutput]:
"""Execute one raw OpenAPI operation through generic HTTP machinery."""
request = build_http_request_parts(
@@ -65,11 +65,11 @@ async def call_openapi_operation(
)
close_client = client is None
active_client = client or httpx.AsyncClient(timeout=config.timeout_seconds)
active_client = client or httpx2.AsyncClient(timeout=config.timeout_seconds)
try:
try:
response = await _send_request(active_client, request)
except httpx.HTTPError as exc:
except httpx2.HTTPError as exc:
return NodeReturn(
outcome="transport_error",
output=OpenApiOperationOutput(
@@ -119,9 +119,9 @@ async def call_openapi_operation(
async def _send_request(
client: httpx.AsyncClient,
client: httpx2.AsyncClient,
request: HttpRequestParts,
) -> httpx.Response:
) -> httpx2.Response:
kwargs: dict[str, Any] = {
"method": request.method,
"url": request.url,
@@ -137,7 +137,7 @@ async def _send_request(
return await client.request(**kwargs)
def _response_body(response: httpx.Response) -> tuple[Any, list[str]]:
def _response_body(response: httpx2.Response) -> tuple[Any, list[str]]:
"""Parse response body while keeping malformed JSON in validation flow."""
content_type = response.headers.get("content-type", "").lower()
if not response.content:
+11 -3
View File
@@ -10,7 +10,7 @@ from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Any, Protocol
import httpx
import httpx2
from wf_api.auth import (
AuthRecord as NeutralAuthRecord,
@@ -180,7 +180,7 @@ class HttpxOAuthTokenRefresher:
data["client_secret"] = auth.client_secret
if auth.scopes:
data["scope"] = " ".join(auth.scopes)
async with httpx.AsyncClient(timeout=10.0) as client:
async with httpx2.AsyncClient(timeout=10.0) as client:
response = await client.post(str(auth.token_url), data=data)
response.raise_for_status()
payload = response.json()
@@ -198,8 +198,16 @@ class HttpxOAuthTokenRefresher:
@dataclass(frozen=True, slots=True)
class BoundMcpHttpAuth:
"""Auth material for MCP HTTP transports.
The MCP transport stack is httpx2-only: ``auth`` must be an httpx2.Auth
(or None). The authlib OAuth login flow in wf_cli stays on httpx by
design and never flows into this field; token refresh here is a plain
httpx2 POST.
"""
headers: dict[str, str] = field(default_factory=dict)
auth: httpx.Auth | None = None
auth: httpx2.Auth | None = None
@dataclass(frozen=True, slots=True)
+9 -5
View File
@@ -5,7 +5,7 @@ from __future__ import annotations
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
import httpx
import httpx2
from mcp.client.session import ClientSession
from mcp.client.stdio import StdioServerParameters, stdio_client
from mcp.client.streamable_http import streamable_http_client
@@ -42,11 +42,12 @@ async def open_mcp_session(
duplicate keys) and passes command, args, env, and cwd to
StdioServerParameters.
For HTTP transports, creates an httpx.AsyncClient with auth headers and
For HTTP transports, creates an httpx2.AsyncClient with auth headers and
enters streamable_http_client.
Yields an initialized ClientSession. Caller owns the session lifetime.
"""
transport = connection.transport
if transport is None:
raise ValueError(f"connection {connection.id!r} requires metadata.transport")
@@ -73,7 +74,7 @@ async def open_mcp_session(
if isinstance(transport, HttpSourceTransport):
bound = await binder.bind_http_auth(stored_auth)
http_client = httpx.AsyncClient(
http_client = httpx2.AsyncClient(
headers=bound.headers or None,
auth=bound.auth,
)
@@ -82,9 +83,12 @@ async def open_mcp_session(
streamable_http_client(
str(transport.url),
http_client=http_client,
) as (read_stream, write_stream, _get_session_id),
) as (
read_stream,
write_stream,
),
ClientSession(read_stream, write_stream) as session,
):
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()
yield session
return
+6 -16
View File
@@ -2,25 +2,15 @@ from __future__ import annotations
from typing import Any
from fastapi.datastructures import _Unset
from fastapi_jsonrpc import Params
class _RpcParams(Params):
def __init__(self, default: Any = ..., **extra: Any) -> None:
super().__init__(default, example=_Unset, **extra)
def RpcParams(default: Any = ...) -> Any:
"""Bind JSON-RPC method params without fastapi-jsonrpc's warning-prone wrapper.
"""Bind JSON-RPC method params with a zero-arg default.
``fastapi_jsonrpc.Params`` currently forwards ``example=Undefined`` into
FastAPI's ``Body``. FastAPI treats that as the deprecated ``example``
argument being explicitly provided, so every method registration emits a
deprecation warning. Keep the upstream subclass so fastapi-jsonrpc still
recognises method params, but pass FastAPI's real "unset" sentinel.
This issue is fixed upstream at https://github.com/smagafurov/fastapi-jsonrpc/pull/101. Remove this wrapper once the next version of fastapi-jsonrpc is released and we upgrade to it.
Upstream fixed the ``example``-sentinel warning in fastapi-jsonrpc 4.0, so
this is now a plain pass-through. The wrapper stays (in this one file) so
the ``params: Model = RpcParams()`` call sites keep working: upstream
``Params`` still requires ``default`` positionally.
"""
# TODO
return _RpcParams(default)
return Params(default)
+15 -15
View File
@@ -31,8 +31,8 @@ async def _fake_stdio_client(params: Any) -> AsyncIterator[tuple[Any, Any]]:
@asynccontextmanager
async def _fake_streamable_http_client(
url: str, *, http_client: Any = None
) -> AsyncIterator[tuple[Any, Any, Any]]:
yield "read", "write", lambda: None
) -> AsyncIterator[tuple[Any, Any]]:
yield "read", "write"
@asynccontextmanager
@@ -169,10 +169,10 @@ async def test_http_auth_headers_passed_to_client(
payload={"token": "secret123"},
)
import httpx as _httpx
import httpx2 as _httpx2
captured_clients: list[_httpx.AsyncClient] = []
_original_client = _httpx.AsyncClient
captured_clients: list[_httpx2.AsyncClient] = []
_original_client = _httpx2.AsyncClient
class _CapturingClient(_original_client): # type: ignore[type-arg]
def __init__(self, **kwargs: Any) -> None:
@@ -181,10 +181,10 @@ async def test_http_auth_headers_passed_to_client(
import wf_sources_mcp.client.transport as mod
class _PatchedHttpx:
class _Patchedhttpx2:
AsyncClient = _CapturingClient
monkeypatch.setattr(mod, "httpx", _PatchedHttpx())
monkeypatch.setattr(mod, "httpx2", _Patchedhttpx2())
async with open_mcp_session(connection, auth) as session:
assert isinstance(session, _FakeSession)
@@ -236,10 +236,10 @@ async def test_http_no_auth_creates_client_with_no_auth_headers(
) -> None:
connection = _http_connection()
import httpx as _httpx
import httpx2 as _httpx2
captured_clients: list[_httpx.AsyncClient] = []
_original_client = _httpx.AsyncClient
captured_clients: list[_httpx2.AsyncClient] = []
_original_client = _httpx2.AsyncClient
class _CapturingClient(_original_client): # type: ignore[type-arg]
def __init__(self, **kwargs: Any) -> None:
@@ -250,8 +250,8 @@ async def test_http_no_auth_creates_client_with_no_auth_headers(
monkeypatch.setattr(
mod,
"httpx",
type("_PatchedHttpx", (), {"AsyncClient": _CapturingClient})(),
"httpx2",
type("_Patchedhttpx2", (), {"AsyncClient": _CapturingClient})(),
)
async with open_mcp_session(connection, None):
@@ -282,7 +282,7 @@ async def test_open_mcp_session_uses_binder_for_http_headers(
import wf_sources_mcp.client.transport as mod
monkeypatch.setattr(mod.httpx, "AsyncClient", _CapturingClient)
monkeypatch.setattr(mod.httpx2, "AsyncClient", _CapturingClient)
connection = _http_connection()
auth = StoredAuthRecord(
@@ -332,8 +332,8 @@ async def test_open_mcp_session_refreshes_oauth_record_for_http(
import wf_sources_mcp.auth as auth_mod
import wf_sources_mcp.client.transport as transport_mod
monkeypatch.setattr(auth_mod.httpx, "AsyncClient", _CapturingClient)
monkeypatch.setattr(transport_mod.httpx, "AsyncClient", _CapturingClient)
monkeypatch.setattr(auth_mod.httpx2, "AsyncClient", _CapturingClient)
monkeypatch.setattr(transport_mod.httpx2, "AsyncClient", _CapturingClient)
connection = _http_connection()
auth = StoredAuthRecord(