Skip to content

Commit f0e232f

Browse files
committed
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>
1 parent b5addb6 commit f0e232f

2 files changed

Lines changed: 53 additions & 0 deletions

File tree

httpx/_client.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ def __init__(
196196
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
197197
follow_redirects: bool = False,
198198
max_redirects: int = DEFAULT_MAX_REDIRECTS,
199+
keep_method_for_redirects: bool = False,
199200
event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None,
200201
base_url: URL | str = "",
201202
trust_env: bool = True,
@@ -212,6 +213,7 @@ def __init__(
212213
self._timeout = Timeout(timeout)
213214
self.follow_redirects = follow_redirects
214215
self.max_redirects = max_redirects
216+
self.keep_method_for_redirects = keep_method_for_redirects
215217
self._event_hooks = {
216218
"request": list(event_hooks.get("request", [])),
217219
"response": list(event_hooks.get("response", [])),
@@ -502,6 +504,9 @@ def _redirect_method(self, request: Request, response: Response) -> str:
502504
if response.status_code == codes.SEE_OTHER and method != "HEAD":
503505
method = "GET"
504506

507+
if self.keep_method_for_redirects:
508+
return method
509+
505510
# Do what the browsers do, despite standards...
506511
# Turn 302s into GETs.
507512
if response.status_code == codes.FOUND and method != "HEAD":
@@ -622,9 +627,13 @@ class Client(BaseClient):
622627
* **proxy** - *(optional)* A proxy URL where all the traffic should be routed.
623628
* **timeout** - *(optional)* The timeout configuration to use when sending
624629
requests.
630+
* **follow_redirects** - *(optional)* Follow redirects and send a new
631+
* request to the redirected url automatically.
625632
* **limits** - *(optional)* The limits configuration to use.
626633
* **max_redirects** - *(optional)* The maximum number of redirect responses
627634
that should be followed.
635+
* **keep_method_for_redirects* - *(optional)* Keep the original HTTP method
636+
when following redirects. This is effective only for 301 and 302 .
628637
* **base_url** - *(optional)* A URL to use as the base when building
629638
request URLs.
630639
* **transport** - *(optional)* A transport class to use for sending requests
@@ -654,6 +663,7 @@ def __init__(
654663
follow_redirects: bool = False,
655664
limits: Limits = DEFAULT_LIMITS,
656665
max_redirects: int = DEFAULT_MAX_REDIRECTS,
666+
keep_method_for_redirects: bool = False,
657667
event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None,
658668
base_url: URL | str = "",
659669
transport: BaseTransport | None = None,
@@ -667,6 +677,7 @@ def __init__(
667677
timeout=timeout,
668678
follow_redirects=follow_redirects,
669679
max_redirects=max_redirects,
680+
keep_method_for_redirects=keep_method_for_redirects,
670681
event_hooks=event_hooks,
671682
base_url=base_url,
672683
trust_env=trust_env,
@@ -1336,9 +1347,13 @@ class AsyncClient(BaseClient):
13361347
* **proxy** - *(optional)* A proxy URL where all the traffic should be routed.
13371348
* **timeout** - *(optional)* The timeout configuration to use when sending
13381349
requests.
1350+
* **follow_redirects** - *(optional)* Follow redirects and send a new
1351+
* request to the redirected url automatically.
13391352
* **limits** - *(optional)* The limits configuration to use.
13401353
* **max_redirects** - *(optional)* The maximum number of redirect responses
13411354
that should be followed.
1355+
* **keep_method_for_redirects* - *(optional)* Keep the original HTTP method
1356+
when following redirects. This is effective only for 301 and 302 .
13421357
* **base_url** - *(optional)* A URL to use as the base when building
13431358
request URLs.
13441359
* **transport** - *(optional)* A transport class to use for sending requests
@@ -1367,6 +1382,7 @@ def __init__(
13671382
follow_redirects: bool = False,
13681383
limits: Limits = DEFAULT_LIMITS,
13691384
max_redirects: int = DEFAULT_MAX_REDIRECTS,
1385+
keep_method_for_redirects: bool = False,
13701386
event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None,
13711387
base_url: URL | str = "",
13721388
transport: AsyncBaseTransport | None = None,
@@ -1381,6 +1397,7 @@ def __init__(
13811397
timeout=timeout,
13821398
follow_redirects=follow_redirects,
13831399
max_redirects=max_redirects,
1400+
keep_method_for_redirects=keep_method_for_redirects,
13841401
event_hooks=event_hooks,
13851402
base_url=base_url,
13861403
trust_env=trust_env,

tests/client/test_redirects.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,18 @@ def test_redirect_301():
118118
response = client.post("https://example.org/redirect_301", follow_redirects=True)
119119
assert response.status_code == httpx.codes.OK
120120
assert response.url == "https://example.org/"
121+
assert response.request.method == "GET"
122+
assert len(response.history) == 1
123+
124+
125+
def test_redirect_301_keep_method():
126+
client = httpx.Client(
127+
transport=httpx.MockTransport(redirects), keep_method_for_redirects=True
128+
)
129+
response = client.post("https://example.org/redirect_301", follow_redirects=True)
130+
assert response.status_code == httpx.codes.OK
131+
assert response.url == "https://example.org/"
132+
assert response.request.method == "POST"
121133
assert len(response.history) == 1
122134

123135

@@ -126,6 +138,18 @@ def test_redirect_302():
126138
response = client.post("https://example.org/redirect_302", follow_redirects=True)
127139
assert response.status_code == httpx.codes.OK
128140
assert response.url == "https://example.org/"
141+
assert response.request.method == "GET"
142+
assert len(response.history) == 1
143+
144+
145+
def test_redirect_302_keep_method():
146+
client = httpx.Client(
147+
transport=httpx.MockTransport(redirects), keep_method_for_redirects=True
148+
)
149+
response = client.post("https://example.org/redirect_302", follow_redirects=True)
150+
assert response.status_code == httpx.codes.OK
151+
assert response.url == "https://example.org/"
152+
assert response.request.method == "POST"
129153
assert len(response.history) == 1
130154

131155

@@ -134,6 +158,18 @@ def test_redirect_303():
134158
response = client.get("https://example.org/redirect_303", follow_redirects=True)
135159
assert response.status_code == httpx.codes.OK
136160
assert response.url == "https://example.org/"
161+
assert response.request.method == "GET"
162+
assert len(response.history) == 1
163+
164+
165+
def test_redirect_303_keep_method():
166+
client = httpx.Client(
167+
transport=httpx.MockTransport(redirects), keep_method_for_redirects=True
168+
)
169+
response = client.get("https://example.org/redirect_303", follow_redirects=True)
170+
assert response.status_code == httpx.codes.OK
171+
assert response.url == "https://example.org/"
172+
assert response.request.method == "GET"
137173
assert len(response.history) == 1
138174

139175

0 commit comments

Comments
 (0)