def test_extra_info(httpbin_secure): ssl_context = ssl.create_default_context() ssl_context.check_hostname = False ssl_context.verify_mode = ssl.CERT_NONE with ConnectionPool(ssl_context=ssl_context) as pool: 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")
def test_unsupported_protocol(): with ConnectionPool() as pool: with pytest.raises(UnsupportedProtocol): pool.request("GET", "ftp://www.example.com/") with pytest.raises(UnsupportedProtocol): pool.request("GET", "://www.example.com/")
def test_ssl_request(httpbin_secure): ssl_context = ssl.create_default_context() ssl_context.check_hostname = False ssl_context.verify_mode = ssl.CERT_NONE with ConnectionPool(ssl_context=ssl_context) as pool: response = pool.request("GET", httpbin_secure.url) assert response.status == 200
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 = MockBackend([ 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!", ]) with ConnectionPool(network_backend=network_backend) as pool: # Sending an intial request, which once complete will not return to the pool. with pool.stream("GET", "https://example.com/", headers={"Connection": "close"}) as response: info = [repr(c) for c in pool.connections] assert info == [ "<HTTPConnection ['https://example.com:443', HTTP/1.1, ACTIVE, Request Count: 1]>" ] response.read() assert response.status == 200 assert response.content == b"Hello, world!" info = [repr(c) for c in pool.connections] assert info == []
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 = MockBackend([ 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!", ]) with ConnectionPool(max_keepalive_connections=0, network_backend=network_backend) as pool: # Sending an intial request, which once complete will not return to the pool. with pool.stream("GET", "https://example.com/") as response: info = [repr(c) for c in pool.connections] assert info == [ "<HTTPConnection ['https://example.com:443', HTTP/1.1, ACTIVE, Request Count: 1]>" ] response.read() assert response.status == 200 assert response.content == b"Hello, world!" info = [repr(c) for c in pool.connections] assert info == []
def test_connection_pool_with_immediate_expiry(): """ Connection pools with keepalive_expiry=0.0 should immediately expire keep alive connections. """ network_backend = MockBackend([ 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!", ]) with ConnectionPool( keepalive_expiry=0.0, network_backend=network_backend, ) as pool: # Sending an intial request, which once complete will not return to the pool. with pool.stream("GET", "https://example.com/") as response: info = [repr(c) for c in pool.connections] assert info == [ "<HTTPConnection ['https://example.com:443', HTTP/1.1, ACTIVE, Request Count: 1]>" ] response.read() assert response.status == 200 assert response.content == b"Hello, world!" info = [repr(c) for c in pool.connections] assert info == []
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(MockBackend): def connect_tcp(self, host: str, port: int, timeout: float = None, local_address: str = None): raise ConnectError("Could not connect") network_backend = FailedConnectBackend([]) called = [] def trace(name, kwargs): called.append(name) with ConnectionPool(network_backend=network_backend) as pool: # Sending an initial request, which once complete will not return to the pool. with pytest.raises(Exception): 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", ]
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 = MockBackend([ 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) def fetch(pool, domain, info_list): with pool.stream("GET", f"https://{domain}/") as response: info = [repr(c) for c in pool.connections] info_list.append(info) response.read() with ConnectionPool(max_connections=1, network_backend=network_backend, http2=True) as pool: info_list: List[str] = [] 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 [ "<HTTPConnection ['https://a.com:443', HTTP/1.1, ACTIVE, Request Count: 1]>", "<HTTPConnection ['https://a.com:443', HTTP/1.1, ACTIVE, Request Count: 2]>", "<HTTPConnection ['https://a.com:443', HTTP/1.1, ACTIVE, Request Count: 3]>", "<HTTPConnection ['https://a.com:443', HTTP/1.1, ACTIVE, Request Count: 4]>", "<HTTPConnection ['https://a.com:443', HTTP/1.1, ACTIVE, Request Count: 5]>", ]
def test_connection_pool_concurrency(): """ HTTP/1.1 requests made in concurrency must not ever exceed the maximum number of allowable connection in the pool. """ network_backend = MockBackend([ 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!", ]) def fetch(pool, domain, info_list): with pool.stream("GET", f"http://{domain}/") as response: info = [repr(c) for c in pool.connections] info_list.append(info) response.read() with ConnectionPool(max_connections=1, network_backend=network_backend) as pool: info_list: List[str] = [] with concurrency.open_nursery() as nursery: for domain in ["a.com", "b.com", "c.com", "d.com", "e.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 # Each connection was to a different host, and only sent a single # request on that connection. assert item[0] in [ "<HTTPConnection ['http://a.com:80', HTTP/1.1, ACTIVE, Request Count: 1]>", "<HTTPConnection ['http://b.com:80', HTTP/1.1, ACTIVE, Request Count: 1]>", "<HTTPConnection ['http://c.com:80', HTTP/1.1, ACTIVE, Request Count: 1]>", "<HTTPConnection ['http://d.com:80', HTTP/1.1, ACTIVE, Request Count: 1]>", "<HTTPConnection ['http://e.com:80', HTTP/1.1, ACTIVE, Request Count: 1]>", ]
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 = MockBackend([ 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 = [] def trace(name, kwargs): called.append(name) with ConnectionPool(network_backend=network_backend) as pool: 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", ]
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 = MockBackend([b"Wait, this isn't valid HTTP!"]) called = [] def trace(name, kwargs): called.append(name) with ConnectionPool(network_backend=network_backend) as pool: # Sending an initial request, which once complete will not return to the pool. with pytest.raises(Exception): 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", ]
def test_request(httpbin): with ConnectionPool() as pool: response = pool.request("GET", httpbin.url) assert response.status == 200
def test_connection_pool_with_keepalive(): """ By default HTTP/1.1 requests should be returned to the connection pool. """ network_backend = MockBackend([ 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!", ]) with ConnectionPool(network_backend=network_backend, ) as pool: # Sending an intial request, which once complete will return to the pool, IDLE. with pool.stream("GET", "https://example.com/") as response: info = [repr(c) for c in pool.connections] assert info == [ "<HTTPConnection ['https://example.com:443', HTTP/1.1, ACTIVE, Request Count: 1]>" ] response.read() assert response.status == 200 assert response.content == b"Hello, world!" info = [repr(c) for c in pool.connections] assert info == [ "<HTTPConnection ['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. with pool.stream("GET", "https://example.com/") as response: info = [repr(c) for c in pool.connections] assert info == [ "<HTTPConnection ['https://example.com:443', HTTP/1.1, ACTIVE, Request Count: 2]>" ] response.read() assert response.status == 200 assert response.content == b"Hello, world!" info = [repr(c) for c in pool.connections] assert info == [ "<HTTPConnection ['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. with pool.stream("GET", "http://example.com/") as response: info = [repr(c) for c in pool.connections] assert info == [ "<HTTPConnection ['http://example.com:80', HTTP/1.1, ACTIVE, Request Count: 1]>", "<HTTPConnection ['https://example.com:443', HTTP/1.1, IDLE, Request Count: 2]>", ] response.read() assert response.status == 200 assert response.content == b"Hello, world!" info = [repr(c) for c in pool.connections] assert info == [ "<HTTPConnection ['http://example.com:80', HTTP/1.1, IDLE, Request Count: 1]>", "<HTTPConnection ['https://example.com:443', HTTP/1.1, IDLE, Request Count: 2]>", ]