@@ -892,3 +892,122 @@ async def test_keepalive_idle_connections():
892892 repr (pool )
893893 == "<AsyncConnectionPool [Requests: 0 active, 0 queued | Connections: 0 active, 1 idle]>"
894894 )
895+
896+
897+ @pytest .mark .anyio
898+ async def test_idle_but_not_available_connection_closing ():
899+ """
900+ Test that a connection that is idle but not available gets closed.
901+ This is a pathological edge case that shouldn't occur in reality, but the connection pool
902+ handles it anyway.
903+ """
904+
905+ class MockIdleNotAvailableConnection (httpcore .AsyncConnectionInterface ):
906+ def __init__ (self ) -> None :
907+ self ._origin = httpcore .Origin (b"https" , b"example.com" , 443 )
908+
909+ def is_available (self ) -> bool :
910+ return False # Not available
911+
912+ def is_idle (self ) -> bool :
913+ return True # But is idle - this is the edge case
914+
915+ def is_closed (self ) -> bool :
916+ return False
917+
918+ def has_expired (self ) -> bool :
919+ return False
920+
921+ network_backend = httpcore .AsyncMockBackend ([])
922+
923+ async with httpcore .AsyncConnectionPool (
924+ network_backend = network_backend ,
925+ ) as pool :
926+ # Replace the connection list with our pathological mock connection
927+ mock_conn = MockIdleNotAvailableConnection ()
928+
929+ pool ._connections = [mock_conn ]
930+
931+ # This should move the idle-but-not-available connection to closing_conns
932+ closing_conns = pool ._assign_requests_to_connections ()
933+
934+ # Verify the connection is marked for closing
935+ assert len (closing_conns ) == 1
936+ assert closing_conns [0 ] is mock_conn
937+
938+ # Verify it's no longer in the connection pool
939+ assert len (pool ._connections ) == 0
940+
941+
942+ @pytest .mark .anyio
943+ async def test_active_http2_connection_keepalive_preservation ():
944+ """
945+ Test that active HTTP/2 connections with capacity are preserved during keepalive enforcement.
946+ This tests line 421 where active HTTP/2 connections are kept during keepalive enforcement.
947+ """
948+
949+ class MockActiveHTTP2Connection (httpcore .AsyncConnectionInterface ):
950+ def __init__ (self ) -> None :
951+ self ._origin = httpcore .Origin (b"https" , b"example.com" , 443 )
952+
953+ def is_available (self ) -> bool :
954+ return True # Available with capacity
955+
956+ def is_idle (self ) -> bool :
957+ return False # Not idle (active HTTP/2 connection with streams)
958+
959+ def is_closed (self ) -> bool :
960+ return False
961+
962+ def has_expired (self ) -> bool :
963+ return False
964+
965+ def get_available_stream_capacity (self ) -> int :
966+ return 5 # HTTP/2 connection with available stream capacity
967+
968+ async def aclose (self ):
969+ pass
970+
971+ class MockIdleConnection (httpcore .AsyncConnectionInterface ):
972+ def __init__ (self ) -> None :
973+ self ._origin = httpcore .Origin (b"https" , b"example.com" , 443 )
974+
975+ def is_available (self ) -> bool :
976+ return True
977+
978+ def is_idle (self ) -> bool :
979+ return True # This is an idle connection
980+
981+ def is_closed (self ) -> bool :
982+ return False
983+
984+ def has_expired (self ) -> bool :
985+ return False
986+
987+ def get_available_stream_capacity (self ) -> int :
988+ return 1
989+
990+ network_backend = httpcore .AsyncMockBackend ([])
991+
992+ async with httpcore .AsyncConnectionPool (
993+ max_keepalive_connections = 0 , # Force keepalive limit to 0
994+ http2 = True ,
995+ network_backend = network_backend ,
996+ ) as pool :
997+ # Create one idle connection and one active HTTP/2 connection
998+ idle_conn = MockIdleConnection ()
999+ active_http2_conn = MockActiveHTTP2Connection ()
1000+
1001+ with pool ._optional_thread_lock :
1002+ pool ._connections = [idle_conn , active_http2_conn ]
1003+
1004+ # This should close idle connections but preserve the active HTTP/2 connection
1005+ closing_conns = pool ._assign_requests_to_connections ()
1006+
1007+ # Verify idle connection is marked for closing
1008+ assert len (closing_conns ) == 1
1009+ assert idle_conn in closing_conns
1010+
1011+ # Verify the active HTTP/2 connection is preserved (line 421)
1012+ assert len (pool ._connections ) == 1
1013+ assert active_http2_conn in pool ._connections
0 commit comments