diff --git a/src/openai/_base_client.py b/src/openai/_base_client.py index a1d0960700..abfb9aff48 100644 --- a/src/openai/_base_client.py +++ b/src/openai/_base_client.py @@ -1038,8 +1038,8 @@ def request( except OpenAIError as err: # Propagate OpenAIErrors as-is, without retrying or wrapping in APIConnectionError raise err - except Exception as err: - log.debug("Encountered Exception", exc_info=True) + except httpx.RequestError as err: + log.debug("Encountered httpx.RequestError", exc_info=True) if remaining_retries > 0: self._sleep_for_retry( @@ -1649,8 +1649,8 @@ async def request( except OpenAIError as err: # Propagate OpenAIErrors as-is, without retrying or wrapping in APIConnectionError raise err - except Exception as err: - log.debug("Encountered Exception", exc_info=True) + except httpx.RequestError as err: + log.debug("Encountered httpx.RequestError", exc_info=True) if remaining_retries > 0: await self._sleep_for_retry( diff --git a/tests/test_client.py b/tests/test_client.py index 570042c46a..a278615685 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -57,6 +57,10 @@ class MockRequestCall(Protocol): request: httpx.Request +class CustomTaskSignal(Exception): + pass + + def _get_params(client: BaseClient[Any, Any]) -> dict[str, str]: request = client._build_request(FinalRequestOptions(method="get", url="/foo")) url = httpx.URL(request.url) @@ -966,7 +970,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: if nb_retries < failures_before_success: nb_retries += 1 if failure_mode == "exception": - raise RuntimeError("oops") + raise httpx.ConnectError("oops") return httpx.Response(500) return httpx.Response(200) @@ -985,6 +989,30 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success + @pytest.mark.respx(base_url=base_url) + def test_non_httpx_request_errors_are_not_retried_or_wrapped(self, respx_mock: MockRouter, client: OpenAI) -> None: + nb_requests = 0 + + def handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_requests + nb_requests += 1 + raise CustomTaskSignal("task timeout") + + respx_mock.post("/chat/completions").mock(side_effect=handler) + + with pytest.raises(CustomTaskSignal): + client.with_options(max_retries=4).chat.completions.create( + messages=[ + { + "content": "string", + "role": "developer", + } + ], + model="gpt-5.4", + ) + + assert nb_requests == 1 + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) @@ -2037,7 +2065,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: if nb_retries < failures_before_success: nb_retries += 1 if failure_mode == "exception": - raise RuntimeError("oops") + raise httpx.ConnectError("oops") return httpx.Response(500) return httpx.Response(200) @@ -2056,6 +2084,32 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success + @pytest.mark.respx(base_url=base_url) + async def test_non_httpx_request_errors_are_not_retried_or_wrapped( + self, respx_mock: MockRouter, async_client: AsyncOpenAI + ) -> None: + nb_requests = 0 + + def handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_requests + nb_requests += 1 + raise CustomTaskSignal("task timeout") + + respx_mock.post("/chat/completions").mock(side_effect=handler) + + with pytest.raises(CustomTaskSignal): + await async_client.with_options(max_retries=4).chat.completions.create( + messages=[ + { + "content": "string", + "role": "developer", + } + ], + model="gpt-5.4", + ) + + assert nb_requests == 1 + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) @mock.patch("openai._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url)