From ade96300e1bdb7899a13ec87a1506f84bd40036f Mon Sep 17 00:00:00 2001 From: Ricardo Boss Date: Thu, 17 Sep 2026 21:57:03 +0200 Subject: [PATCH 1/2] feat: add support for the HTTP QUERY verb (RFC 10008) --- packages/abstractions/kiota_abstractions/method.py | 2 ++ packages/http/httpx/kiota_http/middleware/retry_handler.py | 2 +- .../http/httpx/tests/middleware_tests/test_retry_handler.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/abstractions/kiota_abstractions/method.py b/packages/abstractions/kiota_abstractions/method.py index 461c8567..a1d3500c 100644 --- a/packages/abstractions/kiota_abstractions/method.py +++ b/packages/abstractions/kiota_abstractions/method.py @@ -21,3 +21,5 @@ class Method(Enum): HEAD = "HEAD" # The HTTP PUT method PUT = "PUT" + # The HTTP QUERY method + QUERY = "QUERY" diff --git a/packages/http/httpx/kiota_http/middleware/retry_handler.py b/packages/http/httpx/kiota_http/middleware/retry_handler.py index e9c307c1..43a1cdab 100644 --- a/packages/http/httpx/kiota_http/middleware/retry_handler.py +++ b/packages/http/httpx/kiota_http/middleware/retry_handler.py @@ -39,7 +39,7 @@ class RetryHandler(BaseMiddleware): DEFAULT_RETRY_STATUS_CODES: set[int] = {429, 503, 504} DEFAULT_ALLOWED_METHODS: frozenset[str] = frozenset( - ['HEAD', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'] + ['HEAD', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'QUERY'] ) def __init__(self, options: RetryHandlerOption = RetryHandlerOption()) -> None: diff --git a/packages/http/httpx/tests/middleware_tests/test_retry_handler.py b/packages/http/httpx/tests/middleware_tests/test_retry_handler.py index e45a12b1..78bf92ee 100644 --- a/packages/http/httpx/tests/middleware_tests/test_retry_handler.py +++ b/packages/http/httpx/tests/middleware_tests/test_retry_handler.py @@ -24,7 +24,7 @@ def test_no_config(): assert retry_handler.options.max_retry == options.max_retry assert retry_handler.options.max_delay == options.max_delay assert retry_handler.allowed_methods == frozenset( - ['HEAD', 'GET', 'PUT', 'POST', 'PATCH', 'DELETE', 'OPTIONS'] + ['HEAD', 'GET', 'PUT', 'POST', 'PATCH', 'DELETE', 'OPTIONS', 'QUERY'] ) assert retry_handler.respect_retry_after_header From 96b11134a9a9fedb26a2ad7028b1007a564e1ddb Mon Sep 17 00:00:00 2001 From: Ricardo Boss Date: Mon, 21 Sep 2026 16:29:57 +0200 Subject: [PATCH 2/2] fix: don't turn redirected QUERY requests into GETs --- .../kiota_http/middleware/redirect_handler.py | 4 ++-- .../middleware_tests/test_redirect_handler.py | 21 +++++++++++++++++++ .../middleware_tests/test_retry_handler.py | 18 ++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/packages/http/httpx/kiota_http/middleware/redirect_handler.py b/packages/http/httpx/kiota_http/middleware/redirect_handler.py index 9c73a1ac..62462b9d 100644 --- a/packages/http/httpx/kiota_http/middleware/redirect_handler.py +++ b/packages/http/httpx/kiota_http/middleware/redirect_handler.py @@ -180,8 +180,8 @@ def _redirect_method(self, request: httpx.Request, response: httpx.Response) -> method = "GET" # Do what the browsers do, despite standards... - # Turn 302s into GETs. - if response.status_code == 302 and method != "HEAD": + # Turn 302s into GETs, except QUERY which must be preserved (RFC 10008). + if response.status_code == 302 and method not in ("HEAD", "QUERY"): method = "GET" # If a POST is responded to with a 301, turn it into a GET. diff --git a/packages/http/httpx/tests/middleware_tests/test_redirect_handler.py b/packages/http/httpx/tests/middleware_tests/test_redirect_handler.py index 232b7ea0..46f3fa48 100644 --- a/packages/http/httpx/tests/middleware_tests/test_redirect_handler.py +++ b/packages/http/httpx/tests/middleware_tests/test_redirect_handler.py @@ -182,6 +182,27 @@ def request_handler(request: httpx.Request): assert resp.request.url == REDIRECT_URL +@pytest.mark.asyncio +async def test_query_redirect_with_302_preserves_method_and_body(): + """Test that a QUERY request keeps its method and body on a 302 redirect (RFC 10008)""" + + def request_handler(request: httpx.Request): + if request.url == REDIRECT_URL: + return httpx.Response(200, ) + return httpx.Response( + FOUND, + headers={LOCATION_HEADER: REDIRECT_URL}, + ) + + handler = RedirectHandler() + request = httpx.Request('QUERY', BASE_URL, content=b'select *') + mock_transport = httpx.MockTransport(request_handler) + resp = await handler.send(request, mock_transport) + assert resp.status_code == 200 + assert resp.request.method == 'QUERY' + assert resp.request.read() == b'select *' + + @pytest.mark.asyncio async def test_redirect_to_different_host_removes_auth_header(): """Test that if a request is redirected to a different host, diff --git a/packages/http/httpx/tests/middleware_tests/test_retry_handler.py b/packages/http/httpx/tests/middleware_tests/test_retry_handler.py index 78bf92ee..e6c49849 100644 --- a/packages/http/httpx/tests/middleware_tests/test_retry_handler.py +++ b/packages/http/httpx/tests/middleware_tests/test_retry_handler.py @@ -205,6 +205,24 @@ def request_handler(request: httpx.Request): assert resp.request.headers[RETRY_ATTEMPT] == '1' +@pytest.mark.asyncio +async def test_query_request_is_retried(): + """Test that a QUERY request with a body is retried""" + + def request_handler(request: httpx.Request): + if RETRY_ATTEMPT in request.headers: + return httpx.Response(200, ) + return httpx.Response(SERVICE_UNAVAILABLE, ) + + handler = RetryHandler() + request = httpx.Request('QUERY', BASE_URL, content=b'select *') + mock_transport = httpx.MockTransport(request_handler) + resp = await handler.send(request, mock_transport) + assert resp.status_code == 200 + assert resp.request.method == 'QUERY' + assert resp.request.headers[RETRY_ATTEMPT] == '1' + + @pytest.mark.asyncio async def test_should_retry_false(): """Test that a request is not retried if should_retry is set to False"""