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/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/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_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 e45a12b1..e6c49849 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 @@ -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"""