/ haystack / utils / requests_utils.py
requests_utils.py
  1  # SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
  2  #
  3  # SPDX-License-Identifier: Apache-2.0
  4  
  5  import logging
  6  from typing import Any
  7  
  8  import httpx
  9  from tenacity import after_log, before_log, retry, retry_if_exception_type, stop_after_attempt, wait_exponential
 10  
 11  logger = logging.getLogger(__file__)
 12  
 13  
 14  def request_with_retry(
 15      attempts: int = 3, status_codes_to_retry: list[int] | None = None, **kwargs: Any
 16  ) -> httpx.Response:
 17      """
 18      Executes an HTTP request with a configurable exponential backoff retry on failures.
 19  
 20      Usage example:
 21      <!-- test-ignore -->
 22      ```python
 23      from haystack.utils import request_with_retry
 24  
 25      # Sending an HTTP request with default retry configs
 26      res = request_with_retry(method="GET", url="https://example.com")
 27  
 28      # Sending an HTTP request with custom number of attempts
 29      res = request_with_retry(method="GET", url="https://example.com", attempts=10)
 30  
 31      # Sending an HTTP request with custom HTTP codes to retry
 32      res = request_with_retry(method="GET", url="https://example.com", status_codes_to_retry=[408, 503])
 33  
 34      # Sending an HTTP request with custom timeout in seconds
 35      res = request_with_retry(method="GET", url="https://example.com", timeout=5)
 36  
 37      # Sending an HTTP request with custom headers
 38      res = request_with_retry(method="GET", url="https://example.com", headers={"Authorization": "Bearer <token>"})
 39  
 40      # Sending a POST request
 41      res = request_with_retry(method="POST", url="https://example.com", json={"key": "value"}, attempts=10)
 42  
 43      # Retry all 5xx status codes
 44      res = request_with_retry(method="GET", url="https://example.com", status_codes_to_retry=list(range(500, 600)))
 45      ```
 46  
 47      :param attempts:
 48          Maximum number of attempts to retry the request.
 49      :param status_codes_to_retry:
 50          List of HTTP status codes that will trigger a retry.
 51          When param is `None`, HTTP 408, 418, 429 and 503 will be retried.
 52      :param kwargs:
 53          Optional arguments that `httpx.Client.request` accepts.
 54      :returns:
 55          The `httpx.Response` object.
 56      """
 57  
 58      if status_codes_to_retry is None:
 59          status_codes_to_retry = [408, 418, 429, 503]
 60  
 61      @retry(
 62          reraise=True,
 63          wait=wait_exponential(),
 64          retry=retry_if_exception_type((httpx.HTTPError, TimeoutError)),
 65          stop=stop_after_attempt(attempts),
 66          before=before_log(logger, logging.DEBUG),
 67          after=after_log(logger, logging.DEBUG),
 68      )
 69      def run() -> httpx.Response:
 70          timeout = kwargs.pop("timeout", 10)
 71          with httpx.Client() as client:
 72              res = client.request(**kwargs, timeout=timeout)
 73  
 74              if res.status_code in status_codes_to_retry:
 75                  # We raise only for the status codes that must trigger a retry
 76                  res.raise_for_status()
 77  
 78              return res
 79  
 80      res = run()
 81      # We raise here too in case the request failed with a status code that
 82      # won't trigger a retry, this way the call will still cause an explicit exception
 83      res.raise_for_status()
 84      return res
 85  
 86  
 87  async def async_request_with_retry(
 88      attempts: int = 3, status_codes_to_retry: list[int] | None = None, **kwargs: Any
 89  ) -> httpx.Response:
 90      """
 91      Executes an asynchronous HTTP request with a configurable exponential backoff retry on failures.
 92  
 93      Usage example:
 94      ```python
 95      import asyncio
 96      from haystack.utils import async_request_with_retry
 97  
 98      # Sending an async HTTP request with default retry configs
 99      async def example():
100          res = await async_request_with_retry(method="GET", url="https://example.com")
101          return res
102  
103      # Sending an async HTTP request with custom number of attempts
104      async def example_with_attempts():
105          res = await async_request_with_retry(method="GET", url="https://example.com", attempts=10)
106          return res
107  
108      # Sending an async HTTP request with custom HTTP codes to retry
109      async def example_with_status_codes():
110          res = await async_request_with_retry(method="GET", url="https://example.com", status_codes_to_retry=[408, 503])
111          return res
112  
113      # Sending an async HTTP request with custom timeout in seconds
114      async def example_with_timeout():
115          res = await async_request_with_retry(method="GET", url="https://example.com", timeout=5)
116          return res
117  
118      # Sending an async HTTP request with custom headers
119      async def example_with_headers():
120          headers = {"Authorization": "Bearer <my_token_here>"}
121          res = await async_request_with_retry(method="GET", url="https://example.com", headers=headers)
122          return res
123  
124      # All of the above combined
125      async def example_combined():
126          headers = {"Authorization": "Bearer <my_token_here>"}
127          res = await async_request_with_retry(
128              method="GET",
129              url="https://example.com",
130              headers=headers,
131              attempts=10,
132              status_codes_to_retry=[408, 503],
133              timeout=5
134          )
135          return res
136  
137      # Sending an async POST request
138      async def example_post():
139          res = await async_request_with_retry(
140              method="POST",
141              url="https://example.com",
142              json={"key": "value"},
143              attempts=10
144          )
145          return res
146  
147      # Retry all 5xx status codes
148      async def example_5xx():
149          res = await async_request_with_retry(
150              method="GET",
151              url="https://example.com",
152              status_codes_to_retry=list(range(500, 600))
153          )
154          return res
155      ```
156  
157      :param attempts:
158          Maximum number of attempts to retry the request.
159      :param status_codes_to_retry:
160          List of HTTP status codes that will trigger a retry.
161          When param is `None`, HTTP 408, 418, 429 and 503 will be retried.
162      :param kwargs:
163          Optional arguments that `httpx.AsyncClient.request` accepts.
164      :returns:
165          The `httpx.Response` object.
166      """
167  
168      if status_codes_to_retry is None:
169          status_codes_to_retry = [408, 418, 429, 503]
170  
171      @retry(
172          reraise=True,
173          wait=wait_exponential(),
174          retry=retry_if_exception_type((httpx.HTTPError, TimeoutError)),
175          stop=stop_after_attempt(attempts),
176          before=before_log(logger, logging.DEBUG),
177          after=after_log(logger, logging.DEBUG),
178      )
179      async def run() -> httpx.Response:
180          timeout = kwargs.pop("timeout", 10)
181          async with httpx.AsyncClient() as client:
182              res = await client.request(**kwargs, timeout=timeout)
183  
184              if res.status_code in status_codes_to_retry:
185                  # We raise only for the status codes that must trigger a retry
186                  res.raise_for_status()
187  
188              return res
189  
190      res = await run()
191      # We raise here too in case the request failed with a status code that
192      # won't trigger a retry, this way the call will still cause an explicit exception
193      res.raise_for_status()
194      return res