@@ -101,12 +101,11 @@ def aiter_bytes(self) -> AsyncIterator[bytes]:
101101class HttpClientBase :
102102 """Shared configuration and utilities for HTTP clients.
103103
104- Provides common functionality for both sync and async HTTP clients including:
105- header construction, parameter parsing, request body preparation, URL building,
106- and timeout calculation.
104+ Provides common functionality for both sync and async HTTP clients including: header construction, parameter
105+ parsing, request body preparation, URL building, timeout calculation, and error classification.
107106
108- Subclasses should call `super().__init__()` to initialize shared configuration.
109- The helper methods are then available for use in the `call()` implementation .
107+ Subclasses should call `super().__init__()` to initialize shared configuration. The helpers are then used by the
108+ inherited `call`, and stay available to a client that replaces it .
110109 """
111110
112111 def __init__ (
@@ -365,15 +364,44 @@ def _handle_request_exception(self, exc: Exception, *, stop_retrying: Callable[[
365364 logger .debug ('Exception is not retryable' , exc_info = exc )
366365 stop_retrying ()
367366
367+ def _handle_response_status (
368+ self ,
369+ response : HttpResponse ,
370+ * ,
371+ attempt : int ,
372+ stop_retrying : Callable [[], None ],
373+ ) -> bool :
374+ """Record the response status and stop retrying unless it is a server error or a rate limit.
375+
376+ Returns whether the response is a success, so the caller can hand it back instead of raising.
377+ """
378+ if response .status_code < HTTPStatus .MULTIPLE_CHOICES :
379+ logger .debug ('Request successful' , extra = {'status_code' : response .status_code })
380+ return True
381+
382+ if response .status_code == HTTPStatus .TOO_MANY_REQUESTS :
383+ self ._statistics .add_rate_limit_error (attempt )
384+
385+ logger .debug ('Request unsuccessful' , extra = {'status_code' : response .status_code })
386+ if (
387+ response .status_code < HTTPStatus .INTERNAL_SERVER_ERROR
388+ and response .status_code != HTTPStatus .TOO_MANY_REQUESTS
389+ ):
390+ logger .debug ('Status code is not retryable' , extra = {'status_code' : response .status_code })
391+ stop_retrying ()
392+
393+ return False
394+
368395
369396@docs_group ('HTTP clients' )
370397class HttpClient (HttpClientBase ):
371398 """Base class for synchronous HTTP clients used by `ApifyClient`.
372399
373400 Concrete clients inherit its request preparation, retry, and error handling, and implement the transport,
374401 error-classification, and lifecycle hooks. Only `send_request` has to be implemented, the other hooks have
375- defaults. Replacing `call` itself also remains supported, and then none of the hooks are used. Helper methods
376- from `HttpClientBase` remain available for request preparation, URL building, and parameter parsing.
402+ defaults. Replacing `call` itself also remains supported, and then the transport and retry-classification hooks
403+ are bypassed. Helper methods from `HttpClientBase` remain available for request preparation, URL building, and
404+ parameter parsing.
377405
378406 The client's default headers from `self._headers` have to go out with every request, otherwise the
379407 `Authorization` header never reaches the API. The inherited `call` merges them into the per-request headers
@@ -556,21 +584,9 @@ def _make_request(
556584 self ._handle_request_exception (exc , stop_retrying = stop_retrying )
557585 raise
558586
559- if response .status_code < HTTPStatus .MULTIPLE_CHOICES :
560- logger .debug ('Request successful' , extra = {'status_code' : response .status_code })
587+ if self ._handle_response_status (response , attempt = attempt , stop_retrying = stop_retrying ):
561588 return response
562589
563- if response .status_code == HTTPStatus .TOO_MANY_REQUESTS :
564- self ._statistics .add_rate_limit_error (attempt )
565-
566- logger .debug ('Request unsuccessful' , extra = {'status_code' : response .status_code })
567- if (
568- response .status_code < HTTPStatus .INTERNAL_SERVER_ERROR
569- and response .status_code != HTTPStatus .TOO_MANY_REQUESTS
570- ):
571- logger .debug ('Status code is not retryable' , extra = {'status_code' : response .status_code })
572- stop_retrying ()
573-
574590 # Read the response in case it is a stream, so the error can be raised properly.
575591 response .read ()
576592 raise ApifyApiError (response , attempt , method = method )
@@ -773,21 +789,9 @@ async def _make_request(
773789 self ._handle_request_exception (exc , stop_retrying = stop_retrying )
774790 raise
775791
776- if response .status_code < HTTPStatus .MULTIPLE_CHOICES :
777- logger .debug ('Request successful' , extra = {'status_code' : response .status_code })
792+ if self ._handle_response_status (response , attempt = attempt , stop_retrying = stop_retrying ):
778793 return response
779794
780- if response .status_code == HTTPStatus .TOO_MANY_REQUESTS :
781- self ._statistics .add_rate_limit_error (attempt )
782-
783- logger .debug ('Request unsuccessful' , extra = {'status_code' : response .status_code })
784- if (
785- response .status_code < HTTPStatus .INTERNAL_SERVER_ERROR
786- and response .status_code != HTTPStatus .TOO_MANY_REQUESTS
787- ):
788- logger .debug ('Status code is not retryable' , extra = {'status_code' : response .status_code })
789- stop_retrying ()
790-
791795 # Read the response in case it is a stream, so the error can be raised properly.
792796 await response .aread ()
793797 raise ApifyApiError (response , attempt , method = method )
0 commit comments