From f0380915814acd4e0df34cc258bd6cd5ed67e2c5 Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Thu, 10 Sep 2026 18:03:15 +0200 Subject: [PATCH 1/3] fix: give each request configuration and headers inspection handler its own HeadersCollection RequestConfiguration.headers, HeadersInspectionHandlerOption's two collections and HeadersInspectionHandler's option object were all defaults evaluated once, so every instance built without an explicit value shared them and headers leaked between unrelated requests. --- .../base_request_configuration.py | 4 +-- .../tests/test_base_request_configuration.py | 16 +++++++++- .../middleware/headers_inspection_handler.py | 8 +++-- .../headers_inspection_handler_option.py | 18 ++++++++--- .../test_headers_inspection_handler.py | 30 +++++++++++++++++++ 5 files changed, 66 insertions(+), 10 deletions(-) diff --git a/packages/abstractions/kiota_abstractions/base_request_configuration.py b/packages/abstractions/kiota_abstractions/base_request_configuration.py index e15293f1..ebfbdc84 100644 --- a/packages/abstractions/kiota_abstractions/base_request_configuration.py +++ b/packages/abstractions/kiota_abstractions/base_request_configuration.py @@ -3,7 +3,7 @@ # Licensed under the MIT License. # See License in the project root for license information. # ------------------------------------ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Generic, Optional, TypeVar from warnings import warn @@ -19,7 +19,7 @@ class RequestConfiguration(Generic[QueryParameters]): Configuration for the request such as headers, query parameters, and middleware options. """ # Request headers - headers: HeadersCollection = HeadersCollection() + headers: HeadersCollection = field(default_factory=HeadersCollection) # Request options options: Optional[list[RequestOption]] = None # Request query parameters diff --git a/packages/abstractions/tests/test_base_request_configuration.py b/packages/abstractions/tests/test_base_request_configuration.py index 1a274e6e..fe80bf25 100644 --- a/packages/abstractions/tests/test_base_request_configuration.py +++ b/packages/abstractions/tests/test_base_request_configuration.py @@ -1,6 +1,9 @@ import pytest -from kiota_abstractions.base_request_configuration import BaseRequestConfiguration +from kiota_abstractions.base_request_configuration import ( + BaseRequestConfiguration, + RequestConfiguration, +) def test_base_request_configuration_deprecation_warning(): @@ -11,3 +14,14 @@ def test_base_request_configuration_deprecation_warning(): def test_import_base_request_configuration_no_warning(): from kiota_abstractions.base_request_configuration import BaseRequestConfiguration, RequestConfiguration assert len(pytest.warns()) == 0 + + +def test_request_configurations_do_not_share_a_headers_collection(): + first = RequestConfiguration() + second = RequestConfiguration() + + first.headers.add("Prefer", "outlook.body-content-type=text") + + assert first.headers is not second.headers + assert second.headers.try_get("Prefer") is False + assert RequestConfiguration().headers.count() == 0 diff --git a/packages/http/httpx/kiota_http/middleware/headers_inspection_handler.py b/packages/http/httpx/kiota_http/middleware/headers_inspection_handler.py index 85cc8b51..81b03f40 100644 --- a/packages/http/httpx/kiota_http/middleware/headers_inspection_handler.py +++ b/packages/http/httpx/kiota_http/middleware/headers_inspection_handler.py @@ -4,6 +4,8 @@ # See License in the project root for license information. # ------------------------------------ +from typing import Optional + from kiota_abstractions.request_option import RequestOption import httpx @@ -21,16 +23,16 @@ class HeadersInspectionHandler(BaseMiddleware): def __init__( self, - options: HeadersInspectionHandlerOption = HeadersInspectionHandlerOption(), + options: Optional[HeadersInspectionHandlerOption] = None, ): """Create an instance of HeadersInspectionHandler Args: options (HeadersInspectionHandlerOption, optional): Default options to apply to the - handler. Defaults to HeadersInspectionHandlerOption(). + handler. A new HeadersInspectionHandlerOption per handler when not provided. """ super().__init__() - self.options = options + self.options = options if options is not None else HeadersInspectionHandlerOption() async def send( self, request: httpx.Request, transport: httpx.AsyncBaseTransport diff --git a/packages/http/httpx/kiota_http/middleware/options/headers_inspection_handler_option.py b/packages/http/httpx/kiota_http/middleware/options/headers_inspection_handler_option.py index 09feb2af..f8306cb9 100644 --- a/packages/http/httpx/kiota_http/middleware/options/headers_inspection_handler_option.py +++ b/packages/http/httpx/kiota_http/middleware/options/headers_inspection_handler_option.py @@ -3,6 +3,8 @@ # Licensed under the MIT License. # See License in the project root for license information. # ------------------------------------ +from typing import Optional + from kiota_abstractions.headers_collection import HeadersCollection from kiota_abstractions.request_option import RequestOption @@ -16,8 +18,8 @@ def __init__( self, inspect_request_headers: bool = True, inspect_response_headers: bool = True, - request_headers: HeadersCollection = HeadersCollection(), - response_headers: HeadersCollection = HeadersCollection(), + request_headers: Optional[HeadersCollection] = None, + response_headers: Optional[HeadersCollection] = None, ) -> None: """Creates an instance of headers inspection handler option. @@ -26,11 +28,19 @@ def __init__( should be inspected. Defaults to True. inspect_response_headers (bool, optional): whether the response headers should be inspected. Defaults to True. + request_headers (HeadersCollection, optional): collection that receives the + request headers. A new one per option when not provided. + response_headers (HeadersCollection, optional): collection that receives the + response headers. A new one per option when not provided. """ self._inspect_request_headers = inspect_request_headers self._inspect_response_headers = inspect_response_headers - self._request_headers = request_headers if request_headers else HeadersCollection() - self._response_headers = response_headers if response_headers else HeadersCollection() + if request_headers is None: + request_headers = HeadersCollection() + if response_headers is None: + response_headers = HeadersCollection() + self._request_headers = request_headers + self._response_headers = response_headers @property def inspect_request_headers(self): diff --git a/packages/http/httpx/tests/middleware_tests/test_headers_inspection_handler.py b/packages/http/httpx/tests/middleware_tests/test_headers_inspection_handler.py index d47b9442..d0704140 100644 --- a/packages/http/httpx/tests/middleware_tests/test_headers_inspection_handler.py +++ b/packages/http/httpx/tests/middleware_tests/test_headers_inspection_handler.py @@ -19,6 +19,36 @@ def test_no_config(): assert isinstance(options.request_headers, HeadersCollection) +def test_options_do_not_share_header_collections(): + """ + Two options built without explicit collections must not see each other's headers. + """ + first = HeadersInspectionHandlerOption() + second = HeadersInspectionHandlerOption() + + first.request_headers.add('test_request', 'test_request_header') + first.response_headers.add('test_response', 'test_response_header') + + assert first.request_headers is not second.request_headers + assert first.response_headers is not second.response_headers + assert second.request_headers.try_get('test_request') is False + assert second.response_headers.try_get('test_response') is False + + +def test_handlers_do_not_share_options(): + """ + Two handlers built without explicit options must not share one option object, + or the headers one client inspects show up on, and get cleared by, another. + """ + first = HeadersInspectionHandler() + second = HeadersInspectionHandler() + + first.options.request_headers.add('test_request', 'test_request_header') + + assert first.options is not second.options + assert second.options.request_headers.try_get('test_request') is False + + def test_custom_config(): """ Ensures that setting is_enabled to False. From 4e542705dbb1519d30ff6e3e3bc78cf2371fbe7b Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Thu, 10 Sep 2026 19:45:58 +0200 Subject: [PATCH 2/3] fix: make HeadersInspectionHandlerOption a dataclass with default_factory collections Same fix as the RequestConfiguration half, spelled the same way. --- .../headers_inspection_handler_option.py | 90 +++++-------------- 1 file changed, 22 insertions(+), 68 deletions(-) diff --git a/packages/http/httpx/kiota_http/middleware/options/headers_inspection_handler_option.py b/packages/http/httpx/kiota_http/middleware/options/headers_inspection_handler_option.py index f8306cb9..b1016901 100644 --- a/packages/http/httpx/kiota_http/middleware/options/headers_inspection_handler_option.py +++ b/packages/http/httpx/kiota_http/middleware/options/headers_inspection_handler_option.py @@ -3,80 +3,34 @@ # Licensed under the MIT License. # See License in the project root for license information. # ------------------------------------ -from typing import Optional +from dataclasses import dataclass, field +from typing import ClassVar from kiota_abstractions.headers_collection import HeadersCollection from kiota_abstractions.request_option import RequestOption +@dataclass(eq=False) class HeadersInspectionHandlerOption(RequestOption): - """Config options for the HeaderInspectionHandler""" - - HEADERS_INSPECTION_HANDLER_OPTION_KEY = "HeadersInspectionHandlerOption" - - def __init__( - self, - inspect_request_headers: bool = True, - inspect_response_headers: bool = True, - request_headers: Optional[HeadersCollection] = None, - response_headers: Optional[HeadersCollection] = None, - ) -> None: - """Creates an instance of headers inspection handler option. - - Args: - inspect_request_headers (bool, optional): whether the request headers - should be inspected. Defaults to True. - inspect_response_headers (bool, optional): whether the response headers - should be inspected. Defaults to True. - request_headers (HeadersCollection, optional): collection that receives the - request headers. A new one per option when not provided. - response_headers (HeadersCollection, optional): collection that receives the - response headers. A new one per option when not provided. - """ - self._inspect_request_headers = inspect_request_headers - self._inspect_response_headers = inspect_response_headers - if request_headers is None: - request_headers = HeadersCollection() - if response_headers is None: - response_headers = HeadersCollection() - self._request_headers = request_headers - self._response_headers = response_headers - - @property - def inspect_request_headers(self): - """Whether the request headers should be inspected.""" - return self._inspect_request_headers - - @inspect_request_headers.setter - def inspect_request_headers(self, value: bool): - self._inspect_request_headers = value - - @property - def inspect_response_headers(self): - """Whether the response headers should be inspected.""" - return self._inspect_response_headers - - @inspect_response_headers.setter - def inspect_response_headers(self, value: bool): - self._inspect_response_headers = value - - @property - def request_headers(self): - """Gets the request headers to for the current request.""" - return self._request_headers - - @request_headers.setter - def request_headers(self, value: HeadersCollection): - self._request_headers = value - - @property - def response_headers(self): - """Gets the response headers to for the current request.""" - return self._response_headers - - @response_headers.setter - def response_headers(self, value: HeadersCollection): - self._response_headers = value + """Config options for the HeaderInspectionHandler. + + Args: + inspect_request_headers (bool, optional): whether the request headers + should be inspected. Defaults to True. + inspect_response_headers (bool, optional): whether the response headers + should be inspected. Defaults to True. + request_headers (HeadersCollection, optional): collection that receives the + request headers. A new one per option when not provided. + response_headers (HeadersCollection, optional): collection that receives the + response headers. A new one per option when not provided. + """ + + HEADERS_INSPECTION_HANDLER_OPTION_KEY: ClassVar[str] = "HeadersInspectionHandlerOption" + + inspect_request_headers: bool = True + inspect_response_headers: bool = True + request_headers: HeadersCollection = field(default_factory=HeadersCollection) + response_headers: HeadersCollection = field(default_factory=HeadersCollection) @staticmethod def get_key() -> str: From 27890676799f8731168bf19e50fa73fd5700c4d5 Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Thu, 10 Sep 2026 20:04:25 +0200 Subject: [PATCH 3/3] test: assert the request configuration module imports without warnings The test reloaded nothing and len(pytest.warns()) recorded nothing, so it passed whatever the module did on import. Also name the handler correctly in the option docstring. --- .../tests/test_base_request_configuration.py | 9 +++++++-- .../options/headers_inspection_handler_option.py | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/abstractions/tests/test_base_request_configuration.py b/packages/abstractions/tests/test_base_request_configuration.py index fe80bf25..e40166ab 100644 --- a/packages/abstractions/tests/test_base_request_configuration.py +++ b/packages/abstractions/tests/test_base_request_configuration.py @@ -1,5 +1,9 @@ +import importlib +import warnings + import pytest +from kiota_abstractions import base_request_configuration from kiota_abstractions.base_request_configuration import ( BaseRequestConfiguration, RequestConfiguration, @@ -12,8 +16,9 @@ def test_base_request_configuration_deprecation_warning(): def test_import_base_request_configuration_no_warning(): - from kiota_abstractions.base_request_configuration import BaseRequestConfiguration, RequestConfiguration - assert len(pytest.warns()) == 0 + with warnings.catch_warnings(): + warnings.simplefilter("error") + importlib.reload(base_request_configuration) def test_request_configurations_do_not_share_a_headers_collection(): diff --git a/packages/http/httpx/kiota_http/middleware/options/headers_inspection_handler_option.py b/packages/http/httpx/kiota_http/middleware/options/headers_inspection_handler_option.py index b1016901..1c88a975 100644 --- a/packages/http/httpx/kiota_http/middleware/options/headers_inspection_handler_option.py +++ b/packages/http/httpx/kiota_http/middleware/options/headers_inspection_handler_option.py @@ -12,7 +12,7 @@ @dataclass(eq=False) class HeadersInspectionHandlerOption(RequestOption): - """Config options for the HeaderInspectionHandler. + """Config options for the HeadersInspectionHandler. Args: inspect_request_headers (bool, optional): whether the request headers