@@ -1063,6 +1063,115 @@ async def create_connection(
10631063 established_connection .close ()
10641064
10651065
1066+ @pytest .mark .parametrize (
1067+ ("request_url" ),
1068+ [
1069+ ("http://mocked.host" ),
1070+ ("https://mocked.host" ),
1071+ ],
1072+ )
1073+ async def test_tcp_connector_multiple_hosts_one_timeout (
1074+ loop : asyncio .AbstractEventLoop ,
1075+ request_url : str ,
1076+ ) -> None :
1077+ conn = aiohttp .TCPConnector ()
1078+
1079+ ip1 = "192.168.1.1"
1080+ ip2 = "192.168.1.2"
1081+ ips = [ip1 , ip2 ]
1082+ ips_tried = []
1083+ ips_success = []
1084+ timeout_error = False
1085+ connected = False
1086+
1087+ req = ClientRequest (
1088+ "GET" ,
1089+ URL (request_url ),
1090+ loop = loop ,
1091+ )
1092+
1093+ async def _resolve_host (
1094+ host : str , port : int , traces : object = None
1095+ ) -> List [ResolveResult ]:
1096+ return [
1097+ {
1098+ "hostname" : host ,
1099+ "host" : ip ,
1100+ "port" : port ,
1101+ "family" : socket .AF_INET6 if ":" in ip else socket .AF_INET ,
1102+ "proto" : 0 ,
1103+ "flags" : socket .AI_NUMERICHOST ,
1104+ }
1105+ for ip in ips
1106+ ]
1107+
1108+ async def start_connection (
1109+ addr_infos : Sequence [AddrInfoType ],
1110+ * ,
1111+ interleave : Optional [int ] = None ,
1112+ ** kwargs : object ,
1113+ ) -> socket .socket :
1114+ nonlocal timeout_error
1115+
1116+ addr_info = addr_infos [0 ]
1117+ addr_info_addr = addr_info [- 1 ]
1118+
1119+ ip = addr_info_addr [0 ]
1120+ ips_tried .append (ip )
1121+
1122+ if ip == ip1 :
1123+ timeout_error = True
1124+ raise asyncio .TimeoutError
1125+
1126+ if ip == ip2 :
1127+ mock_socket = mock .create_autospec (
1128+ socket .socket , spec_set = True , instance = True
1129+ )
1130+ mock_socket .getpeername .return_value = addr_info_addr
1131+ return mock_socket # type: ignore[no-any-return]
1132+
1133+ assert False
1134+
1135+ async def create_connection (
1136+ * args : object , sock : Optional [socket .socket ] = None , ** kwargs : object
1137+ ) -> Tuple [ResponseHandler , ResponseHandler ]:
1138+ nonlocal connected
1139+
1140+ assert isinstance (sock , socket .socket )
1141+ addr_info = sock .getpeername ()
1142+ ip = addr_info [0 ]
1143+ ips_success .append (ip )
1144+ connected = True
1145+
1146+ # Close the socket since we are not actually connecting
1147+ # and we don't want to leak it.
1148+ sock .close ()
1149+ tr = create_mocked_conn (loop )
1150+ pr = create_mocked_conn (loop )
1151+ return tr , pr
1152+
1153+ with mock .patch .object (
1154+ conn , "_resolve_host" , autospec = True , spec_set = True , side_effect = _resolve_host
1155+ ), mock .patch .object (
1156+ conn ._loop ,
1157+ "create_connection" ,
1158+ autospec = True ,
1159+ spec_set = True ,
1160+ side_effect = create_connection ,
1161+ ), mock .patch (
1162+ "aiohttp.connector.aiohappyeyeballs.start_connection" , start_connection
1163+ ):
1164+ established_connection = await conn .connect (req , [], ClientTimeout ())
1165+
1166+ assert ips_tried == ips
1167+ assert ips_success == [ip2 ]
1168+
1169+ assert timeout_error
1170+ assert connected
1171+
1172+ established_connection .close ()
1173+
1174+
10661175async def test_tcp_connector_resolve_host (loop : asyncio .AbstractEventLoop ) -> None :
10671176 conn = aiohttp .TCPConnector (use_dns_cache = True )
10681177
0 commit comments