@@ -1043,6 +1043,154 @@ 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+ bad_request_failed = anyio .Event ()
1088+
1089+ async def run_good_request (session : ClientSession ) -> None :
1090+ nonlocal good_result
1091+ good_result = await session .send_request (
1092+ types .ClientRequest (
1093+ types .ReadResourceRequest (
1094+ params = types .ReadResourceRequestParams (uri = AnyUrl ("foobar://good" )),
1095+ )
1096+ ),
1097+ types .ReadResourceResult ,
1098+ )
1099+
1100+ async def run_bad_request (session : ClientSession ) -> None :
1101+ nonlocal bad_error
1102+ try :
1103+ await session .send_request (
1104+ types .ClientRequest (
1105+ types .ReadResourceRequest (
1106+ params = types .ReadResourceRequestParams (uri = AnyUrl ("foobar://bad" )),
1107+ )
1108+ ),
1109+ types .ReadResourceResult ,
1110+ )
1111+ except Exception as exc :
1112+ bad_error = exc
1113+ bad_request_failed .set ()
1114+
1115+ transport = httpx .MockTransport (handler )
1116+ async with httpx .AsyncClient (transport = transport ) as http_client :
1117+ async with streamable_http_client ("http://test/mcp" , http_client = http_client ) as streams : # pragma: no branch
1118+ read_stream , write_stream , _ = streams
1119+ async with ClientSession (read_stream , write_stream ) as session :
1120+ async with anyio .create_task_group () as tg :
1121+ tg .start_soon (run_good_request , session )
1122+ with anyio .fail_after (5 ):
1123+ await good_request_started .wait ()
1124+
1125+ tg .start_soon (run_bad_request , session )
1126+
1127+ with anyio .fail_after (5 ):
1128+ await bad_request_failed .wait ()
1129+
1130+ allow_good_response .set ()
1131+
1132+ assert isinstance (bad_error , McpError )
1133+ assert bad_error .error .code == types .INTERNAL_ERROR
1134+ assert bad_error .error .message == "Server returned HTTP 400"
1135+ assert bad_error .error .data == {"status_code" : 400 }
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 streams : # pragma: no branch
1175+ read_stream , write_stream , _ = streams
1176+ async with ClientSession (read_stream , write_stream ) as session :
1177+ await session .send_notification (types .ClientNotification (types .RootsListChangedNotification ()))
1178+ with anyio .fail_after (5 ):
1179+ await notification_seen .wait ()
1180+
1181+ result = await session .send_request (
1182+ types .ClientRequest (
1183+ types .ReadResourceRequest (
1184+ params = types .ReadResourceRequestParams (uri = AnyUrl ("foobar://good" )),
1185+ )
1186+ ),
1187+ types .ReadResourceResult ,
1188+ )
1189+
1190+ assert isinstance (result .contents [0 ], types .TextResourceContents )
1191+ assert result .contents [0 ].text == "good response"
1192+
1193+
10461194@pytest .mark .anyio
10471195async def test_streamable_http_client_session_persistence (basic_server : None , basic_server_url : str ):
10481196 """Test that session ID persists across requests."""
0 commit comments