From b8d5927c651c5e049a132021e72f8d01c83d5021 Mon Sep 17 00:00:00 2001 From: RKS Date: Mon, 21 Sep 2026 22:48:59 -0400 Subject: [PATCH 1/2] fix: honor Kiota request extensions in Graph transport --- .../middleware/async_graph_transport.py | 11 ++- .../middleware/test_async_graph_transport.py | 77 +++++++++++++++++++ 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/src/msgraph_core/middleware/async_graph_transport.py b/src/msgraph_core/middleware/async_graph_transport.py index bb81ff69..a6c25e01 100644 --- a/src/msgraph_core/middleware/async_graph_transport.py +++ b/src/msgraph_core/middleware/async_graph_transport.py @@ -6,6 +6,9 @@ from .._enums import FeatureUsageFlag from .request_context import GraphRequestContext +# Older supported Kiota releases do not export this request extension key. +REQUEST_OPTIONS_KEY = 'kiota_request_options' + class AsyncGraphTransport(httpx.AsyncBaseTransport): """A custom transport for requests to the Microsoft Graph API @@ -16,7 +19,9 @@ def __init__(self, transport: httpx.AsyncBaseTransport, pipeline: MiddlewarePipe self.pipeline = pipeline async def handle_async_request(self, request: httpx.Request) -> httpx.Response: - if self.pipeline and hasattr(request, 'options'): + if self.pipeline and ( + REQUEST_OPTIONS_KEY in request.extensions or hasattr(request, 'options') + ): self.set_request_context_and_feature_usage(request) response = await self.pipeline.send(request) return response @@ -26,7 +31,9 @@ async def handle_async_request(self, request: httpx.Request) -> httpx.Response: def set_request_context_and_feature_usage(self, request: httpx.Request) -> httpx.Request: - request_options = request.options # type:ignore + request_options = request.extensions.get(REQUEST_OPTIONS_KEY) + if request_options is None: + request_options = request.options # type:ignore context = GraphRequestContext(request_options, request.headers) middleware = self.pipeline._first_middleware diff --git a/tests/middleware/test_async_graph_transport.py b/tests/middleware/test_async_graph_transport.py index 38e1bca7..025f6495 100644 --- a/tests/middleware/test_async_graph_transport.py +++ b/tests/middleware/test_async_graph_transport.py @@ -1,8 +1,17 @@ +import asyncio + +import httpx import pytest +from kiota_abstractions.authentication import AnonymousAuthenticationProvider +from kiota_abstractions.method import Method +from kiota_abstractions.request_information import RequestInformation +from kiota_http.httpx_request_adapter import HttpxRequestAdapter from kiota_http.kiota_client_factory import KiotaClientFactory from msgraph_core._enums import FeatureUsageFlag +from msgraph_core.graph_client_factory import GraphClientFactory from msgraph_core.middleware import AsyncGraphTransport, GraphRequestContext +from msgraph_core.middleware.async_graph_transport import REQUEST_OPTIONS_KEY def test_set_request_context_and_feature_usage(mock_request, mock_transport): @@ -16,3 +25,71 @@ def test_set_request_context_and_feature_usage(mock_request, mock_transport): assert mock_request.context.feature_usage == hex( FeatureUsageFlag.RETRY_HANDLER_ENABLED | FeatureUsageFlag.REDIRECT_HANDLER_ENABLED ) + + +@pytest.mark.parametrize( + 'content_type', [ + 'application/octet-stream', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + ] +) +def test_binary_download_follows_redirect_with_kiota_request_extensions(content_type): + calls = [] + contexts = [] + + def handle_request(request): + calls.append(str(request.url)) + contexts.append(request.context) + if request.url.host == 'graph.example': + return httpx.Response(302, headers={'Location': 'https://download.example/file'}) + return httpx.Response( + 200, content=b'binary content', headers={'Content-Type': content_type} + ) + + async def download(): + client = GraphClientFactory.create_with_default_middleware( + client=httpx.AsyncClient(transport=httpx.MockTransport(handle_request)) + ) + try: + adapter = HttpxRequestAdapter(AnonymousAuthenticationProvider(), http_client=client) + request_info = RequestInformation() + request_info.http_method = Method.GET + request_info.url = 'https://graph.example/drive/item/content' + return await adapter.send_primitive_async(request_info, 'bytes', {}) + finally: + await client.aclose() + + assert asyncio.run(download()) == b'binary content' + assert calls == ['https://graph.example/drive/item/content', 'https://download.example/file'] + assert all(isinstance(context, GraphRequestContext) for context in contexts) + + +def test_extension_options_take_precedence_over_legacy_attribute(mock_transport): + middleware = KiotaClientFactory.get_default_middleware(None) + pipeline = KiotaClientFactory.create_middleware_pipeline(middleware, mock_transport) + transport = AsyncGraphTransport(mock_transport, pipeline) + request = httpx.Request('GET', 'https://example.org', extensions={REQUEST_OPTIONS_KEY: {}}) + request.options = {'legacy': True} + + transport.set_request_context_and_feature_usage(request) + + assert request.context.middleware_control == {} + + +def test_request_without_options_bypasses_graph_pipeline(): + calls = [] + + def handle_request(request): + calls.append(request) + return httpx.Response(200, content=b'body') + + async def send(): + underlying_transport = httpx.MockTransport(handle_request) + middleware = KiotaClientFactory.get_default_middleware(None) + pipeline = KiotaClientFactory.create_middleware_pipeline(middleware, underlying_transport) + transport = AsyncGraphTransport(underlying_transport, pipeline) + return await transport.handle_async_request(httpx.Request('GET', 'https://example.org')) + + assert asyncio.run(send()).status_code == 200 + assert len(calls) == 1 + assert not hasattr(calls[0], 'context') From e78dcaf3b669643e076433d697aa3bd3f38580a6 Mon Sep 17 00:00:00 2001 From: RKS Date: Mon, 5 Oct 2026 22:25:40 -0400 Subject: [PATCH 2/2] fix: align Kiota minimum versions and test extension-only transport --- pyproject.toml | 6 ++--- requirements-dev.txt | 8 +++--- .../middleware/test_async_graph_transport.py | 25 +++++++++++++++++++ 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cd12b5ba..67fccde6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,9 +11,9 @@ version = "1.5.1" authors = [{name = "Microsoft", email = "graphtooling+python@microsoft.com"}] description = "Core component of the Microsoft Graph Python SDK" dependencies = [ - "microsoft-kiota-abstractions >=1.11.6,<2.0.0", - "microsoft-kiota-authentication-azure >=1.11.6,<2.0.0", - "microsoft-kiota-http >=1.11.6,<2.0.0", + "microsoft-kiota-abstractions >=1.13.0,<2.0.0", + "microsoft-kiota-authentication-azure >=1.13.0,<2.0.0", + "microsoft-kiota-http >=1.13.0,<2.0.0", "httpx[http2] >=0.23.0", ] requires-python = ">=3.10" diff --git a/requirements-dev.txt b/requirements-dev.txt index d108450a..b0ea8ee5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -144,13 +144,13 @@ httpx[http2]==0.28.1 hyperframe==6.1.0 ; python_full_version >= '3.6.1' -microsoft-kiota-abstractions==1.11.6 +microsoft-kiota-abstractions==1.13.0 -microsoft-kiota-authentication-azure==1.11.6 +microsoft-kiota-authentication-azure==1.13.0 -microsoft-kiota-http==1.11.6 +microsoft-kiota-http==1.13.0 -microsoft-kiota-serialization-json==1.11.6 +microsoft-kiota-serialization-json==1.13.0 multidict==6.7.1 ; python_version >= '3.7' diff --git a/tests/middleware/test_async_graph_transport.py b/tests/middleware/test_async_graph_transport.py index 025f6495..3c44bd04 100644 --- a/tests/middleware/test_async_graph_transport.py +++ b/tests/middleware/test_async_graph_transport.py @@ -93,3 +93,28 @@ async def send(): assert asyncio.run(send()).status_code == 200 assert len(calls) == 1 assert not hasattr(calls[0], 'context') + + +def test_extension_only_request_uses_graph_pipeline(): + requests = [] + + def handle_request(request): + requests.append(request) + return httpx.Response(200, content=b'body') + + async def send(): + underlying_transport = httpx.MockTransport(handle_request) + middleware = KiotaClientFactory.get_default_middleware(None) + pipeline = KiotaClientFactory.create_middleware_pipeline(middleware, underlying_transport) + transport = AsyncGraphTransport(underlying_transport, pipeline) + request = httpx.Request('GET', 'https://example.org', extensions={REQUEST_OPTIONS_KEY: {}}) + assert not hasattr(request, 'options') + return await transport.handle_async_request(request) + + assert asyncio.run(send()).content == b'body' + assert len(requests) == 1 + assert isinstance(requests[0].context, GraphRequestContext) + assert requests[0].context.middleware_control == {} + assert requests[0].context.feature_usage == hex( + FeatureUsageFlag.RETRY_HANDLER_ENABLED | FeatureUsageFlag.REDIRECT_HANDLER_ENABLED + )