@@ -1043,6 +1043,157 @@ async def test_streamable_http_client_error_handling(initialized_client_session:
10431043 assert "Unknown resource: unknown://test-error" in exc_info .value .error .message
10441044
10451045
1046+ @pytest .mark .anyio
1047+ async def test_streamable_http_client_http_error_does_not_cancel_concurrent_request ():
1048+ """Test that one POST HTTP error does not tear down an unrelated request."""
1049+ good_request_started = anyio .Event ()
1050+ allow_good_response = anyio .Event ()
1051+
1052+ async def handler (request : httpx .Request ) -> httpx .Response :
1053+ payload = json .loads (request .content )
1054+ request_id = payload ["id" ]
1055+ uri = payload ["params" ]["uri" ]
1056+
1057+ if uri == "foobar://bad" :
1058+ with anyio .fail_after (5 ):
1059+ await good_request_started .wait ()
1060+ return httpx .Response (400 , request = request , json = {"error" : "boom" })
1061+
1062+ assert uri == "foobar://good"
1063+ good_request_started .set ()
1064+ with anyio .fail_after (5 ):
1065+ await allow_good_response .wait ()
1066+ return httpx .Response (
1067+ 200 ,
1068+ request = request ,
1069+ headers = {"content-type" : "application/json" },
1070+ json = {
1071+ "jsonrpc" : "2.0" ,
1072+ "id" : request_id ,
1073+ "result" : {
1074+ "contents" : [
1075+ {
1076+ "uri" : uri ,
1077+ "mimeType" : "text/plain" ,
1078+ "text" : "good response" ,
1079+ }
1080+ ]
1081+ },
1082+ },
1083+ )
1084+
1085+ good_result : types .ReadResourceResult | None = None
1086+ bad_error : Exception | None = None
1087+
1088+ async def run_good_request (session : ClientSession ) -> None :
1089+ nonlocal good_result
1090+ good_result = await session .send_request (
1091+ types .ClientRequest (
1092+ types .ReadResourceRequest (
1093+ params = types .ReadResourceRequestParams (uri = AnyUrl ("foobar://good" )),
1094+ )
1095+ ),
1096+ types .ReadResourceResult ,
1097+ )
1098+
1099+ async def run_bad_request (session : ClientSession ) -> None :
1100+ nonlocal bad_error
1101+ try :
1102+ await session .send_request (
1103+ types .ClientRequest (
1104+ types .ReadResourceRequest (
1105+ params = types .ReadResourceRequestParams (uri = AnyUrl ("foobar://bad" )),
1106+ )
1107+ ),
1108+ types .ReadResourceResult ,
1109+ )
1110+ except Exception as exc :
1111+ bad_error = exc
1112+
1113+ transport = httpx .MockTransport (handler )
1114+ async with httpx .AsyncClient (transport = transport ) as http_client :
1115+ async with streamable_http_client ("http://test/mcp" , http_client = http_client ) as (
1116+ read_stream ,
1117+ write_stream ,
1118+ _ ,
1119+ ):
1120+ async with ClientSession (read_stream , write_stream ) as session :
1121+ async with anyio .create_task_group () as tg :
1122+ tg .start_soon (run_good_request , session )
1123+ with anyio .fail_after (5 ):
1124+ await good_request_started .wait ()
1125+
1126+ tg .start_soon (run_bad_request , session )
1127+
1128+ with anyio .fail_after (5 ):
1129+ while bad_error is None :
1130+ await anyio .sleep (0 )
1131+
1132+ allow_good_response .set ()
1133+
1134+ assert isinstance (bad_error , McpError )
1135+ assert bad_error .error .code == types .INTERNAL_ERROR
1136+ assert good_result is not None
1137+ assert isinstance (good_result .contents [0 ], types .TextResourceContents )
1138+ assert good_result .contents [0 ].text == "good response"
1139+
1140+
1141+ @pytest .mark .anyio
1142+ async def test_streamable_http_client_notification_http_error_does_not_cancel_transport ():
1143+ """Test POST HTTP errors for notifications do not synthesize responses."""
1144+ notification_seen = anyio .Event ()
1145+
1146+ async def handler (request : httpx .Request ) -> httpx .Response :
1147+ payload = json .loads (request .content )
1148+
1149+ if "id" not in payload :
1150+ notification_seen .set ()
1151+ return httpx .Response (500 , request = request , json = {"error" : "boom" })
1152+
1153+ return httpx .Response (
1154+ 200 ,
1155+ request = request ,
1156+ headers = {"content-type" : "application/json" },
1157+ json = {
1158+ "jsonrpc" : "2.0" ,
1159+ "id" : payload ["id" ],
1160+ "result" : {
1161+ "contents" : [
1162+ {
1163+ "uri" : "foobar://good" ,
1164+ "mimeType" : "text/plain" ,
1165+ "text" : "good response" ,
1166+ }
1167+ ]
1168+ },
1169+ },
1170+ )
1171+
1172+ transport = httpx .MockTransport (handler )
1173+ async with httpx .AsyncClient (transport = transport ) as http_client :
1174+ async with streamable_http_client ("http://test/mcp" , http_client = http_client ) as (
1175+ read_stream ,
1176+ write_stream ,
1177+ _ ,
1178+ ):
1179+ async with ClientSession (read_stream , write_stream ) as session :
1180+ await session .send_notification (types .ClientNotification (types .RootsListChangedNotification ()))
1181+ with anyio .fail_after (5 ):
1182+ await notification_seen .wait ()
1183+
1184+ result = await session .send_request (
1185+ types .ClientRequest (
1186+ types .ReadResourceRequest (
1187+ params = types .ReadResourceRequestParams (uri = AnyUrl ("foobar://good" )),
1188+ )
1189+ ),
1190+ types .ReadResourceResult ,
1191+ )
1192+
1193+ assert isinstance (result .contents [0 ], types .TextResourceContents )
1194+ assert result .contents [0 ].text == "good response"
1195+
1196+
10461197@pytest .mark .anyio
10471198async def test_streamable_http_client_session_persistence (basic_server : None , basic_server_url : str ):
10481199 """Test that session ID persists across requests."""
0 commit comments