async def test_concurrent_requests_h2() -> None: async with ConnectionPool(http_version="HTTP/2") as http: info = await http.get_connection_info() assert info == {} response_1 = await http.handle_async_request( method=b"GET", url=(b"http", b"example.org", None, b"/"), headers=[], stream=httpcore.ByteStream(b""), extensions={}, ) status_code_1, headers_1, stream_1, ext_1 = response_1 info = await http.get_connection_info() assert info == {"http://example.org": ["ConnectionState.ACTIVE"]} response_2 = await http.handle_async_request( method=b"GET", url=(b"http", b"example.org", None, b"/"), headers=[], stream=httpcore.ByteStream(b""), extensions={}, ) status_code_2, headers_2, stream_2, ext_2 = response_2 info = await http.get_connection_info() assert info == {"http://example.org": ["ConnectionState.ACTIVE"]} await read_body(stream_1) info = await http.get_connection_info() assert info == {"http://example.org": ["ConnectionState.ACTIVE"]} await read_body(stream_2) info = await http.get_connection_info() assert info == {"http://example.org": ["ConnectionState.IDLE"]}
async def test_sequential_requests(http_version) -> None: async with ConnectionPool(http_version=http_version) as http: info = await http.get_connection_info() assert info == {} response = await http.handle_async_request( method=b"GET", url=(b"http", b"example.org", None, b"/"), headers=[], stream=httpcore.ByteStream(b""), extensions={}, ) status_code, headers, stream, extensions = response info = await http.get_connection_info() assert info == {"http://example.org": ["ConnectionState.ACTIVE"]} await read_body(stream) info = await http.get_connection_info() assert info == {"http://example.org": ["ConnectionState.IDLE"]} response = await http.handle_async_request( method=b"GET", url=(b"http", b"example.org", None, b"/"), headers=[], stream=httpcore.ByteStream(b""), extensions={}, ) status_code, headers, stream, extensions = response info = await http.get_connection_info() assert info == {"http://example.org": ["ConnectionState.ACTIVE"]} await read_body(stream) info = await http.get_connection_info() assert info == {"http://example.org": ["ConnectionState.IDLE"]}
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 test_get_request_with_connection_keepalive() -> None: backend = MockBackend( http_buffer=[ b"HTTP/1.1 200 OK\r\n", b"Date: Sat, 06 Oct 2049 12:34:56 GMT\r\n", b"Server: Apache\r\n", b"Content-Length: 13\r\n", b"Content-Type: text/plain\r\n", b"\r\n", b"Hello, world.", b"HTTP/1.1 200 OK\r\n", b"Date: Sat, 06 Oct 2049 12:34:56 GMT\r\n", b"Server: Apache\r\n", b"Content-Length: 13\r\n", b"Content-Type: text/plain\r\n", b"\r\n", b"Hello, world.", ] ) async with httpcore.AsyncConnectionPool(backend=backend) as http: # We're sending a request with a standard keep-alive connection, so # it will remain in the pool once we've sent the request. response = await http.handle_async_request( method=b"GET", url=(b"http", b"example.org", None, b"/"), headers=[(b"Host", b"example.org")], stream=httpcore.ByteStream(b""), extensions={}, ) status_code, headers, stream, extensions = response body = await stream.aread() assert status_code == 200 assert body == b"Hello, world." assert await http.get_connection_info() == { "http://example.org": ["HTTP/1.1, IDLE"] } # This second request will go out over the same connection. response = await http.handle_async_request( method=b"GET", url=(b"http", b"example.org", None, b"/"), headers=[(b"Host", b"example.org")], stream=httpcore.ByteStream(b""), extensions={}, ) status_code, headers, stream, extensions = response body = await stream.aread() assert status_code == 200 assert body == b"Hello, world." assert await http.get_connection_info() == { "http://example.org": ["HTTP/1.1, IDLE"] }
async def test_get_request_with_socket_disconnect_between_requests() -> None: backend = MockBackend( http_buffer=[ b"HTTP/1.1 200 OK\r\n", b"Date: Sat, 06 Oct 2049 12:34:56 GMT\r\n", b"Server: Apache\r\n", b"Content-Length: 13\r\n", b"Content-Type: text/plain\r\n", b"\r\n", b"Hello, world.", ], disconnect=True, ) async with httpcore.AsyncConnectionPool(backend=backend) as http: # Send an initial request. We're using a standard keep-alive # connection, so the connection remains in the pool after completion. response = await http.handle_async_request( method=b"GET", url=(b"http", b"example.org", None, b"/"), headers=[(b"Host", b"example.org")], stream=httpcore.ByteStream(b""), extensions={}, ) status_code, headers, stream, extensions = response body = await stream.aread() assert status_code == 200 assert body == b"Hello, world." assert await http.get_connection_info() == { "http://example.org": ["HTTP/1.1, IDLE"] } # On sending this second request, at the point of pool re-acquiry the # socket indicates that it has disconnected, and we'll send the request # over a new connection. response = await http.handle_async_request( method=b"GET", url=(b"http", b"example.org", None, b"/"), headers=[(b"Host", b"example.org")], stream=httpcore.ByteStream(b""), extensions={}, ) status_code, headers, stream, extensions = response body = await stream.aread() assert status_code == 200 assert body == b"Hello, world." assert await http.get_connection_info() == { "http://example.org": ["HTTP/1.1, IDLE"] }
async def test_broken_socket_detection_many_open_files(backend: str, server: Server) -> None: """ Regression test for: https://github.com/encode/httpcore/issues/182 """ async with httpcore.AsyncConnectionPool(backend=backend) as http: # * First attempt will be successful because it will grab the last # available fd before what select() supports on the platform. # * Second attempt would have failed without a fix, due to a "filedescriptor # out of range in select()" exception. for _ in range(2): ( status_code, response_headers, stream, extensions, ) = await http.handle_async_request( method=b"GET", url=(b"http", *server.netloc, b"/"), headers=[server.host_header], stream=httpcore.ByteStream(b""), extensions={}, ) await read_body(stream) assert status_code == 200 reason_phrase = b"OK" if server.sends_reason else b"" assert extensions == { "http_version": b"HTTP/1.1", "reason_phrase": reason_phrase, } origin = (b"http", *server.netloc) assert len(http._connections[origin]) == 1 # type: ignore
async def test_proxy_socket_does_not_leak_when_the_connection_hasnt_been_added_to_pool( proxy_server: URL, server: Server, proxy_mode: str, protocol: bytes, port: int, ): with pytest.warns(None) as recorded_warnings: async with httpcore.AsyncHTTPProxy(proxy_server, proxy_mode=proxy_mode) as http: for _ in range(100): try: _ = await http.handle_async_request( method=b"GET", url=(protocol, b"blockedhost.example.com", port, b"/"), headers=[(b"host", b"blockedhost.example.com")], stream=httpcore.ByteStream(b""), extensions={}, ) except (httpcore.ProxyError, httpcore.RemoteProtocolError): pass # have to filter out https://github.com/encode/httpx/issues/825 from other tests warnings = [ *filter(lambda warn: "asyncio" not in warn.filename, recorded_warnings.list) ] assert len(warnings) == 0
async def test_proxy_https_requests( proxy_server: URL, proxy_mode: str, http2: bool, https_server: Server, ) -> None: max_connections = 1 async with httpcore.AsyncHTTPProxy( proxy_server, proxy_mode=proxy_mode, max_connections=max_connections, http2=http2, ) as http: status_code, headers, stream, extensions = await http.handle_async_request( method=b"GET", url=(b"https", *https_server.netloc, b"/"), headers=[https_server.host_header], stream=httpcore.ByteStream(b""), extensions={}, ) _ = await read_body(stream) assert status_code == 200 assert extensions["http_version"] == b"HTTP/2" if http2 else b"HTTP/1.1" assert extensions.get("reason_phrase", b"") == b"" if http2 else b"OK"
async def test_max_keepalive_connections_handled_correctly( max_keepalive: int, connections_number: int, backend: str, server: Server) -> None: async with httpcore.AsyncConnectionPool( max_keepalive_connections=max_keepalive, keepalive_expiry=60, backend=backend) as http: connections_streams = [] for _ in range(connections_number): _, _, stream, _ = await http.handle_async_request( method=b"GET", url=(b"http", *server.netloc, b"/"), headers=[server.host_header], stream=httpcore.ByteStream(b""), extensions={}, ) connections_streams.append(stream) try: for i in range(len(connections_streams)): await read_body(connections_streams[i]) finally: stats = await http.get_connection_info() connections_in_pool = next(iter(stats.values())) assert len(connections_in_pool) == min(connections_number, max_keepalive)
async def test_get_request_with_connection_close_header() -> None: backend = MockBackend( http_buffer=[ b"HTTP/1.1 200 OK\r\n", b"Date: Sat, 06 Oct 2049 12:34:56 GMT\r\n", b"Server: Apache\r\n", b"Content-Length: 13\r\n", b"Content-Type: text/plain\r\n", b"\r\n", b"Hello, world.", b"", # Terminate the connection. ] ) async with httpcore.AsyncConnectionPool(backend=backend) as http: # We're sending a request with 'Connection: close', so the connection # does not remain in the pool once we've sent the request. response = await http.handle_async_request( method=b"GET", url=(b"http", b"example.org", None, b"/"), headers=[(b"Host", b"example.org"), (b"Connection", b"close")], stream=httpcore.ByteStream(b""), extensions={}, ) status_code, headers, stream, extensions = response body = await stream.aread() assert status_code == 200 assert body == b"Hello, world." assert await http.get_connection_info() == {} # The second request will go out over a new connection. response = await http.handle_async_request( method=b"GET", url=(b"http", b"example.org", None, b"/"), headers=[(b"Host", b"example.org"), (b"Connection", b"close")], stream=httpcore.ByteStream(b""), extensions={}, ) status_code, headers, stream, extensions = response body = await stream.aread() assert status_code == 200 assert body == b"Hello, world." assert await http.get_connection_info() == {}
async def test_request_unsupported_protocol(backend: str) -> None: async with httpcore.AsyncConnectionPool(backend=backend) as http: with pytest.raises(httpcore.UnsupportedProtocol): await http.handle_async_request( method=b"GET", url=(b"ftp", b"example.org", 443, b"/"), headers=[(b"host", b"example.org")], stream=httpcore.ByteStream(b""), extensions={}, )
async def test_get_request_with_unclean_close_after_first_request() -> None: backend = MockBackend( http_buffer=[ b"HTTP/1.1 200 OK\r\n", b"Date: Sat, 06 Oct 2049 12:34:56 GMT\r\n", b"Server: Apache\r\n", b"Content-Length: 13\r\n", b"Content-Type: text/plain\r\n", b"\r\n", b"Hello, world.", b"", # Terminate the connection. ], ) async with httpcore.AsyncConnectionPool(backend=backend) as http: # Send an initial request. We're using a standard keep-alive # connection, so the connection remains in the pool after completion. response = await http.handle_async_request( method=b"GET", url=(b"http", b"example.org", None, b"/"), headers=[(b"Host", b"example.org")], stream=httpcore.ByteStream(b""), extensions={}, ) status_code, headers, stream, extensions = response body = await stream.aread() assert status_code == 200 assert body == b"Hello, world." assert await http.get_connection_info() == { "http://example.org": ["HTTP/1.1, IDLE"] } # At this point we successfully write another request, but the socket # read returns `b""`, indicating a premature close. with pytest.raises(httpcore.RemoteProtocolError) as excinfo: await http.handle_async_request( method=b"GET", url=(b"http", b"example.org", None, b"/"), headers=[(b"Host", b"example.org")], stream=httpcore.ByteStream(b""), extensions={}, ) assert str( excinfo.value) == "Server disconnected without sending a response."
def request(http: httpcore.SyncHTTPTransport) -> int: status_code, headers, stream, extensions = http.handle_request( method=b"GET", url=(b"http", *server.netloc, b"/"), headers=[server.host_header], stream=httpcore.ByteStream(b""), extensions={}, ) read_body(stream) return status_code
async def test_http_request_cannot_reuse_dropped_connection( backend: str, server: Server) -> None: async with httpcore.AsyncConnectionPool(backend=backend) as http: status_code, headers, stream, extensions = await http.handle_async_request( method=b"GET", url=(b"http", *server.netloc, b"/"), headers=[server.host_header], stream=httpcore.ByteStream(b""), extensions={}, ) await read_body(stream) assert status_code == 200 reason_phrase = b"OK" if server.sends_reason else b"" assert extensions == { "http_version": b"HTTP/1.1", "reason_phrase": reason_phrase, } origin = (b"http", *server.netloc) assert len(http._connections[origin]) == 1 # type: ignore # Mock the connection as having been dropped. connection = list(http._connections[origin])[0] # type: ignore connection.is_socket_readable = lambda: True # type: ignore status_code, headers, stream, extensions = await http.handle_async_request( method=b"GET", url=(b"http", *server.netloc, b"/"), headers=[server.host_header], stream=httpcore.ByteStream(b""), extensions={}, ) await read_body(stream) assert status_code == 200 reason_phrase = b"OK" if server.sends_reason else b"" assert extensions == { "http_version": b"HTTP/1.1", "reason_phrase": reason_phrase, } origin = (b"http", *server.netloc) assert len(http._connections[origin]) == 1 # type: ignore
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={}, )
async def test_cannot_connect_tcp(backend: str, url) -> None: """ A properly wrapped error is raised when connecting to the server fails. """ async with httpcore.AsyncConnectionPool(backend=backend) as http: with pytest.raises(httpcore.ConnectError): await http.handle_async_request( method=b"GET", url=url, headers=[], stream=httpcore.ByteStream(b""), extensions={}, )
async def test_request_with_missing_host_header() -> None: backend = MockBackend(http_buffer=[]) async with httpcore.AsyncConnectionPool(backend=backend) as http: with pytest.raises(httpcore.LocalProtocolError) as excinfo: await http.handle_async_request( method=b"GET", url=(b"http", b"example.org", None, b"/"), headers=[], stream=httpcore.ByteStream(b""), extensions={}, ) assert str(excinfo.value) == "Missing mandatory Host: header"
async def test_https_request_reuse_connection(backend: str, https_server: Server) -> None: async with httpcore.AsyncConnectionPool(backend=backend) as http: status_code, headers, stream, extensions = await http.handle_async_request( method=b"GET", url=(b"https", *https_server.netloc, b"/"), headers=[https_server.host_header], stream=httpcore.ByteStream(b""), extensions={}, ) await read_body(stream) assert status_code == 200 reason_phrase = b"OK" if https_server.sends_reason else b"" assert extensions == { "http_version": b"HTTP/1.1", "reason_phrase": reason_phrase, } origin = (b"https", *https_server.netloc) assert len(http._connections[origin]) == 1 # type: ignore status_code, headers, stream, extensions = await http.handle_async_request( method=b"GET", url=(b"https", *https_server.netloc, b"/"), headers=[https_server.host_header], stream=httpcore.ByteStream(b""), extensions={}, ) await read_body(stream) assert status_code == 200 reason_phrase = b"OK" if https_server.sends_reason else b"" assert extensions == { "http_version": b"HTTP/1.1", "reason_phrase": reason_phrase, } origin = (b"https", *https_server.netloc) assert len(http._connections[origin]) == 1 # type: ignore
async def test_connection_pool_get_connection_info( http2: bool, keepalive_expiry: float, expected_during_active: dict, expected_during_idle: dict, backend: str, https_server: Server, ) -> None: async with httpcore.AsyncConnectionPool(http2=http2, keepalive_expiry=keepalive_expiry, backend=backend) as http: _, _, stream_1, _ = await http.handle_async_request( method=b"GET", url=(b"https", *https_server.netloc, b"/"), headers=[https_server.host_header], stream=httpcore.ByteStream(b""), extensions={}, ) _, _, stream_2, _ = await http.handle_async_request( method=b"GET", url=(b"https", *https_server.netloc, b"/"), headers=[https_server.host_header], stream=httpcore.ByteStream(b""), extensions={}, ) try: stats = await http.get_connection_info() assert stats == expected_during_active finally: await read_body(stream_1) await read_body(stream_2) stats = await http.get_connection_info() assert stats == expected_during_idle stats = await http.get_connection_info() assert stats == {}
async def test_cannot_connect_uds(backend: str) -> None: """ A properly wrapped error is raised when connecting to the UDS server fails. """ uds = "/tmp/doesnotexist.sock" async with httpcore.AsyncConnectionPool(backend=backend, uds=uds) as http: with pytest.raises(httpcore.ConnectError): await http.handle_async_request( method=b"GET", url=(b"http", b"localhost", None, b"/"), headers=[], stream=httpcore.ByteStream(b""), extensions={}, )
def test_http2_request(backend: str, https_server: Server) -> None: with httpcore.SyncConnectionPool(backend=backend, http2=True) as http: status_code, headers, stream, extensions = http.handle_request( method=b"GET", url=(b"https", *https_server.netloc, b"/"), headers=[https_server.host_header], stream=httpcore.ByteStream(b""), extensions={}, ) read_body(stream) assert status_code == 200 assert extensions == {"http_version": b"HTTP/2"} origin = (b"https", *https_server.netloc) assert len(http._connections[origin]) == 1 # type: ignore
def test_request_with_missing_host_header() -> None: backend = MockBackend(http_buffer=[]) server_config = h2.config.H2Configuration(client_side=False) server_conn = h2.connection.H2Connection(config=server_config) server_conn.initiate_connection() backend = MockBackend(http_buffer=[server_conn.data_to_send()]) with httpcore.SyncConnectionPool(backend=backend) as http: with pytest.raises(httpcore.LocalProtocolError) as excinfo: http.handle_request( method=b"GET", url=(b"http", b"example.org", None, b"/"), headers=[], stream=httpcore.ByteStream(b""), extensions={}, ) assert str(excinfo.value) == "Missing mandatory Host: header"
def stream( data, files, boundary: bytes ) -> Union[httpcore.AsyncByteStream, httpcore.SyncByteStream]: if files: # TODO Get rid of this internal import # import is performed at runtime when needed to reduce impact of internal changes in httpx from httpx._multipart import MultipartStream return MultipartStream(data=data or {}, files=files, boundary=boundary) if isinstance(data, str): data = data.encode("utf-8") elif data is None: data = b"" if isinstance(data, bytes): return httpcore.ByteStream(data) return IteratorStream(data)
def test_explicit_backend_name(server: Server) -> None: with httpcore.SyncConnectionPool(backend=lookup_sync_backend()) as http: status_code, headers, stream, extensions = http.handle_request( method=b"GET", url=(b"http", *server.netloc, b"/"), headers=[server.host_header], stream=httpcore.ByteStream(b""), extensions={}, ) read_body(stream) assert status_code == 200 reason_phrase = b"OK" if server.sends_reason else b"" assert extensions == { "http_version": b"HTTP/1.1", "reason_phrase": reason_phrase, } origin = (b"http", *server.netloc) assert len(http._connections[origin]) == 1 # type: ignore
async def test_http_request_unix_domain_socket(uds_server: Server, backend: str) -> None: uds = uds_server.uds async with httpcore.AsyncConnectionPool(uds=uds, backend=backend) as http: status_code, headers, stream, extensions = await http.handle_async_request( method=b"GET", url=(b"http", b"localhost", None, b"/"), headers=[(b"host", b"localhost")], stream=httpcore.ByteStream(b""), extensions={}, ) assert status_code == 200 reason_phrase = b"OK" if uds_server.sends_reason else b"" assert extensions == { "http_version": b"HTTP/1.1", "reason_phrase": reason_phrase, } body = await read_body(stream) assert body == b"Hello, world!"
async def test_closing_http_request(backend: str, server: Server) -> None: async with httpcore.AsyncConnectionPool(backend=backend) as http: status_code, headers, stream, extensions = await http.handle_async_request( method=b"GET", url=(b"http", *server.netloc, b"/"), headers=[server.host_header, (b"connection", b"close")], stream=httpcore.ByteStream(b""), extensions={}, ) await read_body(stream) assert status_code == 200 reason_phrase = b"OK" if server.sends_reason else b"" assert extensions == { "http_version": b"HTTP/1.1", "reason_phrase": reason_phrase, } origin = (b"http", *server.netloc) assert origin not in http._connections # type: ignore
async def test_post_request() -> None: bytes_generator = HTTP2BytesGenerator() bytes_to_send = bytes_generator.get_server_bytes( request_headers=[ (b":method", b"POST"), (b":authority", b"www.example.com"), (b":scheme", b"https"), (b":path", "/"), (b"content-length", b"13"), ], request_data=b"Hello, world.", response_headers=[ (b":status", b"200"), (b"date", b"Sat, 06 Oct 2049 12:34:56 GMT"), (b"server", b"Apache"), (b"content-length", b"13"), (b"content-type", b"text/plain"), ], response_data=b"Hello, world.", ) backend = MockBackend(http_buffer=[bytes_to_send]) async with httpcore.AsyncConnectionPool(http2=True, backend=backend) as http: # We're sending a request with a standard keep-alive connection, so # it will remain in the pool once we've sent the request. response = await http.handle_async_request( method=b"POST", url=(b"https", b"example.org", None, b"/"), headers=[(b"Host", b"example.org"), (b"Content-length", b"13")], stream=httpcore.ByteStream(b"Hello, world."), extensions={}, ) status_code, headers, stream, extensions = response body = await stream.aread() assert status_code == 200 assert body == b"Hello, world." assert await http.get_connection_info() == { "https://example.org": ["HTTP/2, IDLE, 0 streams"] }
def test_http_request_local_address(backend: str, server: Server) -> None: if backend == "sync" and lookup_sync_backend() == "trio": pytest.skip("The trio backend does not support local_address") with httpcore.SyncConnectionPool(backend=backend, local_address="0.0.0.0") as http: status_code, headers, stream, extensions = http.handle_request( method=b"GET", url=(b"http", *server.netloc, b"/"), headers=[server.host_header], stream=httpcore.ByteStream(b""), extensions={}, ) read_body(stream) assert status_code == 200 reason_phrase = b"OK" if server.sends_reason else b"" assert extensions == { "http_version": b"HTTP/1.1", "reason_phrase": reason_phrase, } origin = (b"http", *server.netloc) assert len(http._connections[origin]) == 1 # type: ignore
async def test_http_proxy(proxy_server: URL, proxy_mode: str, backend: str, server: Server) -> None: max_connections = 1 async with httpcore.AsyncHTTPProxy( proxy_server, proxy_mode=proxy_mode, max_connections=max_connections, backend=backend, ) as http: status_code, headers, stream, extensions = await http.handle_async_request( method=b"GET", url=(b"http", *server.netloc, b"/"), headers=[server.host_header], stream=httpcore.ByteStream(b""), extensions={}, ) await read_body(stream) assert status_code == 200 reason_phrase = b"OK" if server.sends_reason else b"" assert extensions == { "http_version": b"HTTP/1.1", "reason_phrase": reason_phrase, }
async def test_retries_enabled(server: Server) -> None: """ When retries are enabled, connection failures are retried on with a fixed exponential backoff. """ 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.handle_async_request( method=b"GET", url=(b"http", *server.netloc, b"/"), headers=[server.host_header], stream=httpcore.ByteStream(b""), extensions={}, ) 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.handle_async_request( method=b"GET", url=(b"http", *server.netloc, b"/"), headers=[server.host_header], stream=httpcore.ByteStream(b""), extensions={}, ) assert backend.pop_open_tcp_stream_intervals() == [ pytest.approx(0, abs=5e-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.handle_async_request( method=b"GET", url=(b"http", *server.netloc, b"/"), headers=[server.host_header], stream=httpcore.ByteStream(b""), extensions={}, ) assert backend.pop_open_tcp_stream_intervals() == [ pytest.approx(0, abs=5e-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.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.NetworkError): await http.handle_async_request( method=b"GET", url=(b"http", *server.netloc, b"/"), headers=[server.host_header], stream=httpcore.ByteStream(b""), extensions={}, )