Skip to content

Commit 88ec221

Browse files
feat(client): add follow_redirects request option
1 parent 80e2782 commit 88ec221

File tree

4 files changed

+64
-0
lines changed

4 files changed

+64
-0
lines changed

src/cloudflare/_base_client.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,6 +1002,9 @@ def request(
10021002
if self.custom_auth is not None:
10031003
kwargs["auth"] = self.custom_auth
10041004

1005+
if options.follow_redirects is not None:
1006+
kwargs["follow_redirects"] = options.follow_redirects
1007+
10051008
log.debug("Sending HTTP Request: %s %s", request.method, request.url)
10061009

10071010
response = None
@@ -1504,6 +1507,9 @@ async def request(
15041507
if self.custom_auth is not None:
15051508
kwargs["auth"] = self.custom_auth
15061509

1510+
if options.follow_redirects is not None:
1511+
kwargs["follow_redirects"] = options.follow_redirects
1512+
15071513
log.debug("Sending HTTP Request: %s %s", request.method, request.url)
15081514

15091515
response = None

src/cloudflare/_models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,7 @@ class FinalRequestOptionsInput(TypedDict, total=False):
739739
json_data: Body
740740
extra_json: AnyMapping
741741
multipart_syntax: MultipartSyntax
742+
follow_redirects: bool
742743

743744

744745
@final
@@ -753,6 +754,7 @@ class FinalRequestOptions(pydantic.BaseModel):
753754
idempotency_key: Union[str, None] = None
754755
multipart_syntax: Union[MultipartSyntax, None] = None
755756
post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven()
757+
follow_redirects: Union[bool, None] = None
756758

757759
# It should be noted that we cannot use `json` here as that would override
758760
# a BaseModel method in an incompatible fashion.

src/cloudflare/_types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ class RequestOptions(TypedDict, total=False):
103103
extra_json: AnyMapping
104104
idempotency_key: str
105105
multipart_syntax: MultipartSyntax
106+
follow_redirects: bool
106107

107108

108109
# Sentinel class used until PEP 0661 is accepted
@@ -218,3 +219,4 @@ class _GenericAlias(Protocol):
218219

219220
class HttpxSendArgs(TypedDict, total=False):
220221
auth: httpx.Auth
222+
follow_redirects: bool

tests/test_client.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,6 +1012,33 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
10121012

10131013
assert response.http_request.headers.get("x-stainless-retry-count") == "42"
10141014

1015+
@pytest.mark.respx(base_url=base_url)
1016+
def test_follow_redirects(self, respx_mock: MockRouter) -> None:
1017+
# Test that the default follow_redirects=True allows following redirects
1018+
respx_mock.post("/redirect").mock(
1019+
return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
1020+
)
1021+
respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"}))
1022+
1023+
response = self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
1024+
assert response.status_code == 200
1025+
assert response.json() == {"status": "ok"}
1026+
1027+
@pytest.mark.respx(base_url=base_url)
1028+
def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None:
1029+
# Test that follow_redirects=False prevents following redirects
1030+
respx_mock.post("/redirect").mock(
1031+
return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
1032+
)
1033+
1034+
with pytest.raises(APIStatusError) as exc_info:
1035+
self.client.post(
1036+
"/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response
1037+
)
1038+
1039+
assert exc_info.value.response.status_code == 302
1040+
assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected"
1041+
10151042

10161043
class TestAsyncCloudflare:
10171044
client = AsyncCloudflare(base_url=base_url, api_key=api_key, api_email=api_email, _strict_response_validation=True)
@@ -1938,3 +1965,30 @@ async def test_main() -> None:
19381965
raise AssertionError("calling get_platform using asyncify resulted in a hung process")
19391966

19401967
time.sleep(0.1)
1968+
1969+
@pytest.mark.respx(base_url=base_url)
1970+
async def test_follow_redirects(self, respx_mock: MockRouter) -> None:
1971+
# Test that the default follow_redirects=True allows following redirects
1972+
respx_mock.post("/redirect").mock(
1973+
return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
1974+
)
1975+
respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"}))
1976+
1977+
response = await self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
1978+
assert response.status_code == 200
1979+
assert response.json() == {"status": "ok"}
1980+
1981+
@pytest.mark.respx(base_url=base_url)
1982+
async def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None:
1983+
# Test that follow_redirects=False prevents following redirects
1984+
respx_mock.post("/redirect").mock(
1985+
return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
1986+
)
1987+
1988+
with pytest.raises(APIStatusError) as exc_info:
1989+
await self.client.post(
1990+
"/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response
1991+
)
1992+
1993+
assert exc_info.value.response.status_code == 302
1994+
assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected"

0 commit comments

Comments
 (0)