diff --git a/httpx/_client.py b/httpx/_client.py index 13cd933673..87a93f9ff1 100644 --- a/httpx/_client.py +++ b/httpx/_client.py @@ -196,6 +196,7 @@ def __init__( timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, follow_redirects: bool = False, max_redirects: int = DEFAULT_MAX_REDIRECTS, + keep_method_for_redirects: bool = False, event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None, base_url: URL | str = "", trust_env: bool = True, @@ -212,6 +213,7 @@ def __init__( self._timeout = Timeout(timeout) self.follow_redirects = follow_redirects self.max_redirects = max_redirects + self.keep_method_for_redirects = keep_method_for_redirects self._event_hooks = { "request": list(event_hooks.get("request", [])), "response": list(event_hooks.get("response", [])), @@ -502,6 +504,9 @@ def _redirect_method(self, request: Request, response: Response) -> str: if response.status_code == codes.SEE_OTHER and method != "HEAD": method = "GET" + if self.keep_method_for_redirects: + return method + # Do what the browsers do, despite standards... # Turn 302s into GETs. if response.status_code == codes.FOUND and method != "HEAD": @@ -622,9 +627,13 @@ class Client(BaseClient): * **proxy** - *(optional)* A proxy URL where all the traffic should be routed. * **timeout** - *(optional)* The timeout configuration to use when sending requests. + * **follow_redirects** - *(optional)* Follow redirects and send a new + * request to the redirected url automatically. * **limits** - *(optional)* The limits configuration to use. * **max_redirects** - *(optional)* The maximum number of redirect responses that should be followed. + * **keep_method_for_redirects* - *(optional)* Keep the original HTTP method + when following redirects. This is effective only for 301 and 302 . * **base_url** - *(optional)* A URL to use as the base when building request URLs. * **transport** - *(optional)* A transport class to use for sending requests @@ -654,6 +663,7 @@ def __init__( follow_redirects: bool = False, limits: Limits = DEFAULT_LIMITS, max_redirects: int = DEFAULT_MAX_REDIRECTS, + keep_method_for_redirects: bool = False, event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None, base_url: URL | str = "", transport: BaseTransport | None = None, @@ -667,6 +677,7 @@ def __init__( timeout=timeout, follow_redirects=follow_redirects, max_redirects=max_redirects, + keep_method_for_redirects=keep_method_for_redirects, event_hooks=event_hooks, base_url=base_url, trust_env=trust_env, @@ -1336,9 +1347,13 @@ class AsyncClient(BaseClient): * **proxy** - *(optional)* A proxy URL where all the traffic should be routed. * **timeout** - *(optional)* The timeout configuration to use when sending requests. + * **follow_redirects** - *(optional)* Follow redirects and send a new + * request to the redirected url automatically. * **limits** - *(optional)* The limits configuration to use. * **max_redirects** - *(optional)* The maximum number of redirect responses that should be followed. + * **keep_method_for_redirects* - *(optional)* Keep the original HTTP method + when following redirects. This is effective only for 301 and 302 . * **base_url** - *(optional)* A URL to use as the base when building request URLs. * **transport** - *(optional)* A transport class to use for sending requests @@ -1367,6 +1382,7 @@ def __init__( follow_redirects: bool = False, limits: Limits = DEFAULT_LIMITS, max_redirects: int = DEFAULT_MAX_REDIRECTS, + keep_method_for_redirects: bool = False, event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None, base_url: URL | str = "", transport: AsyncBaseTransport | None = None, @@ -1381,6 +1397,7 @@ def __init__( timeout=timeout, follow_redirects=follow_redirects, max_redirects=max_redirects, + keep_method_for_redirects=keep_method_for_redirects, event_hooks=event_hooks, base_url=base_url, trust_env=trust_env, diff --git a/tests/client/test_redirects.py b/tests/client/test_redirects.py index f65827134c..a1de9dd28b 100644 --- a/tests/client/test_redirects.py +++ b/tests/client/test_redirects.py @@ -118,6 +118,18 @@ def test_redirect_301(): response = client.post("https://example.org/redirect_301", follow_redirects=True) assert response.status_code == httpx.codes.OK assert response.url == "https://example.org/" + assert response.request.method == "GET" + assert len(response.history) == 1 + + +def test_redirect_301_keep_method(): + client = httpx.Client( + transport=httpx.MockTransport(redirects), keep_method_for_redirects=True + ) + response = client.post("https://example.org/redirect_301", follow_redirects=True) + assert response.status_code == httpx.codes.OK + assert response.url == "https://example.org/" + assert response.request.method == "POST" assert len(response.history) == 1 @@ -126,6 +138,18 @@ def test_redirect_302(): response = client.post("https://example.org/redirect_302", follow_redirects=True) assert response.status_code == httpx.codes.OK assert response.url == "https://example.org/" + assert response.request.method == "GET" + assert len(response.history) == 1 + + +def test_redirect_302_keep_method(): + client = httpx.Client( + transport=httpx.MockTransport(redirects), keep_method_for_redirects=True + ) + response = client.post("https://example.org/redirect_302", follow_redirects=True) + assert response.status_code == httpx.codes.OK + assert response.url == "https://example.org/" + assert response.request.method == "POST" assert len(response.history) == 1 @@ -134,6 +158,18 @@ def test_redirect_303(): response = client.get("https://example.org/redirect_303", follow_redirects=True) assert response.status_code == httpx.codes.OK assert response.url == "https://example.org/" + assert response.request.method == "GET" + assert len(response.history) == 1 + + +def test_redirect_303_keep_method(): + client = httpx.Client( + transport=httpx.MockTransport(redirects), keep_method_for_redirects=True + ) + response = client.get("https://example.org/redirect_303", follow_redirects=True) + assert response.status_code == httpx.codes.OK + assert response.url == "https://example.org/" + assert response.request.method == "GET" assert len(response.history) == 1