async def test_keepalive_timeout(server): """ Keep-alive connections should timeout. """ async with ConnectionPool() as http: response = await http.request("GET", server.url) await response.aread() assert len(http.active_connections) == 0 assert len(http.keepalive_connections) == 1 http.next_keepalive_check = 0.0 await http.check_keepalive_expiry() assert len(http.active_connections) == 0 assert len(http.keepalive_connections) == 1 async with ConnectionPool() as http: http.KEEP_ALIVE_EXPIRY = 0.0 response = await http.request("GET", server.url) await response.aread() assert len(http.active_connections) == 0 assert len(http.keepalive_connections) == 1 http.next_keepalive_check = 0.0 await http.check_keepalive_expiry() assert len(http.active_connections) == 0 assert len(http.keepalive_connections) == 0
async def test_http2_settings_in_handshake(): backend = MockHTTP2Backend(app=app) dispatch = ConnectionPool(backend=backend, http2=True) async with AsyncClient(dispatch=dispatch) as client: await client.get("http://example.org") h2_conn = backend.server.conn assert isinstance(h2_conn, h2.connection.H2Connection) expected_settings = { SettingCodes.HEADER_TABLE_SIZE: 4096, SettingCodes.ENABLE_PUSH: 0, SettingCodes.MAX_CONCURRENT_STREAMS: 100, SettingCodes.INITIAL_WINDOW_SIZE: 65535, SettingCodes.MAX_FRAME_SIZE: 16384, SettingCodes.MAX_HEADER_LIST_SIZE: 65536, # This one's here because h2 helpfully populates remote_settings # with default values even if the peer doesn't send the setting. SettingCodes.ENABLE_CONNECT_PROTOCOL: 0, } assert dict(h2_conn.remote_settings) == expected_settings # We don't expect the ENABLE_CONNECT_PROTOCOL to be in the handshake expected_settings.pop(SettingCodes.ENABLE_CONNECT_PROTOCOL) assert len(backend.server.settings_changed) == 1 settings = backend.server.settings_changed[0] assert isinstance(settings, h2.events.RemoteSettingsChanged) assert len(settings.changed_settings) == len(expected_settings) for setting_code, changed_setting in settings.changed_settings.items(): assert isinstance(changed_setting, h2.settings.ChangedSetting) assert changed_setting.new_value == expected_settings[setting_code]
async def test_http2_get_request(): backend = MockHTTP2Backend(app=app) dispatch = ConnectionPool(backend=backend, http2=True) async with AsyncClient(dispatch=dispatch) as client: response = await client.get("http://example.org") assert response.status_code == 200 assert json.loads(response.content) == {"method": "GET", "path": "/", "body": ""}
async def test_premature_response_close(server): """ A premature close should close the connection. """ async with ConnectionPool() as http: response = await http.request("GET", server.url) await response.aclose() assert len(http.active_connections) == 0 assert len(http.keepalive_connections) == 0
async def test_close_connections(server): """ Using a `Connection: close` header should close the connection. """ headers = [(b"connection", b"close")] async with ConnectionPool() as http: response = await http.request("GET", server.url, headers=headers) await response.aread() assert len(http.active_connections) == 0 assert len(http.keepalive_connections) == 0
async def test_standard_response_close(server): """ A standard close should keep the connection open. """ async with ConnectionPool() as http: response = await http.request("GET", server.url) await response.aread() await response.aclose() assert len(http.active_connections) == 0 assert len(http.keepalive_connections) == 1
async def test_streaming_response_holds_connection(server): """ A streaming request should hold the connection open until the response is read. """ async with ConnectionPool() as http: response = await http.request("GET", server.url) assert len(http.active_connections) == 1 assert len(http.keepalive_connections) == 0 await response.aread() assert len(http.active_connections) == 0 assert len(http.keepalive_connections) == 1
async def test_http2_large_post_request(): backend = MockHTTP2Backend(app=app) dispatch = ConnectionPool(backend=backend, http2=True) data = b"a" * 100000 async with AsyncClient(dispatch=dispatch) as client: response = await client.post("http://example.org", data=data) assert response.status_code == 200 assert json.loads(response.content) == { "method": "POST", "path": "/", "body": data.decode(), }
async def test_keepalive_connections(server): """ Connections should default to staying in a keep-alive state. """ async with ConnectionPool() as http: response = await http.request("GET", server.url) await response.aread() assert len(http.active_connections) == 0 assert len(http.keepalive_connections) == 1 response = await http.request("GET", server.url) await response.aread() assert len(http.active_connections) == 0 assert len(http.keepalive_connections) == 1
async def test_differing_connection_keys(server): """ Connections to differing connection keys should result in multiple connections. """ async with ConnectionPool() as http: response = await http.request("GET", server.url) await response.aread() assert len(http.active_connections) == 0 assert len(http.keepalive_connections) == 1 response = await http.request("GET", "http://localhost:8000/") await response.aread() assert len(http.active_connections) == 0 assert len(http.keepalive_connections) == 2
async def test_connection_closed_free_semaphore_on_acquire(server): """ Verify that max_connections semaphore is released properly on a disconnected connection. """ async with ConnectionPool(pool_limits=httpx.PoolLimits( hard_limit=1)) as http: response = await http.request("GET", server.url) await response.aread() # Close the connection so we're forced to recycle it await server.restart() response = await http.request("GET", server.url) assert response.status_code == 200
async def test_soft_limit(server): """ The soft_limit config should limit the maximum number of keep-alive connections. """ pool_limits = httpx.PoolLimits(soft_limit=1) async with ConnectionPool(pool_limits=pool_limits) as http: response = await http.request("GET", server.url) await response.aread() assert len(http.active_connections) == 0 assert len(http.keepalive_connections) == 1 response = await http.request("GET", "http://localhost:8000/") await response.aread() assert len(http.active_connections) == 0 assert len(http.keepalive_connections) == 1
async def test_keepalive_connection_closed_by_server_is_reestablished(server): """ Upon keep-alive connection closed by remote a new connection should be reestablished. """ async with ConnectionPool() as http: response = await http.request("GET", server.url) await response.aread() # Shutdown the server to close the keep-alive connection await server.restart() response = await http.request("GET", server.url) await response.aread() assert len(http.active_connections) == 0 assert len(http.keepalive_connections) == 1
async def test_http2_multiple_requests(): backend = MockHTTP2Backend(app=app) dispatch = ConnectionPool(backend=backend, http2=True) async with AsyncClient(dispatch=dispatch) as client: response_1 = await client.get("http://example.org/1") response_2 = await client.get("http://example.org/2") response_3 = await client.get("http://example.org/3") assert response_1.status_code == 200 assert json.loads(response_1.content) == {"method": "GET", "path": "/1", "body": ""} assert response_2.status_code == 200 assert json.loads(response_2.content) == {"method": "GET", "path": "/2", "body": ""} assert response_3.status_code == 200 assert json.loads(response_3.content) == {"method": "GET", "path": "/3", "body": ""}
async def test_http2_reconnect(): """ If a connection has been dropped between requests, then we should be seemlessly reconnected. """ backend = MockHTTP2Backend(app=app) dispatch = ConnectionPool(backend=backend, http2=True) async with AsyncClient(dispatch=dispatch) as client: response_1 = await client.get("http://example.org/1") backend.server.close_connection = True response_2 = await client.get("http://example.org/2") assert response_1.status_code == 200 assert json.loads(response_1.content) == {"method": "GET", "path": "/1", "body": ""} assert response_2.status_code == 200 assert json.loads(response_2.content) == {"method": "GET", "path": "/2", "body": ""}
async def test_multiple_concurrent_connections(server): """ Multiple concurrent requests should open multiple concurrent connections. """ async with ConnectionPool() as http: response_a = await http.request("GET", server.url) assert len(http.active_connections) == 1 assert len(http.keepalive_connections) == 0 response_b = await http.request("GET", server.url) assert len(http.active_connections) == 2 assert len(http.keepalive_connections) == 0 await response_b.aread() assert len(http.active_connections) == 1 assert len(http.keepalive_connections) == 1 await response_a.aread() assert len(http.active_connections) == 0 assert len(http.keepalive_connections) == 2
async def test_connection_pool_closed_close_keepalive_and_free_semaphore( server): """ Closing the connection pool should close remaining keepalive connections and release the max_connections semaphore. """ http = ConnectionPool(pool_limits=httpx.PoolLimits(hard_limit=1)) async with http: response = await http.request("GET", server.url) await response.aread() assert response.status_code == 200 assert len(http.keepalive_connections) == 1 assert len(http.keepalive_connections) == 0 # Perform a second round of requests to make sure the max_connections semaphore # was released properly. async with http: response = await http.request("GET", server.url) await response.aread() assert response.status_code == 200