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