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

View file

@ -6,7 +6,7 @@ import time
from unittest import mock
from urllib.parse import parse_qs, quote, unquote, urlparse
import httpx
import httpx2
import pytest
from inline_snapshot import Is, snapshot
from pydantic import AnyHttpUrl, AnyUrl
@ -111,7 +111,7 @@ def oauth_provider(client_metadata: OAuthClientMetadata, mock_storage: MockToken
@pytest.fixture
def prm_metadata_response():
"""PRM metadata response with scopes."""
return httpx.Response(
return httpx2.Response(
200,
content=(
b'{"resource": "https://api.example.com/v1/mcp", '
@ -124,7 +124,7 @@ def prm_metadata_response():
@pytest.fixture
def prm_metadata_without_scopes_response():
"""PRM metadata response without scopes."""
return httpx.Response(
return httpx2.Response(
200,
content=(
b'{"resource": "https://api.example.com/v1/mcp", '
@ -137,20 +137,20 @@ def prm_metadata_without_scopes_response():
@pytest.fixture
def init_response_with_www_auth_scope():
"""Initial 401 response with WWW-Authenticate header containing scope."""
return httpx.Response(
return httpx2.Response(
401,
headers={"WWW-Authenticate": 'Bearer scope="special:scope from:www-authenticate"'},
request=httpx.Request("GET", "https://api.example.com/test"),
request=httpx2.Request("GET", "https://api.example.com/test"),
)
@pytest.fixture
def init_response_without_www_auth_scope():
"""Initial 401 response without WWW-Authenticate scope."""
return httpx.Response(
return httpx2.Response(
401,
headers={},
request=httpx.Request("GET", "https://api.example.com/test"),
request=httpx2.Request("GET", "https://api.example.com/test"),
)
@ -290,8 +290,8 @@ class TestOAuthFlow:
)
# Test without WWW-Authenticate (fallback)
init_response = httpx.Response(
status_code=401, headers={}, request=httpx.Request("GET", "https://request-api.example.com")
init_response = httpx2.Response(
status_code=401, headers={}, request=httpx2.Request("GET", "https://request-api.example.com")
)
urls = build_protected_resource_metadata_discovery_urls(
@ -408,7 +408,7 @@ class TestOAuthFallback:
)
# Create a test request
test_request = httpx.Request("GET", "https://api.example.com/v1/mcp")
test_request = httpx2.Request("GET", "https://api.example.com/v1/mcp")
# Mock the auth flow
auth_flow = oauth_provider.async_auth_flow(test_request)
@ -418,7 +418,7 @@ class TestOAuthFallback:
assert "Authorization" not in request.headers
# Send a 401 response to trigger the OAuth flow
response = httpx.Response(
response = httpx2.Response(
401,
headers={
"WWW-Authenticate": 'Bearer resource_metadata="https://api.example.com/.well-known/oauth-protected-resource"'
@ -433,7 +433,7 @@ class TestOAuthFallback:
# Send a successful discovery response with minimal protected resource metadata
# Note: auth server URL has a path (/v1/mcp), so only path-based URLs will be tried
discovery_response = httpx.Response(
discovery_response = httpx2.Response(
200,
content=b'{"resource": "https://api.example.com/v1/mcp", "authorization_servers": ["https://auth.example.com/v1/mcp"]}',
request=discovery_request,
@ -448,7 +448,7 @@ class TestOAuthFallback:
assert oauth_metadata_request_1.method == "GET"
# Send a 404 response
oauth_metadata_response_1 = httpx.Response(
oauth_metadata_response_1 = httpx2.Response(
404,
content=b"Not Found",
request=oauth_metadata_request_1,
@ -460,7 +460,7 @@ class TestOAuthFallback:
assert oauth_metadata_request_2.method == "GET"
# Send a 400 response
oauth_metadata_response_2 = httpx.Response(
oauth_metadata_response_2 = httpx2.Response(
400,
content=b"Bad Request",
request=oauth_metadata_request_2,
@ -472,7 +472,7 @@ class TestOAuthFallback:
assert oauth_metadata_request_3.method == "GET"
# Send a 500 response
oauth_metadata_response_3 = httpx.Response(
oauth_metadata_response_3 = httpx2.Response(
500,
content=b"Internal Server Error",
request=oauth_metadata_request_3,
@ -490,7 +490,7 @@ class TestOAuthFallback:
assert token_request.method == "POST"
# Send a successful token response
token_response = httpx.Response(
token_response = httpx2.Response(
200,
content=(
b'{"access_token": "new_access_token", "token_type": "Bearer", "expires_in": 3600, '
@ -506,7 +506,7 @@ class TestOAuthFallback:
assert str(final_request.url) == "https://api.example.com/v1/mcp"
# Send final success response to properly close the generator
final_response = httpx.Response(200, request=final_request)
final_response = httpx2.Response(200, request=final_request)
try:
await auth_flow.asend(final_response)
except StopAsyncIteration:
@ -521,7 +521,7 @@ class TestOAuthFallback:
"authorization_endpoint": "https://auth.example.com/authorize",
"token_endpoint": "https://auth.example.com/token"
}"""
response = httpx.Response(200, content=content)
response = httpx2.Response(200, content=content)
# Should set metadata; the empty path is preserved (no trailing slash added)
await oauth_provider._handle_oauth_metadata_response(response)
@ -532,8 +532,8 @@ class TestOAuthFallback:
async def test_prioritize_www_auth_scope_over_prm(
self,
oauth_provider: OAuthClientProvider,
prm_metadata_response: httpx.Response,
init_response_with_www_auth_scope: httpx.Response,
prm_metadata_response: httpx2.Response,
init_response_with_www_auth_scope: httpx2.Response,
):
"""Test that WWW-Authenticate scope is prioritized over PRM scopes."""
# First, process PRM metadata to set protected_resource_metadata with scopes
@ -552,8 +552,8 @@ class TestOAuthFallback:
async def test_prioritize_prm_scopes_when_no_www_auth_scope(
self,
oauth_provider: OAuthClientProvider,
prm_metadata_response: httpx.Response,
init_response_without_www_auth_scope: httpx.Response,
prm_metadata_response: httpx2.Response,
init_response_without_www_auth_scope: httpx2.Response,
):
"""Test that PRM scopes are prioritized when WWW-Authenticate header has no scopes."""
# Process the PRM metadata to set protected_resource_metadata with scopes
@ -572,8 +572,8 @@ class TestOAuthFallback:
async def test_omit_scope_when_no_prm_scopes_or_www_auth(
self,
oauth_provider: OAuthClientProvider,
prm_metadata_without_scopes_response: httpx.Response,
init_response_without_www_auth_scope: httpx.Response,
prm_metadata_without_scopes_response: httpx2.Response,
init_response_without_www_auth_scope: httpx2.Response,
):
"""Test that scope is omitted when PRM has no scopes and WWW-Authenticate doesn't specify scope."""
# Process the PRM metadata without scopes
@ -981,7 +981,7 @@ class TestRegistrationResponse:
"""Test that response.aread() is called before accessing response.text."""
# Track if aread() was called
class MockResponse(httpx.Response):
class MockResponse(httpx2.Response):
def __init__(self):
self.status_code = 400
self._aread_called = False
@ -1068,7 +1068,7 @@ def test_registration_request_sends_application_type():
class TestAuthFlow:
"""Test the auth flow in httpx."""
"""Test the auth flow in httpx2."""
@pytest.mark.anyio
async def test_auth_flow_with_valid_tokens(
@ -1082,7 +1082,7 @@ class TestAuthFlow:
oauth_provider._initialized = True
# Create a test request
test_request = httpx.Request("GET", "https://api.example.com/test")
test_request = httpx2.Request("GET", "https://api.example.com/test")
# Mock the auth flow
auth_flow = oauth_provider.async_auth_flow(test_request)
@ -1092,7 +1092,7 @@ class TestAuthFlow:
assert request.headers["Authorization"] == "Bearer test_access_token"
# Send a successful response
response = httpx.Response(200)
response = httpx2.Response(200)
try:
await auth_flow.asend(response)
except StopAsyncIteration:
@ -1107,7 +1107,7 @@ class TestAuthFlow:
oauth_provider._initialized = True
# Create a test request
test_request = httpx.Request("GET", "https://api.example.com/mcp")
test_request = httpx2.Request("GET", "https://api.example.com/mcp")
# Mock the auth flow
auth_flow = oauth_provider.async_auth_flow(test_request)
@ -1117,7 +1117,7 @@ class TestAuthFlow:
assert "Authorization" not in request.headers
# Send a 401 response to trigger the OAuth flow
response = httpx.Response(
response = httpx2.Response(
401,
headers={
"WWW-Authenticate": 'Bearer resource_metadata="https://api.example.com/.well-known/oauth-protected-resource"'
@ -1131,7 +1131,7 @@ class TestAuthFlow:
assert str(discovery_request.url) == "https://api.example.com/.well-known/oauth-protected-resource"
# Send a successful discovery response with minimal protected resource metadata
discovery_response = httpx.Response(
discovery_response = httpx2.Response(
200,
content=b'{"resource": "https://api.example.com/v1/mcp", "authorization_servers": ["https://auth.example.com"]}',
request=discovery_request,
@ -1144,7 +1144,7 @@ class TestAuthFlow:
assert "mcp-protocol-version" in oauth_metadata_request.headers
# Send a successful OAuth metadata response
oauth_metadata_response = httpx.Response(
oauth_metadata_response = httpx2.Response(
200,
content=(
b'{"issuer": "https://auth.example.com", '
@ -1161,7 +1161,7 @@ class TestAuthFlow:
assert str(registration_request.url) == "https://auth.example.com/register"
# Send a successful registration response
registration_response = httpx.Response(
registration_response = httpx2.Response(
201,
content=b'{"client_id": "test_client_id", "client_secret": "test_client_secret", "redirect_uris": ["http://localhost:3030/callback"]}',
request=registration_request,
@ -1179,7 +1179,7 @@ class TestAuthFlow:
assert "code=test_auth_code" in token_request.content.decode()
# Send a successful token response
token_response = httpx.Response(
token_response = httpx2.Response(
200,
content=(
b'{"access_token": "new_access_token", "token_type": "Bearer", "expires_in": 3600, '
@ -1195,7 +1195,7 @@ class TestAuthFlow:
assert str(final_request.url) == "https://api.example.com/mcp"
# Send final success response to properly close the generator
final_response = httpx.Response(200, request=final_request)
final_response = httpx2.Response(200, request=final_request)
try:
await auth_flow.asend(final_response)
except StopAsyncIteration:
@ -1217,7 +1217,7 @@ class TestAuthFlow:
oauth_provider.context.token_expiry_time = time.time() + 1800
oauth_provider._initialized = True
test_request = httpx.Request("GET", "https://api.example.com/mcp")
test_request = httpx2.Request("GET", "https://api.example.com/mcp")
auth_flow = oauth_provider.async_auth_flow(test_request)
# Count how many times the request is yielded
@ -1229,7 +1229,7 @@ class TestAuthFlow:
assert request.headers["Authorization"] == "Bearer test_access_token"
# Send a successful 200 response
response = httpx.Response(200, request=request)
response = httpx2.Response(200, request=request)
# In the buggy version, this would yield the request AGAIN unconditionally
# In the fixed version, this should end the generator
@ -1260,7 +1260,7 @@ class TestAuthFlow:
oauth_provider._initialized = True
# Create a test request
test_request = httpx.Request("GET", "https://api.example.com/mcp")
test_request = httpx2.Request("GET", "https://api.example.com/mcp")
# Mock the auth flow
auth_flow = oauth_provider.async_auth_flow(test_request)
@ -1270,7 +1270,7 @@ class TestAuthFlow:
assert "Authorization" not in request.headers
# Send a 401 response to trigger the OAuth flow
response = httpx.Response(
response = httpx2.Response(
401,
headers={
"WWW-Authenticate": 'Bearer resource_metadata="https://api.example.com/.well-known/oauth-protected-resource"'
@ -1284,7 +1284,7 @@ class TestAuthFlow:
assert str(discovery_request.url) == "https://api.example.com/.well-known/oauth-protected-resource"
# Send a successful discovery response with minimal protected resource metadata
discovery_response = httpx.Response(
discovery_response = httpx2.Response(
200,
content=b'{"resource": "https://api.example.com/v1/mcp", "authorization_servers": ["https://auth.example.com"]}',
request=discovery_request,
@ -1297,7 +1297,7 @@ class TestAuthFlow:
assert "mcp-protocol-version" in oauth_metadata_request.headers
# Send a successful OAuth metadata response
oauth_metadata_response = httpx.Response(
oauth_metadata_response = httpx2.Response(
200,
content=(
b'{"issuer": "https://auth.example.com", '
@ -1314,7 +1314,7 @@ class TestAuthFlow:
assert str(registration_request.url) == "https://auth.example.com/register"
# Send a successful registration response with 201 status
registration_response = httpx.Response(
registration_response = httpx2.Response(
201,
content=b'{"client_id": "test_client_id", "client_secret": "test_client_secret", "redirect_uris": ["http://localhost:3030/callback"]}',
request=registration_request,
@ -1332,7 +1332,7 @@ class TestAuthFlow:
assert "code=test_auth_code" in token_request.content.decode()
# Send a successful token response with 201 status code (test both 200 and 201 are accepted)
token_response = httpx.Response(
token_response = httpx2.Response(
201,
content=(
b'{"access_token": "new_access_token", "token_type": "Bearer", "expires_in": 3600, '
@ -1348,7 +1348,7 @@ class TestAuthFlow:
assert str(final_request.url) == "https://api.example.com/mcp"
# Send final success response to properly close the generator
final_response = httpx.Response(200, request=final_request)
final_response = httpx2.Response(200, request=final_request)
try:
await auth_flow.asend(final_response)
except StopAsyncIteration:
@ -1405,14 +1405,14 @@ class TestAuthFlow:
oauth_provider.context.callback_handler = mock_callback
test_request = httpx.Request("GET", "https://api.example.com/mcp")
test_request = httpx2.Request("GET", "https://api.example.com/mcp")
auth_flow = oauth_provider.async_auth_flow(test_request)
# First request
request = await auth_flow.__anext__()
# Send 403 with new scope requirement
response_403 = httpx.Response(
response_403 = httpx2.Response(
403,
headers={"WWW-Authenticate": 'Bearer error="insufficient_scope", scope="admin:write admin:delete"'},
request=request,
@ -1426,7 +1426,7 @@ class TestAuthFlow:
assert redirect_captured
# Complete the flow with successful token response
token_response = httpx.Response(
token_response = httpx2.Response(
200,
json={
"access_token": "new_token_with_new_scope",
@ -1441,7 +1441,7 @@ class TestAuthFlow:
final_request = await auth_flow.asend(token_response)
# Send success response - flow should complete
success_response = httpx.Response(200, request=final_request)
success_response = httpx2.Response(200, request=final_request)
try:
await auth_flow.asend(success_response)
pytest.fail("Should have stopped after successful response") # pragma: no cover
@ -1485,9 +1485,9 @@ async def test_403_step_up_preserves_scope_from_stored_token(
oauth_provider.context.redirect_handler = capture_redirect
oauth_provider.context.callback_handler = mock_callback
auth_flow = oauth_provider.async_auth_flow(httpx.Request("GET", "https://api.example.com/mcp"))
auth_flow = oauth_provider.async_auth_flow(httpx2.Request("GET", "https://api.example.com/mcp"))
request = await auth_flow.__anext__()
response_403 = httpx.Response(
response_403 = httpx2.Response(
403,
headers={"WWW-Authenticate": 'Bearer error="insufficient_scope", scope="write"'},
request=request,
@ -1497,14 +1497,14 @@ async def test_403_step_up_preserves_scope_from_stored_token(
assert reauthorize_scope == "read write"
# Drive the flow to completion so the context lock is released cleanly
token_response = httpx.Response(
token_response = httpx2.Response(
200,
json={"access_token": "new", "token_type": "Bearer", "expires_in": 3600, "scope": "read write"},
request=token_exchange_request,
)
final_request = await auth_flow.asend(token_response)
try:
await auth_flow.asend(httpx.Response(200, request=final_request))
await auth_flow.asend(httpx2.Response(200, request=final_request))
except StopAsyncIteration:
pass
@ -1619,7 +1619,7 @@ class TestLegacyServerFallback:
redirect_uris=[AnyUrl("http://localhost:3030/callback")],
)
test_request = httpx.Request("GET", "https://mcp.linear.app/sse")
test_request = httpx2.Request("GET", "https://mcp.linear.app/sse")
auth_flow = provider.async_auth_flow(test_request)
# First request
@ -1627,21 +1627,21 @@ class TestLegacyServerFallback:
assert "Authorization" not in request.headers
# Send 401 without WWW-Authenticate header (typical legacy server)
response = httpx.Response(401, headers={}, request=test_request)
response = httpx2.Response(401, headers={}, request=test_request)
# Should try path-based PRM first
prm_request_1 = await auth_flow.asend(response)
assert str(prm_request_1.url) == "https://mcp.linear.app/.well-known/oauth-protected-resource/sse"
# PRM returns 404
prm_response_1 = httpx.Response(404, request=prm_request_1)
prm_response_1 = httpx2.Response(404, request=prm_request_1)
# Should try root-based PRM
prm_request_2 = await auth_flow.asend(prm_response_1)
assert str(prm_request_2.url) == "https://mcp.linear.app/.well-known/oauth-protected-resource"
# PRM returns 404 again - all PRM URLs failed
prm_response_2 = httpx.Response(404, request=prm_request_2)
prm_response_2 = httpx2.Response(404, request=prm_request_2)
# Should fall back to root OAuth discovery (March 2025 spec behavior)
oauth_metadata_request = await auth_flow.asend(prm_response_2)
@ -1649,7 +1649,7 @@ class TestLegacyServerFallback:
assert oauth_metadata_request.method == "GET"
# Send successful OAuth metadata response
oauth_metadata_response = httpx.Response(
oauth_metadata_response = httpx2.Response(
200,
content=(
b'{"issuer": "https://mcp.linear.app", '
@ -1669,7 +1669,7 @@ class TestLegacyServerFallback:
assert str(token_request.url) == "https://mcp.linear.app/token"
# Send successful token response
token_response = httpx.Response(
token_response = httpx2.Response(
200,
content=b'{"access_token": "linear_token", "token_type": "Bearer", "expires_in": 3600}',
request=token_request,
@ -1681,7 +1681,7 @@ class TestLegacyServerFallback:
assert str(final_request.url) == "https://mcp.linear.app/sse"
# Complete flow
final_response = httpx.Response(200, request=final_request)
final_response = httpx2.Response(200, request=final_request)
try:
await auth_flow.asend(final_response)
except StopAsyncIteration:
@ -1716,13 +1716,13 @@ class TestLegacyServerFallback:
redirect_uris=[AnyUrl("http://localhost:3030/callback")],
)
test_request = httpx.Request("GET", "https://api.example.com/v1/mcp")
test_request = httpx2.Request("GET", "https://api.example.com/v1/mcp")
auth_flow = provider.async_auth_flow(test_request)
await auth_flow.__anext__()
# 401 with custom WWW-Authenticate PRM URL
response = httpx.Response(
response = httpx2.Response(
401,
headers={
"WWW-Authenticate": 'Bearer resource_metadata="https://custom.prm.com/.well-known/oauth-protected-resource"'
@ -1735,28 +1735,28 @@ class TestLegacyServerFallback:
assert str(prm_request_1.url) == "https://custom.prm.com/.well-known/oauth-protected-resource"
# Returns 500
prm_response_1 = httpx.Response(500, request=prm_request_1)
prm_response_1 = httpx2.Response(500, request=prm_request_1)
# Try path-based fallback
prm_request_2 = await auth_flow.asend(prm_response_1)
assert str(prm_request_2.url) == "https://api.example.com/.well-known/oauth-protected-resource/v1/mcp"
# Returns 404
prm_response_2 = httpx.Response(404, request=prm_request_2)
prm_response_2 = httpx2.Response(404, request=prm_request_2)
# Try root fallback
prm_request_3 = await auth_flow.asend(prm_response_2)
assert str(prm_request_3.url) == "https://api.example.com/.well-known/oauth-protected-resource"
# Also returns 404 - all PRM URLs failed
prm_response_3 = httpx.Response(404, request=prm_request_3)
prm_response_3 = httpx2.Response(404, request=prm_request_3)
# Should fall back to root OAuth discovery
oauth_metadata_request = await auth_flow.asend(prm_response_3)
assert str(oauth_metadata_request.url) == "https://api.example.com/.well-known/oauth-authorization-server"
# Complete the flow
oauth_metadata_response = httpx.Response(
oauth_metadata_response = httpx2.Response(
200,
content=(
b'{"issuer": "https://api.example.com", '
@ -1773,7 +1773,7 @@ class TestLegacyServerFallback:
token_request = await auth_flow.asend(oauth_metadata_response)
assert str(token_request.url) == "https://api.example.com/token"
token_response = httpx.Response(
token_response = httpx2.Response(
200,
content=b'{"access_token": "test_token", "token_type": "Bearer", "expires_in": 3600}',
request=token_request,
@ -1782,7 +1782,7 @@ class TestLegacyServerFallback:
final_request = await auth_flow.asend(token_response)
assert final_request.headers["Authorization"] == "Bearer test_token"
final_response = httpx.Response(200, request=final_request)
final_response = httpx2.Response(200, request=final_request)
try:
await auth_flow.asend(final_response)
except StopAsyncIteration:
@ -1813,8 +1813,8 @@ class TestSEP985Discovery:
)
# Test with 401 response without WWW-Authenticate header
init_response = httpx.Response(
status_code=401, headers={}, request=httpx.Request("GET", "https://api.example.com/v1/mcp")
init_response = httpx2.Response(
status_code=401, headers={}, request=httpx2.Request("GET", "https://api.example.com/v1/mcp")
)
# Build discovery URLs
@ -1859,7 +1859,7 @@ class TestSEP985Discovery:
)
# Create a test request
test_request = httpx.Request("GET", "https://api.example.com/v1/mcp")
test_request = httpx2.Request("GET", "https://api.example.com/v1/mcp")
# Mock the auth flow
auth_flow = provider.async_auth_flow(test_request)
@ -1869,7 +1869,7 @@ class TestSEP985Discovery:
assert "Authorization" not in request.headers
# Send a 401 response without WWW-Authenticate header
response = httpx.Response(401, headers={}, request=test_request)
response = httpx2.Response(401, headers={}, request=test_request)
# Next request should be to discover protected resource metadata (path-based)
discovery_request_1 = await auth_flow.asend(response)
@ -1877,7 +1877,7 @@ class TestSEP985Discovery:
assert discovery_request_1.method == "GET"
# Send 404 response for path-based discovery
discovery_response_1 = httpx.Response(404, request=discovery_request_1)
discovery_response_1 = httpx2.Response(404, request=discovery_request_1)
# Next request should be to root-based well-known URI
discovery_request_2 = await auth_flow.asend(discovery_response_1)
@ -1885,7 +1885,7 @@ class TestSEP985Discovery:
assert discovery_request_2.method == "GET"
# Send successful discovery response
discovery_response_2 = httpx.Response(
discovery_response_2 = httpx2.Response(
200,
content=(
b'{"resource": "https://api.example.com/v1/mcp", "authorization_servers": ["https://auth.example.com"]}'
@ -1901,7 +1901,7 @@ class TestSEP985Discovery:
assert oauth_metadata_request.method == "GET"
# Complete the flow
oauth_metadata_response = httpx.Response(
oauth_metadata_response = httpx2.Response(
200,
content=(
b'{"issuer": "https://auth.example.com", '
@ -1912,7 +1912,7 @@ class TestSEP985Discovery:
)
token_request = await auth_flow.asend(oauth_metadata_response)
token_response = httpx.Response(
token_response = httpx2.Response(
200,
content=(
b'{"access_token": "new_access_token", "token_type": "Bearer", "expires_in": 3600, '
@ -1922,7 +1922,7 @@ class TestSEP985Discovery:
)
final_request = await auth_flow.asend(token_response)
final_response = httpx.Response(200, request=final_request)
final_response = httpx2.Response(200, request=final_request)
try:
await auth_flow.asend(final_response)
except StopAsyncIteration:
@ -1949,12 +1949,12 @@ class TestSEP985Discovery:
)
# Test with 401 response with WWW-Authenticate header
init_response = httpx.Response(
init_response = httpx2.Response(
status_code=401,
headers={
"WWW-Authenticate": 'Bearer resource_metadata="https://custom.example.com/.well-known/oauth-protected-resource"'
},
request=httpx.Request("GET", "https://api.example.com/v1/mcp"),
request=httpx2.Request("GET", "https://api.example.com/v1/mcp"),
)
# Build discovery URLs
@ -2028,10 +2028,10 @@ class TestWWWAuthenticate:
):
"""Test extraction of various fields from valid WWW-Authenticate headers."""
init_response = httpx.Response(
init_response = httpx2.Response(
status_code=401,
headers={"WWW-Authenticate": www_auth_header},
request=httpx.Request("GET", "https://api.example.com/test"),
request=httpx2.Request("GET", "https://api.example.com/test"),
)
result = extract_field_from_www_auth(init_response, field_name)
@ -2063,8 +2063,8 @@ class TestWWWAuthenticate:
"""Test extraction returns None for invalid cases."""
headers = {"WWW-Authenticate": www_auth_header} if www_auth_header is not None else {}
init_response = httpx.Response(
status_code=401, headers=headers, request=httpx.Request("GET", "https://api.example.com/test")
init_response = httpx2.Response(
status_code=401, headers=headers, request=httpx2.Request("GET", "https://api.example.com/test")
)
result = extract_field_from_www_auth(init_response, field_name)
@ -2211,7 +2211,7 @@ class TestCIMD:
provider.context.token_expiry_time = None
provider._initialized = True
test_request = httpx.Request("GET", "https://api.example.com/v1/mcp")
test_request = httpx2.Request("GET", "https://api.example.com/v1/mcp")
auth_flow = provider.async_auth_flow(test_request)
# First request
@ -2219,11 +2219,11 @@ class TestCIMD:
assert "Authorization" not in request.headers
# Send 401 response
response = httpx.Response(401, headers={}, request=test_request)
response = httpx2.Response(401, headers={}, request=test_request)
# PRM discovery
prm_request = await auth_flow.asend(response)
prm_response = httpx.Response(
prm_response = httpx2.Response(
200,
content=b'{"resource": "https://api.example.com/v1/mcp", "authorization_servers": ["https://auth.example.com"]}',
request=prm_request,
@ -2231,7 +2231,7 @@ class TestCIMD:
# OAuth metadata discovery
oauth_request = await auth_flow.asend(prm_response)
oauth_response = httpx.Response(
oauth_response = httpx2.Response(
200,
content=(
b'{"issuer": "https://auth.example.com", '
@ -2262,7 +2262,7 @@ class TestCIMD:
assert provider.context.client_info.token_endpoint_auth_method == "none"
# Complete the flow
token_response = httpx.Response(
token_response = httpx2.Response(
200,
content=b'{"access_token": "test_token", "token_type": "Bearer", "expires_in": 3600}',
request=token_request,
@ -2271,7 +2271,7 @@ class TestCIMD:
final_request = await auth_flow.asend(token_response)
assert final_request.headers["Authorization"] == "Bearer test_token"
final_response = httpx.Response(200, request=final_request)
final_response = httpx2.Response(200, request=final_request)
try:
await auth_flow.asend(final_response)
except StopAsyncIteration:
@ -2302,18 +2302,18 @@ class TestCIMD:
provider.context.token_expiry_time = None
provider._initialized = True
test_request = httpx.Request("GET", "https://api.example.com/v1/mcp")
test_request = httpx2.Request("GET", "https://api.example.com/v1/mcp")
auth_flow = provider.async_auth_flow(test_request)
# First request
await auth_flow.__anext__()
# Send 401 response
response = httpx.Response(401, headers={}, request=test_request)
response = httpx2.Response(401, headers={}, request=test_request)
# PRM discovery
prm_request = await auth_flow.asend(response)
prm_response = httpx.Response(
prm_response = httpx2.Response(
200,
content=b'{"resource": "https://api.example.com/v1/mcp", "authorization_servers": ["https://auth.example.com"]}',
request=prm_request,
@ -2321,7 +2321,7 @@ class TestCIMD:
# OAuth metadata discovery - server does NOT support CIMD
oauth_request = await auth_flow.asend(prm_response)
oauth_response = httpx.Response(
oauth_response = httpx2.Response(
200,
content=(
b'{"issuer": "https://auth.example.com", '
@ -2338,7 +2338,7 @@ class TestCIMD:
assert str(registration_request.url) == "https://auth.example.com/register"
# Complete the flow to avoid generator cleanup issues
registration_response = httpx.Response(
registration_response = httpx2.Response(
201,
content=b'{"client_id": "dcr_client_id", "redirect_uris": ["http://localhost:3030/callback"]}',
request=registration_request,
@ -2350,14 +2350,14 @@ class TestCIMD:
)
token_request = await auth_flow.asend(registration_response)
token_response = httpx.Response(
token_response = httpx2.Response(
200,
content=b'{"access_token": "test_token", "token_type": "Bearer", "expires_in": 3600}',
request=token_request,
)
final_request = await auth_flow.asend(token_response)
final_response = httpx.Response(200, request=final_request)
final_response = httpx2.Response(200, request=final_request)
try:
await auth_flow.asend(final_response)
except StopAsyncIteration:
@ -2541,7 +2541,7 @@ class TestSEP2207OfflineAccessScope:
redirect_uris=[AnyUrl("http://localhost:3030/callback")],
)
test_request = httpx.Request("GET", "https://api.example.com/v1/mcp")
test_request = httpx2.Request("GET", "https://api.example.com/v1/mcp")
auth_flow = provider.async_auth_flow(test_request)
# First request
@ -2549,11 +2549,11 @@ class TestSEP2207OfflineAccessScope:
assert "Authorization" not in request.headers
# Send 401
response = httpx.Response(401, headers={}, request=test_request)
response = httpx2.Response(401, headers={}, request=test_request)
# PRM discovery
prm_request = await auth_flow.asend(response)
prm_response = httpx.Response(
prm_response = httpx2.Response(
200,
content=(
b'{"resource": "https://api.example.com/v1/mcp",'
@ -2565,7 +2565,7 @@ class TestSEP2207OfflineAccessScope:
# OAuth metadata discovery - AS advertises offline_access
oauth_request = await auth_flow.asend(prm_response)
oauth_response = httpx.Response(
oauth_response = httpx2.Response(
200,
content=(
b'{"issuer": "https://auth.example.com",'
@ -2593,7 +2593,7 @@ class TestSEP2207OfflineAccessScope:
assert params["prompt"][0] == "consent"
# Complete the token exchange
token_response = httpx.Response(
token_response = httpx2.Response(
200,
content=(
b'{"access_token": "new_access_token", "token_type": "Bearer",'
@ -2606,7 +2606,7 @@ class TestSEP2207OfflineAccessScope:
assert final_request.headers["Authorization"] == "Bearer new_access_token"
# Close the generator
final_response = httpx.Response(200, request=final_request)
final_response = httpx2.Response(200, request=final_request)
try:
await auth_flow.asend(final_response)
except StopAsyncIteration:
@ -2650,18 +2650,18 @@ class TestSEP2207OfflineAccessScope:
redirect_uris=[AnyUrl("http://localhost:3030/callback")],
)
test_request = httpx.Request("GET", "https://api.example.com/v1/mcp")
test_request = httpx2.Request("GET", "https://api.example.com/v1/mcp")
auth_flow = provider.async_auth_flow(test_request)
# First request
await auth_flow.__anext__()
# Send 401
response = httpx.Response(401, headers={}, request=test_request)
response = httpx2.Response(401, headers={}, request=test_request)
# PRM discovery
prm_request = await auth_flow.asend(response)
prm_response = httpx.Response(
prm_response = httpx2.Response(
200,
content=(
b'{"resource": "https://api.example.com/v1/mcp",'
@ -2673,7 +2673,7 @@ class TestSEP2207OfflineAccessScope:
# OAuth metadata discovery - AS does NOT advertise offline_access
oauth_request = await auth_flow.asend(prm_response)
oauth_response = httpx.Response(
oauth_response = httpx2.Response(
200,
content=(
b'{"issuer": "https://auth.example.com",'
@ -2701,7 +2701,7 @@ class TestSEP2207OfflineAccessScope:
assert "prompt" not in params
# Complete the token exchange
token_response = httpx.Response(
token_response = httpx2.Response(
200,
content=b'{"access_token": "new_access_token", "token_type": "Bearer", "expires_in": 3600}',
request=token_request,
@ -2711,7 +2711,7 @@ class TestSEP2207OfflineAccessScope:
assert final_request.headers["Authorization"] == "Bearer new_access_token"
# Close the generator
final_response = httpx.Response(200, request=final_request)
final_response = httpx2.Response(200, request=final_request)
try:
await auth_flow.asend(final_response)
except StopAsyncIteration:
@ -2846,10 +2846,10 @@ async def test_handle_token_response_backfills_omitted_scope_from_request(
has reverted to its constructor value.
"""
oauth_provider.context.client_metadata.scope = "read admin"
response = httpx.Response(
response = httpx2.Response(
200,
json={"access_token": "t", "token_type": "Bearer", "expires_in": 3600},
request=httpx.Request("POST", "https://auth.example.com/token"),
request=httpx2.Request("POST", "https://auth.example.com/token"),
)
await oauth_provider._handle_token_response(response)
@ -2862,10 +2862,10 @@ async def test_handle_token_response_backfills_omitted_scope_from_request(
@pytest.mark.anyio
async def test_handle_token_response_raises_on_non_2xx_with_body(oauth_provider: OAuthClientProvider):
response = httpx.Response(
response = httpx2.Response(
400,
json={"error": "invalid_grant"},
request=httpx.Request("POST", "https://auth.example.com/token"),
request=httpx2.Request("POST", "https://auth.example.com/token"),
)
with pytest.raises(OAuthTokenError, match=r"Token exchange failed \(400\).*invalid_grant"):
await oauth_provider._handle_token_response(response)
@ -2884,10 +2884,10 @@ async def test_handle_refresh_response_carries_prior_scope_and_refresh_token_whe
oauth_provider.context.current_tokens = OAuthToken(
access_token="old", scope="read write", refresh_token="prior-refresh"
)
response = httpx.Response(
response = httpx2.Response(
200,
json={"access_token": "new", "token_type": "Bearer", "expires_in": 3600},
request=httpx.Request("POST", "https://auth.example.com/token"),
request=httpx2.Request("POST", "https://auth.example.com/token"),
)
ok = await oauth_provider._handle_refresh_response(response)
@ -2910,10 +2910,10 @@ async def test_handle_refresh_response_adopts_rotated_refresh_token_when_returne
oauth_provider.context.current_tokens = OAuthToken(
access_token="old", scope="read write", refresh_token="prior-refresh"
)
response = httpx.Response(
response = httpx2.Response(
200,
json={"access_token": "new", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "rotated"},
request=httpx.Request("POST", "https://auth.example.com/token"),
request=httpx2.Request("POST", "https://auth.example.com/token"),
)
ok = await oauth_provider._handle_refresh_response(response)
@ -2943,20 +2943,20 @@ async def test_issuer_binding_re_evaluated_after_asm_when_prm_discovery_failed(
issuer="https://old-as.example.com",
)
auth_flow = oauth_provider.async_auth_flow(httpx.Request("GET", "https://api.example.com/v1/mcp"))
auth_flow = oauth_provider.async_auth_flow(httpx2.Request("GET", "https://api.example.com/v1/mcp"))
request = await auth_flow.__anext__()
response_401 = httpx.Response(401, request=request)
response_401 = httpx2.Response(401, request=request)
# PRM discovery: path-based then root, both 404.
prm_req = await auth_flow.asend(response_401)
assert str(prm_req.url) == "https://api.example.com/.well-known/oauth-protected-resource/v1/mcp"
prm_req = await auth_flow.asend(httpx.Response(404, request=prm_req))
prm_req = await auth_flow.asend(httpx2.Response(404, request=prm_req))
assert str(prm_req.url) == "https://api.example.com/.well-known/oauth-protected-resource"
# ASM discovery via root fallback (no auth_server_url) succeeds with a different issuer.
asm_req = await auth_flow.asend(httpx.Response(404, request=prm_req))
asm_req = await auth_flow.asend(httpx2.Response(404, request=prm_req))
assert str(asm_req.url) == "https://api.example.com/.well-known/oauth-authorization-server"
asm_response = httpx.Response(
asm_response = httpx2.Response(
200,
content=(
b'{"issuer": "https://api.example.com", '
@ -2981,12 +2981,12 @@ async def test_issuer_binding_re_evaluated_after_asm_when_prm_discovery_failed(
"asm_responses",
[
pytest.param(
[httpx.Response(404), httpx.Response(404)],
[httpx2.Response(404), httpx2.Response(404)],
id="asm-discovery-failed",
),
pytest.param(
[
httpx.Response(
httpx2.Response(
200,
content=(
b'{"issuer": "https://new-as.example.com", '
@ -3000,7 +3000,7 @@ async def test_issuer_binding_re_evaluated_after_asm_when_prm_discovery_failed(
],
)
async def test_issuer_is_not_stamped_when_registration_falls_back_to_the_resource_origin(
oauth_provider: OAuthClientProvider, mock_storage: MockTokenStorage, asm_responses: list[httpx.Response]
oauth_provider: OAuthClientProvider, mock_storage: MockTokenStorage, asm_responses: list[httpx2.Response]
):
"""SEP-2352: a fallback registration is not recorded as bound to the PRM-advertised AS.
@ -3032,9 +3032,9 @@ async def test_issuer_is_not_stamped_when_registration_falls_back_to_the_resourc
oauth_provider.context.redirect_handler = capture_redirect
oauth_provider.context.callback_handler = echo_callback
auth_flow = oauth_provider.async_auth_flow(httpx.Request("GET", "https://api.example.com/v1/mcp"))
auth_flow = oauth_provider.async_auth_flow(httpx2.Request("GET", "https://api.example.com/v1/mcp"))
request = await auth_flow.__anext__()
response_401 = httpx.Response(
response_401 = httpx2.Response(
401,
headers={
"WWW-Authenticate": (
@ -3047,7 +3047,7 @@ async def test_issuer_is_not_stamped_when_registration_falls_back_to_the_resourc
# PRM succeeds and advertises a new AS — the discard block fires.
prm_req = await auth_flow.asend(response_401)
assert str(prm_req.url) == "https://api.example.com/.well-known/oauth-protected-resource"
prm_response = httpx.Response(
prm_response = httpx2.Response(
200,
content=(
b'{"resource": "https://api.example.com/v1/mcp", "authorization_servers": ["https://new-as.example.com"]}'
@ -3069,7 +3069,7 @@ async def test_issuer_is_not_stamped_when_registration_falls_back_to_the_resourc
dcr_req = next_req
assert dcr_req.method == "POST"
assert str(dcr_req.url) == "https://api.example.com/register"
dcr_response = httpx.Response(
dcr_response = httpx2.Response(
201,
json={"client_id": "fallback-client", "redirect_uris": ["http://localhost:3030/callback"]},
request=dcr_req,
@ -3083,12 +3083,12 @@ async def test_issuer_is_not_stamped_when_registration_falls_back_to_the_resourc
assert stored.issuer is None
# Drive the flow to completion so the context lock is released cleanly.
token_response = httpx.Response(
token_response = httpx2.Response(
200, json={"access_token": "t", "token_type": "Bearer", "expires_in": 3600}, request=token_req
)
final_req = await auth_flow.asend(token_response)
try:
await auth_flow.asend(httpx.Response(200, request=final_req))
await auth_flow.asend(httpx2.Response(200, request=final_req))
except StopAsyncIteration:
pass
@ -3121,19 +3121,19 @@ async def test_issuer_is_stamped_when_same_origin_fallback_register_is_on_the_di
oauth_provider.context.redirect_handler = capture_redirect
oauth_provider.context.callback_handler = echo_callback
auth_flow = oauth_provider.async_auth_flow(httpx.Request("GET", "https://api.example.com/v1/mcp"))
auth_flow = oauth_provider.async_auth_flow(httpx2.Request("GET", "https://api.example.com/v1/mcp"))
request = await auth_flow.__anext__()
# PRM discovery 404s on both well-known URLs.
prm_req = await auth_flow.asend(httpx.Response(401, request=request))
prm_req = await auth_flow.asend(httpx2.Response(401, request=request))
assert str(prm_req.url) == "https://api.example.com/.well-known/oauth-protected-resource/v1/mcp"
prm_req = await auth_flow.asend(httpx.Response(404, request=prm_req))
prm_req = await auth_flow.asend(httpx2.Response(404, request=prm_req))
assert str(prm_req.url) == "https://api.example.com/.well-known/oauth-protected-resource"
# Root ASM discovery succeeds with the resource origin as issuer and no registration_endpoint.
asm_req = await auth_flow.asend(httpx.Response(404, request=prm_req))
asm_req = await auth_flow.asend(httpx2.Response(404, request=prm_req))
assert str(asm_req.url) == "https://api.example.com/.well-known/oauth-authorization-server"
asm_response = httpx.Response(
asm_response = httpx2.Response(
200,
content=(
b'{"issuer": "https://api.example.com", '
@ -3147,7 +3147,7 @@ async def test_issuer_is_stamped_when_same_origin_fallback_register_is_on_the_di
dcr_req = await auth_flow.asend(asm_response)
assert dcr_req.method == "POST"
assert str(dcr_req.url) == "https://api.example.com/register"
dcr_response = httpx.Response(
dcr_response = httpx2.Response(
201,
json={"client_id": "embedded-client", "redirect_uris": ["http://localhost:3030/callback"]},
request=dcr_req,
@ -3161,11 +3161,11 @@ async def test_issuer_is_stamped_when_same_origin_fallback_register_is_on_the_di
assert stored.issuer == str(oauth_provider.context.oauth_metadata.issuer)
assert urlparse(stored.issuer).netloc == "api.example.com"
token_response = httpx.Response(
token_response = httpx2.Response(
200, json={"access_token": "t", "token_type": "Bearer", "expires_in": 3600}, request=token_req
)
final_req = await auth_flow.asend(token_response)
try:
await auth_flow.asend(httpx.Response(200, request=final_req))
await auth_flow.asend(httpx2.Response(200, request=final_req))
except StopAsyncIteration:
pass