Add option to keep the same method for 301/302 redirects

RFC hasn't been clear enough about the expected behavior for 301 and
302 response. While it's (unfortunately) common for browsers to switch
the original http method to GET when following redirects, some server
applications expects the "legacy" behavior which keeps the same method
over redirects.

Add new option to clients to select the behavior.

Signed-off-by: Takashi Kajinami <kajinamit@oss.nttdata.com>
This commit is contained in:
Takashi Kajinami 2026-02-26 23:30:48 +09:00
parent b5addb64f0
commit f0e232fc94
2 changed files with 53 additions and 0 deletions

View File

@ -196,6 +196,7 @@ class BaseClient:
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 @@ class BaseClient:
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 @@ class BaseClient:
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 @@ class Client(BaseClient):
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 @@ class Client(BaseClient):
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 @@ class AsyncClient(BaseClient):
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 @@ class AsyncClient(BaseClient):
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,

View File

@ -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