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
3 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