Skip to content
This repository was archived by the owner on Oct 14, 2024. It is now read-only.

Bugfix/middleware handlers #300

Merged
merged 6 commits into from
Feb 21, 2024
Merged
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.3.1] - 2024-02-13

### Added

### Changed
- Bugfix issues with middleware maintaining state across requests.[#281](https://github.com/microsoft/kiota-http-python/issues/281)
- Fix issue with redirect handler not closing old responses.[#299](https://github.com/microsoft/kiota-http-python/issues/299)

## [1.3.0] - 2024-02-08

### Added
Expand Down
2 changes: 1 addition & 1 deletion kiota_http/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
VERSION: str = '1.3.0'
VERSION: str = '1.3.1'
13 changes: 10 additions & 3 deletions kiota_http/middleware/headers_inspection_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,16 @@ def _get_current_options(self, request: httpx.Request) -> HeadersInspectionHandl
Returns:
HeadersInspectionHandlerOption: The options to be used.
"""
if options := getattr(request, "options", None):
current_options = options.get( # type:ignore
HeadersInspectionHandlerOption.get_key(), self.options
current_options = None
request_options = getattr(request, "options", None)
if request_options:
current_options = request_options.get( # type:ignore
HeadersInspectionHandlerOption.get_key(), None
)
if current_options:
return current_options

# Clear headers per request
self.options.request_headers.clear()
self.options.response_headers.clear()
return self.options
Original file line number Diff line number Diff line change
Expand Up @@ -18,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: HeadersCollection = None,
response_headers: HeadersCollection = None,
) -> None:
"""Creates an instance of headers inspection handler option.

Expand All @@ -31,8 +31,8 @@ def __init__(
"""
self._inspect_request_headers = inspect_request_headers
self._inspect_response_headers = inspect_response_headers
self._request_headers = request_headers
self._response_headers = response_headers
self._request_headers = request_headers if request_headers else HeadersCollection()
self._response_headers = response_headers if response_headers else HeadersCollection()

@property
def inspect_request_headers(self):
Expand Down
5 changes: 3 additions & 2 deletions kiota_http/middleware/parameters_name_decoding_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,9 @@ def _get_current_options(self, request: httpx.Request) -> ParametersNameDecoding
Returns:
ParametersNameDecodingHandlerOption: The options to used.
"""
if options := getattr(request, "options", None):
current_options = options.get( # type:ignore
request_options = getattr(request, "options", None)
if request_options:
current_options = request_options.get( # type:ignore
ParametersNameDecodingHandlerOption.get_key(), self.options
)
return current_options
Expand Down
39 changes: 26 additions & 13 deletions kiota_http/middleware/redirect_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class RedirectHandler(BaseMiddleware):
301, # Moved Permanently
302, # Found
303, # See Other
307, # Temporary Permanently
307, # Temporary Redirect
308, # Moved Permanently
}
STATUS_CODE_SEE_OTHER: int = 303
Expand Down Expand Up @@ -83,13 +83,13 @@ async def send(
if not self.increment(response, max_redirect, history[:]):
break
_redirect_span.set_attribute(REDIRECT_COUNT_KEY, len(history))
new_request = self._build_redirect_request(request, response)
new_request = self._build_redirect_request(request, response, current_options)
history.append(request)
request = new_request
await response.aclose()
continue

response.history = history
break

response.history = history
if max_redirect < 0:
exc = RedirectError(f"Too many redirects. {response.history}")
_redirect_span.record_exception(exc)
Expand All @@ -108,19 +108,22 @@ def _get_current_options(self, request: httpx.Request) -> RedirectHandlerOption:
Returns:
RedirectHandlerOption: The options to used.
"""
current_options = request.options.get( # type:ignore
RedirectHandlerOption.get_key(), self.options)
return current_options
request_options = getattr(request, "options", None)
if request_options:
current_options = request_options.get( # type:ignore
RedirectHandlerOption.get_key(), self.options)
return current_options
return self.options

def _build_redirect_request(
self, request: httpx.Request, response: httpx.Response
self, request: httpx.Request, response: httpx.Response, options: RedirectHandlerOption
) -> httpx.Request:
"""
Given a request and a redirect response, return a new request that
should be used to effect the redirect.
"""
method = self._redirect_method(request, response)
url = self._redirect_url(request, response)
url = self._redirect_url(request, response, options)
headers = self._redirect_headers(request, url, method)
stream = self._redirect_stream(request, method)
new_request = httpx.Request(
Expand All @@ -130,7 +133,8 @@ def _build_redirect_request(
stream=stream,
extensions=request.extensions,
)
new_request.context = request.context #type: ignore
if hasattr(request, "context"):
new_request.context = request.context #type: ignore
new_request.options = {} #type: ignore
return new_request

Expand All @@ -142,7 +146,7 @@ def _redirect_method(self, request: httpx.Request, response: httpx.Response) ->
method = request.method

# https://tools.ietf.org/html/rfc7231#section-6.4.4
if response.status_code == 303 and method != "HEAD":
if response.status_code == self.STATUS_CODE_SEE_OTHER and method != "HEAD":
method = "GET"

# Do what the browsers do, despite standards...
Expand All @@ -157,7 +161,9 @@ def _redirect_method(self, request: httpx.Request, response: httpx.Response) ->

return method

def _redirect_url(self, request: httpx.Request, response: httpx.Response) -> httpx.URL:
def _redirect_url(
self, request: httpx.Request, response: httpx.Response, options: RedirectHandlerOption
) -> httpx.URL:
"""
Return the URL for the redirect to follow.
"""
Expand All @@ -168,6 +174,13 @@ def _redirect_url(self, request: httpx.Request, response: httpx.Response) -> htt
except Exception as exc:
raise Exception(f"Invalid URL in location header: {exc}.")

if url.scheme != request.url.scheme and not options.allow_redirect_on_scheme_change:
raise Exception(
"Redirects with changing schemes not allowed by default.\
You can change this by modifying the allow_redirect_on_scheme_change\
request option."
)

# Handle malformed 'Location' headers that are "absolute" form, have no host.
# See: https://github.com/encode/httpx/issues/771
if url.scheme and not url.host:
Expand Down
14 changes: 9 additions & 5 deletions kiota_http/middleware/retry_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ async def send(self, request: httpx.Request, transport: httpx.AsyncBaseTransport
_span.set_attribute("com.microsoft.kiota.handler.retry.enable", True)
_span.end()
retry_valid = current_options.should_retry
max_delay = current_options.max_delay
_retry_span = self._create_observability_span(
request, f"RetryHandler_send - attempt {retry_count}"
)
Expand All @@ -91,10 +92,10 @@ async def send(self, request: httpx.Request, transport: httpx.AsyncBaseTransport
# Check if the request needs to be retried based on the response method
# and status code
should_retry = self.should_retry(request, current_options, response)
if all([should_retry, retry_valid, delay < current_options.max_delay]):
if all([should_retry, retry_valid, delay < max_delay]):
time.sleep(delay)
end_time = time.time()
current_options.max_delay -= (end_time - start_time)
max_delay -= (end_time - start_time)
# increment the count for retries
retry_count += 1
request.headers.update({'retry-attempt': f'{retry_count}'})
Expand All @@ -116,9 +117,12 @@ def _get_current_options(self, request: httpx.Request) -> RetryHandlerOption:
Returns:
RetryHandlerOption: The options to used.
"""
current_options = request.options.get( # type:ignore
RetryHandlerOption.get_key(), self.options)
return current_options
request_options = getattr(request, "options", None)
if request_options:
current_options = request_options.get( # type:ignore
RetryHandlerOption.get_key(), self.options)
return current_options
return self.options

def should_retry(self, request, options, response):
"""
Expand Down
11 changes: 7 additions & 4 deletions kiota_http/middleware/url_replace_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,13 @@ def _get_current_options(self, request: httpx.Request) -> UrlReplaceHandlerOptio
Returns:
UrlReplaceHandlerOption: The options to be used.
"""
current_options = request.options.get( # type:ignore
UrlReplaceHandlerOption.get_key(), self.options
)
return current_options
request_options = getattr(request, "options", None)
if request_options:
current_options = request.options.get( # type:ignore
UrlReplaceHandlerOption.get_key(), self.options
)
return current_options
return self.options

def replace_url_segment(self, url_str: str, current_options: UrlReplaceHandlerOption) -> str:
if all([current_options, current_options.is_enabled, current_options.replacement_pairs]):
Expand Down
21 changes: 20 additions & 1 deletion kiota_http/middleware/user_agent_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,33 @@ async def send(self, request: Request, transport: AsyncBaseTransport) -> Respons
Checks if the request has a User-Agent header and updates it if the
platform config allows.
"""
current_options = self._get_current_options(request)
_span = self._create_observability_span(request, "UserAgentHandler_send")
if self.options and self.options.is_enabled:
if current_options and current_options.is_enabled:
_span.set_attribute("com.microsoft.kiota.handler.useragent.enable", True)
value = f"{self.options.product_name}/{self.options.product_version}"
self._update_user_agent(request, value)
_span.end()
return await super().send(request, transport)

def _get_current_options(self, request: Request) -> UserAgentHandlerOption:
"""Returns the options to use for the request.Overries default options if
request options are passed.

Args:
request (httpx.Request): The prepared request object

Returns:
UserAgentHandlerOption: The options to be used.
"""
request_options = getattr(request, "options", None)
if request_options:
current_options = request.options.get( # type:ignore
UserAgentHandlerOption.get_key(), self.options
)
return current_options
return self.options

def _update_user_agent(self, request: Request, value: str):
"""Updates the values of the User-Agent header."""
user_agent = request.headers.get("User-Agent", "")
Expand Down
35 changes: 30 additions & 5 deletions tests/middleware_tests/test_headers_inspection_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,37 @@ def test_headers_inspection_handler_construction():
assert handler

@pytest.mark.asyncio
async def test_headers_inspection_handler_gets_headers(mock_async_transport):
request = httpx.Request('GET', 'https://localhost', headers={'test': 'test_request_header'})
async def test_headers_inspection_handler_gets_headers():
def request_handler(request: httpx.Request):
return httpx.Response(
200,
json={"text": "Hello, world!"},
headers={'test_response': 'test_response_header'}
)
handler = HeadersInspectionHandler()
resp = await handler.send(request, mock_async_transport)

# First request
request = httpx.Request(
'GET',
'https://localhost',
headers={'test_request': 'test_request_header'}
)
mock_transport = httpx.MockTransport(request_handler)
resp = await handler.send(request, mock_transport)
assert resp.status_code == 200
assert handler.options.request_headers.try_get('test_request') == {'test_request_header'}
assert handler.options.response_headers.try_get('test_response') == {'test_response_header'}

# Second request
request2 = httpx.Request(
'GET',
'https://localhost',
headers={'test_request_2': 'test_request_header_2'}
)
resp = await handler.send(request2, mock_transport)
assert resp.status_code == 200
assert handler.options.request_headers.try_get('test') == {'test_request_header'}
assert handler.options.response_headers.try_get('test') == {'test_response_header'}
assert not handler.options.request_headers.try_get('test_request') == {'test_request_header'}
assert handler.options.request_headers.try_get('test_request_2') == {'test_request_header_2'}
assert handler.options.response_headers.try_get('test_response') == {'test_response_header'}


Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
from kiota_http.middleware import ParametersNameDecodingHandler
from kiota_http.middleware.options import ParametersNameDecodingHandlerOption


OPTION_KEY = "ParametersNameDecodingHandlerOption"
def test_no_config():
"""
Test that default values are used if no custom confguration is passed
"""
handler = ParametersNameDecodingHandler()
assert handler.options.enabled is True
assert handler.options.characters_to_decode == [".", "-", "~", "$"]
assert handler.options.get_key() == "ParametersNameDecodingHandlerOption"
assert handler.options.get_key() == OPTION_KEY


def test_custom_options():
Expand All @@ -26,7 +26,7 @@ def test_custom_options():

assert handler.options.enabled is not True
assert "$" not in handler.options.characters_to_decode
assert handler.options.get_key() == "ParametersNameDecodingHandlerOption"
assert handler.options.get_key() == OPTION_KEY


@pytest.mark.asyncio
Expand Down
Loading