async def test_extra_info(httpbin_secure): ssl_context = ssl.create_default_context() ssl_context.check_hostname = False ssl_context.verify_mode = ssl.CERT_NONE async with AsyncConnectionPool(ssl_context=ssl_context) as pool: async with pool.stream("GET", httpbin_secure.url) as response: assert response.status == 200 stream = response.extensions["network_stream"] ssl_object = stream.get_extra_info("ssl_object") assert ssl_object.version() == "TLSv1.3" local_addr = stream.get_extra_info("client_addr") assert local_addr[0] == "127.0.0.1" remote_addr = stream.get_extra_info("server_addr") assert "https://%s:%d" % remote_addr == httpbin_secure.url sock = stream.get_extra_info("socket") assert hasattr(sock, "family") assert hasattr(sock, "type") invalid = stream.get_extra_info("invalid") assert invalid is None stream.get_extra_info("is_readable")
async def test_unsupported_protocol(): async with AsyncConnectionPool() as pool: with pytest.raises(UnsupportedProtocol): await pool.request("GET", "ftp://www.example.com/") with pytest.raises(UnsupportedProtocol): await pool.request("GET", "://www.example.com/")
async def test_ssl_request(httpbin_secure): ssl_context = ssl.create_default_context() ssl_context.check_hostname = False ssl_context.verify_mode = ssl.CERT_NONE async with AsyncConnectionPool(ssl_context=ssl_context) as pool: response = await pool.request("GET", httpbin_secure.url) assert response.status == 200
async def test_connection_pool_with_close(): """ HTTP/1.1 requests that include a 'Connection: Close' header should not be returned to the connection pool. """ network_backend = AsyncMockBackend( [ b"HTTP/1.1 200 OK\r\n", b"Content-Type: plain/text\r\n", b"Content-Length: 13\r\n", b"\r\n", b"Hello, world!", ] ) async with AsyncConnectionPool(network_backend=network_backend) as pool: # Sending an intial request, which once complete will not return to the pool. async with pool.stream( "GET", "https://example.com/", headers={"Connection": "close"} ) as response: info = [repr(c) for c in pool.connections] assert info == [ "<AsyncHTTPConnection ['https://example.com:443', HTTP/1.1, ACTIVE, Request Count: 1]>" ] await response.aread() assert response.status == 200 assert response.content == b"Hello, world!" info = [repr(c) for c in pool.connections] assert info == []
async def test_connection_pool_with_no_keepalive_connections_allowed(): """ When 'max_keepalive_connections=0' is used, IDLE connections should not be returned to the pool. """ network_backend = AsyncMockBackend( [ b"HTTP/1.1 200 OK\r\n", b"Content-Type: plain/text\r\n", b"Content-Length: 13\r\n", b"\r\n", b"Hello, world!", ] ) async with AsyncConnectionPool( max_keepalive_connections=0, network_backend=network_backend ) as pool: # Sending an intial request, which once complete will not return to the pool. async with pool.stream("GET", "https://example.com/") as response: info = [repr(c) for c in pool.connections] assert info == [ "<AsyncHTTPConnection ['https://example.com:443', HTTP/1.1, ACTIVE, Request Count: 1]>" ] await response.aread() assert response.status == 200 assert response.content == b"Hello, world!" info = [repr(c) for c in pool.connections] assert info == []
async def test_connection_pool_with_immediate_expiry(): """ Connection pools with keepalive_expiry=0.0 should immediately expire keep alive connections. """ network_backend = AsyncMockBackend( [ b"HTTP/1.1 200 OK\r\n", b"Content-Type: plain/text\r\n", b"Content-Length: 13\r\n", b"\r\n", b"Hello, world!", ] ) async with AsyncConnectionPool( keepalive_expiry=0.0, network_backend=network_backend, ) as pool: # Sending an intial request, which once complete will not return to the pool. async with pool.stream("GET", "https://example.com/") as response: info = [repr(c) for c in pool.connections] assert info == [ "<AsyncHTTPConnection ['https://example.com:443', HTTP/1.1, ACTIVE, Request Count: 1]>" ] await response.aread() assert response.status == 200 assert response.content == b"Hello, world!" info = [repr(c) for c in pool.connections] assert info == []
async def test_connection_pool_with_connect_exception(): """ HTTP/1.1 requests that result in an exception during connection should not be returned to the connection pool. """ class FailedConnectBackend(AsyncMockBackend): async def connect_tcp( self, host: str, port: int, timeout: float = None, local_address: str = None ): raise ConnectError("Could not connect") network_backend = FailedConnectBackend([]) called = [] async def trace(name, kwargs): called.append(name) async with AsyncConnectionPool(network_backend=network_backend) as pool: # Sending an initial request, which once complete will not return to the pool. with pytest.raises(Exception): await pool.request( "GET", "https://example.com/", extensions={"trace": trace} ) info = [repr(c) for c in pool.connections] assert info == [] assert called == [ "connection.connect_tcp.started", "connection.connect_tcp.failed", ]
async def close_connections_for_url( connection_pool: httpcore.AsyncConnectionPool, url: httpcore._utils.URL): origin = httpcore._utils.url_to_origin(url) logger.debug('Drop connections for %r', origin) connections_to_close = connection_pool._connections_for_origin(origin) for connection in connections_to_close: await connection_pool._remove_from_pool(connection) try: await connection.aclose() except httpcore.NetworkError as e: logger.warning('Error closing an existing connection', exc_info=e)
async def test_connection_pool_concurrency_same_domain_keepalive(): """ HTTP/1.1 requests made in concurrency must not ever exceed the maximum number of allowable connection in the pool. """ network_backend = AsyncMockBackend( [ b"HTTP/1.1 200 OK\r\n", b"Content-Type: plain/text\r\n", b"Content-Length: 13\r\n", b"\r\n", b"Hello, world!", ] * 5 ) async def fetch(pool, domain, info_list): async with pool.stream("GET", f"https://{domain}/") as response: info = [repr(c) for c in pool.connections] info_list.append(info) await response.aread() async with AsyncConnectionPool( max_connections=1, network_backend=network_backend, http2=True ) as pool: info_list: List[str] = [] async with concurrency.open_nursery() as nursery: for domain in ["a.com", "a.com", "a.com", "a.com", "a.com"]: nursery.start_soon(fetch, pool, domain, info_list) for item in info_list: # Check that each time we inspected the connection pool, only a # single connection was established at any one time. assert len(item) == 1 # The connection sent multiple requests. assert item[0] in [ "<AsyncHTTPConnection ['https://a.com:443', HTTP/1.1, ACTIVE, Request Count: 1]>", "<AsyncHTTPConnection ['https://a.com:443', HTTP/1.1, ACTIVE, Request Count: 2]>", "<AsyncHTTPConnection ['https://a.com:443', HTTP/1.1, ACTIVE, Request Count: 3]>", "<AsyncHTTPConnection ['https://a.com:443', HTTP/1.1, ACTIVE, Request Count: 4]>", "<AsyncHTTPConnection ['https://a.com:443', HTTP/1.1, ACTIVE, Request Count: 5]>", ]
async def test_trace_request(): """ The 'trace' request extension allows for a callback function to inspect the internal events that occur while sending a request. """ network_backend = AsyncMockBackend( [ b"HTTP/1.1 200 OK\r\n", b"Content-Type: plain/text\r\n", b"Content-Length: 13\r\n", b"\r\n", b"Hello, world!", ] ) called = [] async def trace(name, kwargs): called.append(name) async with AsyncConnectionPool(network_backend=network_backend) as pool: await pool.request("GET", "https://example.com/", extensions={"trace": trace}) assert called == [ "connection.connect_tcp.started", "connection.connect_tcp.complete", "connection.start_tls.started", "connection.start_tls.complete", "http11.send_request_headers.started", "http11.send_request_headers.complete", "http11.send_request_body.started", "http11.send_request_body.complete", "http11.receive_response_headers.started", "http11.receive_response_headers.complete", "http11.receive_response_body.started", "http11.receive_response_body.complete", "http11.response_closed.started", "http11.response_closed.complete", ]
async def test_connection_pool_with_http_exception(): """ HTTP/1.1 requests that result in an exception during the connection should not be returned to the connection pool. """ network_backend = AsyncMockBackend([b"Wait, this isn't valid HTTP!"]) called = [] async def trace(name, kwargs): called.append(name) async with AsyncConnectionPool(network_backend=network_backend) as pool: # Sending an initial request, which once complete will not return to the pool. with pytest.raises(Exception): await pool.request( "GET", "https://example.com/", extensions={"trace": trace} ) info = [repr(c) for c in pool.connections] assert info == [] assert called == [ "connection.connect_tcp.started", "connection.connect_tcp.complete", "connection.start_tls.started", "connection.start_tls.complete", "http11.send_request_headers.started", "http11.send_request_headers.complete", "http11.send_request_body.started", "http11.send_request_body.complete", "http11.receive_response_headers.started", "http11.receive_response_headers.failed", "http11.response_closed.started", "http11.response_closed.complete", ]
async def test_request(httpbin): async with AsyncConnectionPool() as pool: response = await pool.request("GET", httpbin.url) assert response.status == 200
async def test_connection_pool_with_keepalive(): """ By default HTTP/1.1 requests should be returned to the connection pool. """ network_backend = AsyncMockBackend( [ b"HTTP/1.1 200 OK\r\n", b"Content-Type: plain/text\r\n", b"Content-Length: 13\r\n", b"\r\n", b"Hello, world!", b"HTTP/1.1 200 OK\r\n", b"Content-Type: plain/text\r\n", b"Content-Length: 13\r\n", b"\r\n", b"Hello, world!", ] ) async with AsyncConnectionPool( network_backend=network_backend, ) as pool: # Sending an intial request, which once complete will return to the pool, IDLE. async with pool.stream("GET", "https://example.com/") as response: info = [repr(c) for c in pool.connections] assert info == [ "<AsyncHTTPConnection ['https://example.com:443', HTTP/1.1, ACTIVE, Request Count: 1]>" ] await response.aread() assert response.status == 200 assert response.content == b"Hello, world!" info = [repr(c) for c in pool.connections] assert info == [ "<AsyncHTTPConnection ['https://example.com:443', HTTP/1.1, IDLE, Request Count: 1]>" ] # Sending a second request to the same origin will reuse the existing IDLE connection. async with pool.stream("GET", "https://example.com/") as response: info = [repr(c) for c in pool.connections] assert info == [ "<AsyncHTTPConnection ['https://example.com:443', HTTP/1.1, ACTIVE, Request Count: 2]>" ] await response.aread() assert response.status == 200 assert response.content == b"Hello, world!" info = [repr(c) for c in pool.connections] assert info == [ "<AsyncHTTPConnection ['https://example.com:443', HTTP/1.1, IDLE, Request Count: 2]>" ] # Sending a request to a different origin will not reuse the existing IDLE connection. async with pool.stream("GET", "http://example.com/") as response: info = [repr(c) for c in pool.connections] assert info == [ "<AsyncHTTPConnection ['http://example.com:80', HTTP/1.1, ACTIVE, Request Count: 1]>", "<AsyncHTTPConnection ['https://example.com:443', HTTP/1.1, IDLE, Request Count: 2]>", ] await response.aread() assert response.status == 200 assert response.content == b"Hello, world!" info = [repr(c) for c in pool.connections] assert info == [ "<AsyncHTTPConnection ['http://example.com:80', HTTP/1.1, IDLE, Request Count: 1]>", "<AsyncHTTPConnection ['https://example.com:443', HTTP/1.1, IDLE, Request Count: 2]>", ]