Skip to content

Commit 26d715f

Browse files
feat(client): add follow_redirects request option
1 parent c7978e9 commit 26d715f

File tree

4 files changed

+64
-0
lines changed

4 files changed

+64
-0
lines changed

src/openai/_base_client.py

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

965+
if options.follow_redirects is not None:
966+
kwargs["follow_redirects"] = options.follow_redirects
967+
965968
log.debug("Sending HTTP Request: %s %s", request.method, request.url)
966969

967970
response = None
@@ -1477,6 +1480,9 @@ async def request(
14771480
if self.custom_auth is not None:
14781481
kwargs["auth"] = self.custom_auth
14791482

1483+
if options.follow_redirects is not None:
1484+
kwargs["follow_redirects"] = options.follow_redirects
1485+
14801486
log.debug("Sending HTTP Request: %s %s", request.method, request.url)
14811487

14821488
response = None

src/openai/_models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,7 @@ class FinalRequestOptionsInput(TypedDict, total=False):
777777
idempotency_key: str
778778
json_data: Body
779779
extra_json: AnyMapping
780+
follow_redirects: bool
780781

781782

782783
@final
@@ -790,6 +791,7 @@ class FinalRequestOptions(pydantic.BaseModel):
790791
files: Union[HttpxRequestFiles, None] = None
791792
idempotency_key: Union[str, None] = None
792793
post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven()
794+
follow_redirects: Union[bool, None] = None
793795

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

src/openai/_types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ class RequestOptions(TypedDict, total=False):
101101
params: Query
102102
extra_json: AnyMapping
103103
idempotency_key: str
104+
follow_redirects: bool
104105

105106

106107
# Sentinel class used until PEP 0661 is accepted
@@ -217,3 +218,4 @@ class _GenericAlias(Protocol):
217218

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

tests/test_client.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -908,6 +908,33 @@ def retry_handler(_request: httpx.Request) -> httpx.Response:
908908
assert response.retries_taken == failures_before_success
909909
assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success
910910

911+
@pytest.mark.respx(base_url=base_url)
912+
def test_follow_redirects(self, respx_mock: MockRouter) -> None:
913+
# Test that the default follow_redirects=True allows following redirects
914+
respx_mock.post("/redirect").mock(
915+
return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
916+
)
917+
respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"}))
918+
919+
response = self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
920+
assert response.status_code == 200
921+
assert response.json() == {"status": "ok"}
922+
923+
@pytest.mark.respx(base_url=base_url)
924+
def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None:
925+
# Test that follow_redirects=False prevents following redirects
926+
respx_mock.post("/redirect").mock(
927+
return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
928+
)
929+
930+
with pytest.raises(APIStatusError) as exc_info:
931+
self.client.post(
932+
"/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response
933+
)
934+
935+
assert exc_info.value.response.status_code == 302
936+
assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected"
937+
911938

912939
class TestAsyncOpenAI:
913940
client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True)
@@ -1829,3 +1856,30 @@ async def test_main() -> None:
18291856
raise AssertionError("calling get_platform using asyncify resulted in a hung process")
18301857

18311858
time.sleep(0.1)
1859+
1860+
@pytest.mark.respx(base_url=base_url)
1861+
async def test_follow_redirects(self, respx_mock: MockRouter) -> None:
1862+
# Test that the default follow_redirects=True allows following redirects
1863+
respx_mock.post("/redirect").mock(
1864+
return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
1865+
)
1866+
respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"}))
1867+
1868+
response = await self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
1869+
assert response.status_code == 200
1870+
assert response.json() == {"status": "ok"}
1871+
1872+
@pytest.mark.respx(base_url=base_url)
1873+
async def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None:
1874+
# Test that follow_redirects=False prevents following redirects
1875+
respx_mock.post("/redirect").mock(
1876+
return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
1877+
)
1878+
1879+
with pytest.raises(APIStatusError) as exc_info:
1880+
await self.client.post(
1881+
"/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response
1882+
)
1883+
1884+
assert exc_info.value.response.status_code == 302
1885+
assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected"

0 commit comments

Comments
 (0)