async def test_retries_enabled(server: Server) -> None: """ When retries are enabled, connection failures are retried on with a fixed exponential backoff. """ method = b"GET" url = (b"http", *server.netloc, b"/") headers = [server.host_header] backend = AsyncMockBackend() retries = 10 # Large enough to not run out of retries within this test. async with httpcore.AsyncConnectionPool(retries=retries, max_keepalive_connections=0, backend=backend) as http: # Standard case, no failures. response = await http.arequest(method, url, headers) assert backend.pop_open_tcp_stream_intervals() == [] status_code, _, stream, _ = response assert status_code == 200 await read_body(stream) # One failure, then success. backend.push(httpcore.ConnectError(), None) response = await http.arequest(method, url, headers) assert backend.pop_open_tcp_stream_intervals() == [ pytest.approx(0, abs=1e-3), # Retry immediately. ] status_code, _, stream, _ = response assert status_code == 200 await read_body(stream) # Three failures, then success. backend.push( httpcore.ConnectError(), httpcore.ConnectTimeout(), httpcore.ConnectTimeout(), None, ) response = await http.arequest(method, url, headers) assert backend.pop_open_tcp_stream_intervals() == [ pytest.approx(0, abs=1e-3), # Retry immediately. pytest.approx(0.5, rel=0.1), # First backoff. pytest.approx(1.0, rel=0.1), # Second (increased) backoff. ] status_code, _, stream, _ = response assert status_code == 200 await read_body(stream) # Non-connect exceptions are not retried on. backend.push(httpcore.ReadTimeout(), httpcore.NetworkError()) with pytest.raises(httpcore.ReadTimeout): await http.arequest(method, url, headers) with pytest.raises(httpcore.NetworkError): await http.arequest(method, url, headers)
async def test_retries_exceeded(server: Server) -> None: """ When retries are enabled and connecting failures more than the configured number of retries, connect exceptions are raised. """ backend = AsyncMockBackend() retries = 1 async with httpcore.AsyncConnectionPool( retries=retries, max_keepalive_connections=0, backend=backend ) as http: response = await http.handle_async_request( method=b"GET", url=(b"http", *server.netloc, b"/"), headers=[server.host_header], stream=httpcore.ByteStream(b""), extensions={}, ) status_code, _, stream, _ = response assert status_code == 200 await read_body(stream) # First failure is retried on, second one isn't. backend.push(httpcore.ConnectError(), httpcore.ConnectTimeout()) with pytest.raises(httpcore.ConnectTimeout): await http.handle_async_request( method=b"GET", url=(b"http", *server.netloc, b"/"), headers=[server.host_header], stream=httpcore.ByteStream(b""), extensions={}, )
async def arequest(self, method, url, headers=None, stream=None, ext=None): retry = 2 while retry > 0: retry -= 1 try: return await super().arequest(method, url, headers, stream, ext) except OSError as e: # socket.gaierror when DNS resolution fails raise httpcore.ConnectError(e) except httpcore.CloseError as e: # httpcore.CloseError: [Errno 104] Connection reset by peer # raised by _keepalive_sweep() # from https://github.com/encode/httpcore/blob/4b662b5c42378a61e54d673b4c949420102379f5/httpcore/_backends/asyncio.py#L198 # noqa await close_connections_for_url(self._pool, url) logger.warning('httpcore.CloseError: retry', exc_info=e) # retry except httpcore.RemoteProtocolError as e: # in case of httpcore.RemoteProtocolError: Server disconnected await close_connections_for_url(self._pool, url) logger.warning('httpcore.RemoteProtocolError: retry', exc_info=e) # retry except (httpcore.ProtocolError, httpcore.NetworkError) as e: await close_connections_for_url(self._pool, url) raise e
def test_raise_on_request_error(client: Client) -> None: """If raise exception on a request error.""" respx.put( re.compile(PLATFORM_URL + "/api/v1/requests/.*$"), content=httpcore.ConnectError(), ) # TODO: do we really want to leak httpx to our clients? # We could catch all exception thrown by httpx, wrap it in a few library # exceptions and rethrow those. with pytest.raises(httpx.ConnectError): client.put(client.identity.public_identity)
async def test_no_retries(server: Server) -> None: """ By default, connection failures are not retried on. """ backend = AsyncMockBackend() async with httpcore.AsyncConnectionPool( max_keepalive_connections=0, backend=backend ) as http: response = await http.handle_async_request( method=b"GET", url=(b"http", *server.netloc, b"/"), headers=[server.host_header], stream=httpcore.ByteStream(b""), extensions={}, ) status_code, _, stream, _ = response assert status_code == 200 await read_body(stream) backend.push(httpcore.ConnectTimeout(), httpcore.ConnectError()) with pytest.raises(httpcore.ConnectTimeout): await http.handle_async_request( method=b"GET", url=(b"http", *server.netloc, b"/"), headers=[server.host_header], stream=httpcore.ByteStream(b""), extensions={}, ) with pytest.raises(httpcore.ConnectError): await http.handle_async_request( method=b"GET", url=(b"http", *server.netloc, b"/"), headers=[server.host_header], stream=httpcore.ByteStream(b""), extensions={}, )
def test_no_retries(server: Server) -> None: """ By default, connection failures are not retried on. """ method = b"GET" url = (b"http", *server.netloc, b"/") headers = [server.host_header] backend = SyncMockBackend() with httpcore.SyncConnectionPool(max_keepalive_connections=0, backend=backend) as http: response = http.request(method, url, headers) status_code, _, stream, _ = response assert status_code == 200 read_body(stream) backend.push(httpcore.ConnectTimeout(), httpcore.ConnectError()) with pytest.raises(httpcore.ConnectTimeout): http.request(method, url, headers) with pytest.raises(httpcore.ConnectError): http.request(method, url, headers)
def raise_exception_once(*args, **kwargs): nonlocal times_called times_called += 1 if times_called == 1: raise httpcore.ConnectError() return pytest_httpx.to_response()
def connect_failed(*args, **kwargs): raise httpcore.ConnectError()