Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/msgraph_core/middleware/async_graph_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
77 changes: 77 additions & 0 deletions tests/middleware/test_async_graph_transport.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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')