fix: repair httpx2/params/transport upgrade fallout
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,11 +83,14 @@ 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
|
||||
await session.initialize()
|
||||
yield session
|
||||
return
|
||||
|
||||
raise ValueError(f"unsupported MCP transport {transport.kind!r}")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user