Replace httpx and httpx-sse with httpx2 (#2972)
Some checks failed
CI / checks (push) Has been cancelled
CI / all-green (push) Has been cancelled

Co-authored-by: Max Isbey <224885523+maxisbey@users.noreply.github.com>
This commit is contained in:
Marcelo Trylesinski 2026-07-14 18:05:08 +02:00 • committed by GitHub
commit 2713b53b12
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 1029 additions and 948 deletions

View file

@ -7,9 +7,9 @@ import unittest.mock
from typing import Any
from urllib.parse import parse_qs, urlparse
import httpx
import httpx2
import pytest
from httpx import ASGITransport
from httpx2 import ASGITransport
from pydantic import AnyHttpUrl
from starlette.applications import Starlette
@ -47,7 +47,7 @@ def app(oauth_provider: MockOAuthProvider):
def client(app: Starlette):
transport = ASGITransport(app=app)
# Use base_url without a path since routes are directly on the app
return httpx.AsyncClient(transport=transport, base_url="http://localhost")
return httpx2.AsyncClient(transport=transport, base_url="http://localhost")
@pytest.fixture
@ -65,7 +65,7 @@ def pkce_challenge():
@pytest.fixture
async def registered_client(client: httpx.AsyncClient) -> dict[str, Any]:
async def registered_client(client: httpx2.AsyncClient) -> dict[str, Any]:
"""Create and register a test client."""
# Default client metadata
client_metadata = {
@ -84,7 +84,7 @@ async def registered_client(client: httpx.AsyncClient) -> dict[str, Any]:
@pytest.mark.anyio
async def test_registration_error_handling(client: httpx.AsyncClient, oauth_provider: MockOAuthProvider):
async def test_registration_error_handling(client: httpx2.AsyncClient, oauth_provider: MockOAuthProvider):
# Mock the register_client method to raise a registration error
with unittest.mock.patch.object(
oauth_provider,
@ -118,7 +118,7 @@ async def test_registration_error_handling(client: httpx.AsyncClient, oauth_prov
@pytest.mark.anyio
async def test_authorize_error_handling(
client: httpx.AsyncClient,
client: httpx2.AsyncClient,
oauth_provider: MockOAuthProvider,
registered_client: dict[str, Any],
pkce_challenge: dict[str, str],
@ -159,7 +159,7 @@ async def test_authorize_error_handling(
@pytest.mark.anyio
async def test_token_error_handling_auth_code(
client: httpx.AsyncClient,
client: httpx2.AsyncClient,
oauth_provider: MockOAuthProvider,
registered_client: dict[str, Any],
pkce_challenge: dict[str, str],
@ -218,7 +218,7 @@ async def test_token_error_handling_auth_code(
@pytest.mark.anyio
async def test_token_error_handling_refresh_token(
client: httpx.AsyncClient,
client: httpx2.AsyncClient,
oauth_provider: MockOAuthProvider,
registered_client: dict[str, Any],
pkce_challenge: dict[str, str],

View file

@ -3,9 +3,9 @@
import secrets
import time
import httpx
import httpx2
import pytest
from httpx import ASGITransport
from httpx2 import ASGITransport
from pydantic import AnyHttpUrl
from starlette.applications import Starlette
@ -121,7 +121,7 @@ def app(provider: IdentityAssertionProvider) -> Starlette:
@pytest.fixture
async def client(app: Starlette):
transport = ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="https://auth.example.com") as http:
async with httpx2.AsyncClient(transport=transport, base_url="https://auth.example.com") as http:
yield http
@ -160,7 +160,7 @@ def test_build_metadata_advertises_id_jag_profile_when_enabled():
@pytest.mark.anyio
async def test_metadata_endpoint_lists_id_jag_profile(client: httpx.AsyncClient):
async def test_metadata_endpoint_lists_id_jag_profile(client: httpx2.AsyncClient):
response = await client.get("/.well-known/oauth-authorization-server")
assert response.status_code == 200
body = response.json()
@ -169,7 +169,7 @@ async def test_metadata_endpoint_lists_id_jag_profile(client: httpx.AsyncClient)
@pytest.mark.anyio
async def test_identity_assertion_success(client: httpx.AsyncClient, provider: IdentityAssertionProvider):
async def test_identity_assertion_success(client: httpx2.AsyncClient, provider: IdentityAssertionProvider):
response = await client.post("/token", data=assertion_form(scope="mcp", resource="https://mcp.example.com/mcp"))
assert response.status_code == 200, response.content
@ -189,7 +189,7 @@ async def test_identity_assertion_success(client: httpx.AsyncClient, provider: I
@pytest.mark.anyio
async def test_identity_assertion_invalid_assertion(client: httpx.AsyncClient):
async def test_identity_assertion_invalid_assertion(client: httpx2.AsyncClient):
response = await client.post("/token", data=assertion_form(assertion="forged"))
assert response.status_code == 400
@ -207,7 +207,7 @@ async def test_identity_assertion_rejected_when_disabled(provider: IdentityAsser
)
app = Starlette(routes=routes)
transport = ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="https://auth.example.com") as http:
async with httpx2.AsyncClient(transport=transport, base_url="https://auth.example.com") as http:
response = await http.post("/token", data=assertion_form())
assert response.status_code == 400
@ -216,7 +216,9 @@ async def test_identity_assertion_rejected_when_disabled(provider: IdentityAsser
@pytest.mark.anyio
async def test_identity_assertion_rejects_public_client(client: httpx.AsyncClient, provider: IdentityAssertionProvider):
async def test_identity_assertion_rejects_public_client(
client: httpx2.AsyncClient, provider: IdentityAssertionProvider
):
"""A public (auth method 'none') client cannot use the grant, even if it presents a valid assertion."""
provider.clients["public-client"] = OAuthClientInformationFull(
client_id="public-client",
@ -238,7 +240,7 @@ async def test_identity_assertion_rejects_public_client(client: httpx.AsyncClien
@pytest.mark.anyio
async def test_identity_assertion_rejects_secretless_confidential_client(
client: httpx.AsyncClient, provider: IdentityAssertionProvider
client: httpx2.AsyncClient, provider: IdentityAssertionProvider
):
"""A client registered with a secret-based method but no stored secret fails authentication.
@ -267,7 +269,7 @@ async def test_identity_assertion_rejects_secretless_confidential_client(
@pytest.mark.anyio
async def test_malformed_request_missing_assertion_is_invalid_request(client: httpx.AsyncClient):
async def test_malformed_request_missing_assertion_is_invalid_request(client: httpx2.AsyncClient):
"""A jwt-bearer request without the required `assertion` fails validation with invalid_request."""
response = await client.post(
"/token",
@ -284,7 +286,7 @@ async def test_malformed_request_missing_assertion_is_invalid_request(client: ht
@pytest.mark.anyio
async def test_client_without_the_grant_registered_is_rejected(
client: httpx.AsyncClient, provider: IdentityAssertionProvider
client: httpx2.AsyncClient, provider: IdentityAssertionProvider
):
"""A confidential client whose registration omits the jwt-bearer grant is refused the grant."""
provider.clients["no-grant-client"] = OAuthClientInformationFull(
@ -313,7 +315,7 @@ async def test_client_without_the_grant_registered_is_rejected(
@pytest.mark.anyio
async def test_dcr_refuses_to_register_the_jwt_bearer_grant(
client: httpx.AsyncClient, provider: IdentityAssertionProvider
client: httpx2.AsyncClient, provider: IdentityAssertionProvider
):
"""Dynamic client registration rejects the jwt-bearer grant; the ID-JAG flow needs pre-registration."""
response = await client.post(

View file

@ -2,7 +2,7 @@
from urllib.parse import urlparse
import httpx
import httpx2
import pytest
from inline_snapshot import snapshot
from pydantic import AnyHttpUrl
@ -31,12 +31,14 @@ def test_app():
@pytest.fixture
async def test_client(test_app: Starlette):
"""Fixture to create an HTTP client for the protected resource app."""
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=test_app), base_url="https://mcptest.com") as client:
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=test_app), base_url="https://mcptest.com"
) as client:
yield client
@pytest.mark.anyio
async def test_metadata_endpoint_with_path(test_client: httpx.AsyncClient):
async def test_metadata_endpoint_with_path(test_client: httpx2.AsyncClient):
"""Test the OAuth 2.0 Protected Resource metadata endpoint for path-based resource."""
# For resource with path "/resource", metadata should be accessible at the path-aware location
@ -54,7 +56,7 @@ async def test_metadata_endpoint_with_path(test_client: httpx.AsyncClient):
@pytest.mark.anyio
async def test_metadata_endpoint_root_path_returns_404(test_client: httpx.AsyncClient):
async def test_metadata_endpoint_root_path_returns_404(test_client: httpx2.AsyncClient):
"""Test that root path returns 404 for path-based resource."""
# Root path should return 404 for path-based resources
@ -81,14 +83,14 @@ def root_resource_app():
@pytest.fixture
async def root_resource_client(root_resource_app: Starlette):
"""Fixture to create an HTTP client for the root resource app."""
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=root_resource_app), base_url="https://mcptest.com"
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=root_resource_app), base_url="https://mcptest.com"
) as client:
yield client
@pytest.mark.anyio
async def test_metadata_endpoint_without_path(root_resource_client: httpx.AsyncClient):
async def test_metadata_endpoint_without_path(root_resource_client: httpx2.AsyncClient):
"""Test metadata endpoint for root-level resource."""
# For root resource, metadata should be at standard location

View file

@ -8,7 +8,7 @@ import unittest.mock
from typing import Any
from urllib.parse import parse_qs, urlparse
import httpx
import httpx2
import pytest
from pydantic import AnyHttpUrl, AnyUrl
from starlette.applications import Starlette
@ -220,13 +220,15 @@ def auth_app(mock_oauth_provider: MockOAuthProvider):
@pytest.fixture
async def test_client(auth_app: Starlette):
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=auth_app), base_url="https://mcptest.com") as client:
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=auth_app), base_url="https://mcptest.com"
) as client:
yield client
@pytest.fixture
async def registered_client(
test_client: httpx.AsyncClient, request: pytest.FixtureRequest
test_client: httpx2.AsyncClient, request: pytest.FixtureRequest
) -> OAuthClientInformationFull:
"""Create and register a test client.
@ -264,7 +266,7 @@ def pkce_challenge():
@pytest.fixture
async def auth_code(
test_client: httpx.AsyncClient,
test_client: httpx2.AsyncClient,
registered_client: dict[str, Any],
pkce_challenge: dict[str, str],
request: pytest.FixtureRequest,
@ -310,7 +312,7 @@ async def auth_code(
class TestAuthEndpoints:
@pytest.mark.anyio
async def test_metadata_endpoint(self, test_client: httpx.AsyncClient):
async def test_metadata_endpoint(self, test_client: httpx2.AsyncClient):
"""Test the OAuth 2.0 metadata endpoint."""
response = await test_client.get("/.well-known/oauth-authorization-server")
@ -332,7 +334,7 @@ class TestAuthEndpoints:
assert metadata["service_documentation"] == "https://docs.example.com/"
@pytest.mark.anyio
async def test_token_validation_error(self, test_client: httpx.AsyncClient):
async def test_token_validation_error(self, test_client: httpx2.AsyncClient):
"""Test token endpoint error - validation error."""
# Missing required fields
response = await test_client.post(
@ -351,7 +353,7 @@ class TestAuthEndpoints:
@pytest.mark.anyio
async def test_token_invalid_client_secret_returns_invalid_client(
self,
test_client: httpx.AsyncClient,
test_client: httpx2.AsyncClient,
registered_client: dict[str, Any],
pkce_challenge: dict[str, str],
mock_oauth_provider: MockOAuthProvider,
@ -399,7 +401,7 @@ class TestAuthEndpoints:
@pytest.mark.anyio
async def test_token_invalid_auth_code(
self,
test_client: httpx.AsyncClient,
test_client: httpx2.AsyncClient,
registered_client: dict[str, Any],
pkce_challenge: dict[str, str],
):
@ -425,7 +427,7 @@ class TestAuthEndpoints:
@pytest.mark.anyio
async def test_token_expired_auth_code(
self,
test_client: httpx.AsyncClient,
test_client: httpx2.AsyncClient,
registered_client: dict[str, Any],
auth_code: dict[str, str],
pkce_challenge: dict[str, str],
@ -480,7 +482,7 @@ class TestAuthEndpoints:
)
async def test_token_redirect_uri_mismatch(
self,
test_client: httpx.AsyncClient,
test_client: httpx2.AsyncClient,
registered_client: dict[str, Any],
auth_code: dict[str, str],
pkce_challenge: dict[str, str],
@ -506,7 +508,7 @@ class TestAuthEndpoints:
@pytest.mark.anyio
async def test_token_code_verifier_mismatch(
self, test_client: httpx.AsyncClient, registered_client: dict[str, Any], auth_code: dict[str, str]
self, test_client: httpx2.AsyncClient, registered_client: dict[str, Any], auth_code: dict[str, str]
):
"""Test token endpoint error - PKCE code verifier mismatch."""
# Try to use the code with an incorrect code verifier
@ -528,7 +530,9 @@ class TestAuthEndpoints:
assert "incorrect code_verifier" in error_response["error_description"]
@pytest.mark.anyio
async def test_token_invalid_refresh_token(self, test_client: httpx.AsyncClient, registered_client: dict[str, Any]):
async def test_token_invalid_refresh_token(
self, test_client: httpx2.AsyncClient, registered_client: dict[str, Any]
):
"""Test token endpoint error - refresh token does not exist."""
# Try to use a non-existent refresh token
response = await test_client.post(
@ -548,7 +552,7 @@ class TestAuthEndpoints:
@pytest.mark.anyio
async def test_token_expired_refresh_token(
self,
test_client: httpx.AsyncClient,
test_client: httpx2.AsyncClient,
registered_client: dict[str, Any],
auth_code: dict[str, str],
pkce_challenge: dict[str, str],
@ -596,7 +600,7 @@ class TestAuthEndpoints:
@pytest.mark.anyio
async def test_token_invalid_scope(
self,
test_client: httpx.AsyncClient,
test_client: httpx2.AsyncClient,
registered_client: dict[str, Any],
auth_code: dict[str, str],
pkce_challenge: dict[str, str],
@ -636,7 +640,7 @@ class TestAuthEndpoints:
assert "cannot request scope" in error_response["error_description"]
@pytest.mark.anyio
async def test_client_registration(self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider):
async def test_client_registration(self, test_client: httpx2.AsyncClient, mock_oauth_provider: MockOAuthProvider):
"""Test client registration."""
client_metadata = {
"redirect_uris": ["https://client.example.com/callback"],
@ -662,7 +666,7 @@ class TestAuthEndpoints:
# ) is not None
@pytest.mark.anyio
async def test_client_registration_missing_required_fields(self, test_client: httpx.AsyncClient):
async def test_client_registration_missing_required_fields(self, test_client: httpx2.AsyncClient):
"""Test client registration with missing required fields."""
# Missing redirect_uris which is a required field
client_metadata = {
@ -681,7 +685,7 @@ class TestAuthEndpoints:
assert error_data["error_description"] == "redirect_uris: Field required"
@pytest.mark.anyio
async def test_client_registration_invalid_uri(self, test_client: httpx.AsyncClient):
async def test_client_registration_invalid_uri(self, test_client: httpx2.AsyncClient):
"""Test client registration with invalid URIs."""
# Invalid redirect_uri format
client_metadata = {
@ -702,7 +706,7 @@ class TestAuthEndpoints:
)
@pytest.mark.anyio
async def test_client_registration_empty_redirect_uris(self, test_client: httpx.AsyncClient):
async def test_client_registration_empty_redirect_uris(self, test_client: httpx2.AsyncClient):
"""Test client registration with empty redirect_uris array."""
redirect_uris: list[str] = []
client_metadata = {
@ -723,7 +727,7 @@ class TestAuthEndpoints:
)
@pytest.mark.anyio
async def test_authorize_form_post(self, test_client: httpx.AsyncClient, pkce_challenge: dict[str, str]):
async def test_authorize_form_post(self, test_client: httpx2.AsyncClient, pkce_challenge: dict[str, str]):
"""Test the authorization endpoint using POST with form-encoded data."""
# Register a client
client_metadata = {
@ -764,7 +768,7 @@ class TestAuthEndpoints:
@pytest.mark.anyio
async def test_authorization_get(
self,
test_client: httpx.AsyncClient,
test_client: httpx2.AsyncClient,
mock_oauth_provider: MockOAuthProvider,
pkce_challenge: dict[str, str],
):
@ -877,7 +881,7 @@ class TestAuthEndpoints:
assert await mock_oauth_provider.load_access_token(new_token_response["access_token"]) is None
@pytest.mark.anyio
async def test_revoke_invalid_token(self, test_client: httpx.AsyncClient, registered_client: dict[str, Any]):
async def test_revoke_invalid_token(self, test_client: httpx2.AsyncClient, registered_client: dict[str, Any]):
"""Test revoking an invalid token."""
response = await test_client.post(
"/revoke",
@ -891,7 +895,9 @@ class TestAuthEndpoints:
assert response.status_code == 200
@pytest.mark.anyio
async def test_revoke_with_malformed_token(self, test_client: httpx.AsyncClient, registered_client: dict[str, Any]):
async def test_revoke_with_malformed_token(
self, test_client: httpx2.AsyncClient, registered_client: dict[str, Any]
):
response = await test_client.post(
"/revoke",
data={
@ -907,7 +913,7 @@ class TestAuthEndpoints:
assert "token_type_hint" in error_response["error_description"]
@pytest.mark.anyio
async def test_client_registration_disallowed_scopes(self, test_client: httpx.AsyncClient):
async def test_client_registration_disallowed_scopes(self, test_client: httpx2.AsyncClient):
"""Test client registration with scopes that are not allowed."""
client_metadata = {
"redirect_uris": ["https://client.example.com/callback"],
@ -925,7 +931,7 @@ class TestAuthEndpoints:
@pytest.mark.anyio
async def test_client_registration_default_scopes(
self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider
self, test_client: httpx2.AsyncClient, mock_oauth_provider: MockOAuthProvider
):
client_metadata = {
"redirect_uris": ["https://client.example.com/callback"],
@ -948,7 +954,7 @@ class TestAuthEndpoints:
assert registered_client.scope == "read write"
@pytest.mark.anyio
async def test_client_registration_with_authorization_code_only(self, test_client: httpx.AsyncClient):
async def test_client_registration_with_authorization_code_only(self, test_client: httpx2.AsyncClient):
"""Test that registration succeeds with only authorization_code (refresh_token is optional per RFC 7591)."""
client_metadata = {
"redirect_uris": ["https://client.example.com/callback"],
@ -963,7 +969,7 @@ class TestAuthEndpoints:
assert client_info["grant_types"] == ["authorization_code"]
@pytest.mark.anyio
async def test_client_registration_missing_authorization_code(self, test_client: httpx.AsyncClient):
async def test_client_registration_missing_authorization_code(self, test_client: httpx2.AsyncClient):
"""Test that registration fails when authorization_code grant type is missing."""
client_metadata = {
"redirect_uris": ["https://client.example.com/callback"],
@ -979,7 +985,7 @@ class TestAuthEndpoints:
assert error_data["error_description"] == "grant_types must include 'authorization_code'"
@pytest.mark.anyio
async def test_client_registration_with_additional_grant_type(self, test_client: httpx.AsyncClient):
async def test_client_registration_with_additional_grant_type(self, test_client: httpx2.AsyncClient):
client_metadata = {
"redirect_uris": ["https://client.example.com/callback"],
"client_name": "Test Client",
@ -997,7 +1003,7 @@ class TestAuthEndpoints:
@pytest.mark.anyio
async def test_client_registration_with_additional_response_types(
self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider
self, test_client: httpx2.AsyncClient, mock_oauth_provider: MockOAuthProvider
):
"""Test that registration accepts additional response_types values alongside 'code'."""
client_metadata = {
@ -1016,7 +1022,7 @@ class TestAuthEndpoints:
assert "code" in client.response_types
@pytest.mark.anyio
async def test_client_registration_response_types_without_code(self, test_client: httpx.AsyncClient):
async def test_client_registration_response_types_without_code(self, test_client: httpx2.AsyncClient):
"""Test that registration rejects response_types that don't include 'code'."""
client_metadata = {
"redirect_uris": ["https://client.example.com/callback"],
@ -1034,7 +1040,7 @@ class TestAuthEndpoints:
@pytest.mark.anyio
async def test_client_registration_default_response_types(
self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider
self, test_client: httpx2.AsyncClient, mock_oauth_provider: MockOAuthProvider
):
"""Test that registration uses default response_types of ['code'] when not specified."""
client_metadata = {
@ -1053,7 +1059,7 @@ class TestAuthEndpoints:
@pytest.mark.anyio
async def test_client_secret_basic_authentication(
self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider, pkce_challenge: dict[str, str]
self, test_client: httpx2.AsyncClient, mock_oauth_provider: MockOAuthProvider, pkce_challenge: dict[str, str]
):
"""Test that client_secret_basic authentication works correctly."""
client_metadata = {
@ -1099,7 +1105,7 @@ class TestAuthEndpoints:
@pytest.mark.anyio
async def test_wrong_auth_method_without_valid_credentials_fails(
self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider, pkce_challenge: dict[str, str]
self, test_client: httpx2.AsyncClient, mock_oauth_provider: MockOAuthProvider, pkce_challenge: dict[str, str]
):
"""Test that using the wrong authentication method fails when credentials are missing."""
client_metadata = {
@ -1151,7 +1157,7 @@ class TestAuthEndpoints:
@pytest.mark.anyio
async def test_basic_auth_without_header_fails(
self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider, pkce_challenge: dict[str, str]
self, test_client: httpx2.AsyncClient, mock_oauth_provider: MockOAuthProvider, pkce_challenge: dict[str, str]
):
"""Test that omitting Basic auth when client_secret_basic is registered fails."""
client_metadata = {
@ -1196,7 +1202,7 @@ class TestAuthEndpoints:
@pytest.mark.anyio
async def test_basic_auth_invalid_base64_fails(
self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider, pkce_challenge: dict[str, str]
self, test_client: httpx2.AsyncClient, mock_oauth_provider: MockOAuthProvider, pkce_challenge: dict[str, str]
):
"""Test that invalid base64 in Basic auth header fails."""
client_metadata = {
@ -1241,7 +1247,7 @@ class TestAuthEndpoints:
@pytest.mark.anyio
async def test_basic_auth_no_colon_fails(
self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider, pkce_challenge: dict[str, str]
self, test_client: httpx2.AsyncClient, mock_oauth_provider: MockOAuthProvider, pkce_challenge: dict[str, str]
):
"""Test that Basic auth without colon separator fails."""
client_metadata = {
@ -1287,7 +1293,7 @@ class TestAuthEndpoints:
@pytest.mark.anyio
async def test_basic_auth_client_id_mismatch_fails(
self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider, pkce_challenge: dict[str, str]
self, test_client: httpx2.AsyncClient, mock_oauth_provider: MockOAuthProvider, pkce_challenge: dict[str, str]
):
"""Test that client_id mismatch between body and Basic auth fails."""
client_metadata = {
@ -1333,7 +1339,7 @@ class TestAuthEndpoints:
@pytest.mark.anyio
async def test_none_auth_method_public_client(
self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider, pkce_challenge: dict[str, str]
self, test_client: httpx2.AsyncClient, mock_oauth_provider: MockOAuthProvider, pkce_challenge: dict[str, str]
):
"""Test that 'none' authentication method works for public clients."""
client_metadata = {
@ -1381,7 +1387,7 @@ class TestAuthorizeEndpointErrors:
"""Test error handling in the OAuth authorization endpoint."""
@pytest.mark.anyio
async def test_authorize_missing_client_id(self, test_client: httpx.AsyncClient, pkce_challenge: dict[str, str]):
async def test_authorize_missing_client_id(self, test_client: httpx2.AsyncClient, pkce_challenge: dict[str, str]):
"""Test authorization endpoint with missing client_id.
According to the OAuth2.0 spec, if client_id is missing, the server should
@ -1405,7 +1411,7 @@ class TestAuthorizeEndpointErrors:
assert "client_id" in response.text.lower()
@pytest.mark.anyio
async def test_authorize_invalid_client_id(self, test_client: httpx.AsyncClient, pkce_challenge: dict[str, str]):
async def test_authorize_invalid_client_id(self, test_client: httpx2.AsyncClient, pkce_challenge: dict[str, str]):
"""Test authorization endpoint with invalid client_id.
According to the OAuth2.0 spec, if client_id is invalid, the server should
@ -1430,7 +1436,7 @@ class TestAuthorizeEndpointErrors:
@pytest.mark.anyio
async def test_authorize_missing_redirect_uri(
self, test_client: httpx.AsyncClient, registered_client: dict[str, Any], pkce_challenge: dict[str, str]
self, test_client: httpx2.AsyncClient, registered_client: dict[str, Any], pkce_challenge: dict[str, str]
):
"""Test authorization endpoint with missing redirect_uri.
@ -1456,7 +1462,7 @@ class TestAuthorizeEndpointErrors:
@pytest.mark.anyio
async def test_authorize_invalid_redirect_uri(
self, test_client: httpx.AsyncClient, registered_client: dict[str, Any], pkce_challenge: dict[str, str]
self, test_client: httpx2.AsyncClient, registered_client: dict[str, Any], pkce_challenge: dict[str, str]
):
"""Test authorization endpoint with invalid redirect_uri.
@ -1496,7 +1502,7 @@ class TestAuthorizeEndpointErrors:
indirect=True,
)
async def test_authorize_missing_redirect_uri_multiple_registered(
self, test_client: httpx.AsyncClient, registered_client: dict[str, Any], pkce_challenge: dict[str, str]
self, test_client: httpx2.AsyncClient, registered_client: dict[str, Any], pkce_challenge: dict[str, str]
):
"""Test endpoint with missing redirect_uri with multiple registered URIs.
@ -1522,7 +1528,7 @@ class TestAuthorizeEndpointErrors:
@pytest.mark.anyio
async def test_authorize_unsupported_response_type(
self, test_client: httpx.AsyncClient, registered_client: dict[str, Any], pkce_challenge: dict[str, str]
self, test_client: httpx2.AsyncClient, registered_client: dict[str, Any], pkce_challenge: dict[str, str]
):
"""Test authorization endpoint with unsupported response_type.
@ -1556,7 +1562,7 @@ class TestAuthorizeEndpointErrors:
@pytest.mark.anyio
async def test_authorize_missing_response_type(
self, test_client: httpx.AsyncClient, registered_client: dict[str, Any], pkce_challenge: dict[str, str]
self, test_client: httpx2.AsyncClient, registered_client: dict[str, Any], pkce_challenge: dict[str, str]
):
"""Test authorization endpoint with missing response_type.
@ -1589,7 +1595,7 @@ class TestAuthorizeEndpointErrors:
@pytest.mark.anyio
async def test_authorize_missing_pkce_challenge(
self, test_client: httpx.AsyncClient, registered_client: dict[str, Any]
self, test_client: httpx2.AsyncClient, registered_client: dict[str, Any]
):
"""Test authorization endpoint with missing PKCE code_challenge.
@ -1620,7 +1626,7 @@ class TestAuthorizeEndpointErrors:
@pytest.mark.anyio
async def test_authorize_invalid_scope(
self, test_client: httpx.AsyncClient, registered_client: dict[str, Any], pkce_challenge: dict[str, str]
self, test_client: httpx2.AsyncClient, registered_client: dict[str, Any], pkce_challenge: dict[str, str]
):
"""Test authorization endpoint with invalid scope.

View file

@ -4,7 +4,7 @@ import logging
import re
import anyio
import httpx
import httpx2
import pytest
import sse_starlette.sse
from mcp_types import JSONRPCRequest, JSONRPCResponse
@ -40,8 +40,8 @@ def reset_sse_starlette_exit_event() -> None:
app_status.should_exit_event = None
def sse_security_client(security_settings: TransportSecuritySettings | None = None) -> httpx.AsyncClient:
"""An httpx client whose requests are served in process by an SSE app with the given settings."""
def sse_security_client(security_settings: TransportSecuritySettings | None = None) -> httpx2.AsyncClient:
"""An httpx2 client whose requests are served in process by an SSE app with the given settings."""
server = Server(SERVER_NAME)
sse_transport = SseServerTransport("/messages/", security_settings)
@ -65,7 +65,7 @@ def sse_security_client(security_settings: TransportSecuritySettings | None = No
# The SSE GET runs until it observes a disconnect, so the bridge must let the application
# drain on close rather than cancelling it.
transport = StreamingASGITransport(app, cancel_on_close=False)
return httpx.AsyncClient(transport=transport, base_url=BASE_URL)
return httpx2.AsyncClient(transport=transport, base_url=BASE_URL)
@pytest.mark.anyio

View file

@ -6,7 +6,7 @@ from typing import Any
from unittest.mock import AsyncMock, patch
import anyio
import httpx
import httpx2
import pytest
from mcp_types import INVALID_REQUEST, ListToolsResult, PaginatedRequestParams
from starlette.types import Message, Scope
@ -331,8 +331,8 @@ async def test_e2e_streamable_http_server_cleanup():
mcp_app = app.streamable_http_app(host=host)
async with (
mcp_app.router.lifespan_context(mcp_app),
httpx.ASGITransport(mcp_app) as transport,
httpx.AsyncClient(transport=transport) as http_client,
httpx2.ASGITransport(mcp_app) as transport,
httpx2.AsyncClient(transport=transport) as http_client,
Client(streamable_http_client(f"http://{host}/mcp", http_client=http_client), mode="legacy") as client,
):
await client.list_tools()

View file

@ -12,7 +12,7 @@ from collections.abc import Callable
from typing import Any
import anyio
import httpx
import httpx2
import pytest
from mcp_types import (
CLIENT_CAPABILITIES_META_KEY,
@ -81,12 +81,12 @@ def _asgi_client(
*,
json_response: bool = True,
accept: str = "application/json, text/event-stream",
) -> httpx.AsyncClient:
) -> httpx2.AsyncClient:
async def app(scope: Scope, receive: Receive, send: Send) -> None:
async with server.lifespan(server) as lifespan_state:
await handle_modern_request(server, security_settings, json_response, lifespan_state, scope, receive, send)
return httpx.AsyncClient(
return httpx2.AsyncClient(
transport=StreamingASGITransport(app),
base_url="http://testserver",
headers={
@ -894,8 +894,8 @@ async def test_modern_tools_call_leaves_mis_shaped_name_and_arguments_to_dispatc
async def test_modern_tools_call_rejects_a_duplicated_mcp_param_header() -> None:
"""A duplicated recognized header is rejected even if one copy matches: readers may disagree on which wins."""
# An httpx header list with a repeated name reaches the ASGI scope as two raw header lines.
duplicated = httpx.Headers(
# An httpx2 header list with a repeated name reaches the ASGI scope as two raw header lines.
duplicated = httpx2.Headers(
[*_TOOL_CALL_HEADERS.items(), ("mcp-param-region", "spoofed"), ("mcp-param-region", "eu")]
)
async with _asgi_client(_x_mcp_server()) as http:
@ -928,7 +928,7 @@ async def test_modern_synthetic_listing_does_not_replay_caller_meta_extras() ->
async def test_modern_post_rejects_a_duplicated_routing_header() -> None:
"""A duplicated routing header (`Mcp-Name`) is unverifiable and rejected before the validation ladder runs."""
duplicated = httpx.Headers(
duplicated = httpx2.Headers(
[(MCP_METHOD_HEADER, "tools/call"), (MCP_NAME_HEADER, "search"), (MCP_NAME_HEADER, "admin-tool")]
)
async with _asgi_client(_x_mcp_server()) as http:
@ -1066,7 +1066,7 @@ async def test_json_response_mode_still_streams_subscriptions_listen() -> None:
server = Server("test", on_subscriptions_listen=handler)
body = _listen_body()
responses: list[httpx.Response] = []
responses: list[httpx2.Response] = []
async with _asgi_client(server, json_response=True) as http:
async with anyio.create_task_group() as tg:

View file

@ -3,7 +3,7 @@
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
import httpx
import httpx2
import pytest
from starlette.applications import Starlette
from starlette.routing import Mount
@ -23,13 +23,13 @@ BASE_URL = "http://127.0.0.1:8000"
@asynccontextmanager
async def streamable_http_security_client(
security_settings: TransportSecuritySettings | None = None,
) -> AsyncIterator[httpx.AsyncClient]:
"""Yield an httpx client served in process by a StreamableHTTP app with the given settings."""
) -> AsyncIterator[httpx2.AsyncClient]:
"""Yield an httpx2 client served in process by a StreamableHTTP app with the given settings."""
session_manager = StreamableHTTPSessionManager(app=Server(SERVER_NAME), security_settings=security_settings)
app = Starlette(routes=[Mount("/", app=session_manager.handle_request)])
async with session_manager.run():
async with httpx.AsyncClient(transport=StreamingASGITransport(app), base_url=BASE_URL) as client:
async with httpx2.AsyncClient(transport=StreamingASGITransport(app), base_url=BASE_URL) as client:
yield client