1313This can cause connection pool exhaustion in production.
1414"""
1515
16+ from typing import Any
17+
1618import pytest
1719
1820from mcp .client .streamable_http import StreamableHTTPTransport
2123class MockResponse :
2224 """Simple mock to track if aclose() gets called"""
2325
24- def __init__ (self ):
26+ closed : bool
27+ close_count : int
28+ _is_closed : bool
29+
30+ def __init__ (self ) -> None :
2531 self .closed = False
2632 self .close_count = 0
2733 self ._is_closed = False
2834
29- async def aclose (self ):
35+ async def aclose (self ) -> None :
3036 self .closed = True
3137 self .close_count += 1
3238 self ._is_closed = True
3339
3440 @property
35- def is_closed (self ):
41+ def is_closed (self ) -> bool :
3642 return self ._is_closed
3743
3844
3945class MockEventSource :
4046 """Mock that throws an exception to simulate broken SSE"""
4147
42- def __init__ (self , response ) :
48+ def __init__ (self , response : MockResponse ) -> None :
4349 self .response = response
4450
45- def __aiter__ (self ):
51+ def __aiter__ (self ) -> "MockEventSource" :
4652 return self
4753
48- async def __anext__ (self ):
54+ async def __anext__ (self ) -> Any :
4955 # Simulate what happens when SSE parsing fails
5056 raise Exception ("SSE parsing failed - connection broken" )
5157
5258
5359class MockTransport (StreamableHTTPTransport ):
5460 """Mock that shows the same bug as the real code"""
5561
56- def __init__ (self ):
62+ def __init__ (self ) -> None :
5763 super ().__init__ ("http://test" )
5864 self .mock_response = MockResponse ()
5965
60- async def _handle_sse_response (self , response , ctx , is_initialization = False ):
66+ async def _handle_sse_response (
67+ self , response : MockResponse , ctx : Any , is_initialization : bool = False
68+ ) -> None :
6169 """
6270 This mimics the actual bug in the real code.
6371
@@ -66,7 +74,7 @@ async def _handle_sse_response(self, response, ctx, is_initialization=False):
6674 """
6775 try :
6876 event_source = MockEventSource (response )
69- async for sse in event_source :
77+ async for _sse in event_source :
7078 # This never runs because the exception happens first
7179 is_complete = False # Simulate event processing
7280 if is_complete :
@@ -81,13 +89,13 @@ class TestStreamableHTTPResourceLeak:
8189 """Tests for the resource leak I found in streamable HTTP"""
8290
8391 @pytest .mark .anyio
84- async def test_handle_sse_response_resource_leak (self ):
92+ async def test_handle_sse_response_resource_leak (self ) -> None :
8593 """Test that _handle_sse_response leaks resources when SSE fails"""
8694 transport = MockTransport ()
8795
8896 # Create mock context
8997 class MockContext :
90- def __init__ (self ):
98+ def __init__ (self ) -> None :
9199 self .read_stream_writer = None
92100 self .metadata = None
93101
@@ -106,33 +114,33 @@ def __init__(self):
106114 )
107115
108116 @pytest .mark .anyio
109- async def test_handle_resumption_request_resource_leak (self ):
117+ async def test_handle_resumption_request_resource_leak (self ) -> None :
110118 """Test that _handle_resumption_request leaks resources when SSE fails"""
111119 transport = MockTransport ()
112120
113121 # Override the method to reproduce the bug
114- async def mock_handle_resumption_request (ctx ) :
122+ async def mock_handle_resumption_request (ctx : Any ) -> None :
115123 try :
116124 # Mock aconnect_sse context manager
117125 class MockEventSourceWithResponse :
118- def __init__ (self , response ) :
126+ def __init__ (self , response : MockResponse ) -> None :
119127 self .response = response
120128
121- async def __aenter__ (self ):
129+ async def __aenter__ (self ) -> "MockEventSourceWithResponse" :
122130 return self
123131
124- async def __aexit__ (self , exc_type , exc_val , exc_tb ) :
132+ async def __aexit__ (self , exc_type : Any , exc_val : Any , exc_tb : Any ) -> None :
125133 # Even if context manager exits, the response might not be closed
126134 pass
127135
128- def __aiter__ (self ):
136+ def __aiter__ (self ) -> "MockEventSourceWithResponse" :
129137 return self
130138
131- async def __anext__ (self ):
139+ async def __anext__ (self ) -> Any :
132140 raise Exception ("Resumption SSE parsing failed" )
133141
134142 async with MockEventSourceWithResponse (transport .mock_response ) as event_source :
135- async for sse in event_source :
143+ async for _sse in event_source :
136144 # This code will never be reached due to the exception
137145 is_complete = False
138146 if is_complete :
@@ -144,7 +152,7 @@ async def __anext__(self):
144152
145153 # Create mock context with resumption token
146154 class MockResumptionContext :
147- def __init__ (self ):
155+ def __init__ (self ) -> None :
148156 self .read_stream_writer = None
149157 self .metadata = type ("obj" , (object ,), {"resumption_token" : "test-token" })()
150158 self .session_message = type (
@@ -168,23 +176,23 @@ def __init__(self):
168176 )
169177
170178 @pytest .mark .anyio
171- async def test_resource_leak_fix_verification (self ):
179+ async def test_resource_leak_fix_verification (self ) -> None :
172180 """Test that shows how the fix should work"""
173181 transport = MockTransport ()
174182
175183 # Create mock context
176184 class MockContext :
177- def __init__ (self ):
185+ def __init__ (self ) -> None :
178186 self .read_stream_writer = None
179187 self .metadata = None
180188
181189 ctx = MockContext ()
182190
183191 # Simulate the FIXED version with finally block
184- async def fixed_handle_sse_response (response , ctx , is_initialization = False ):
192+ async def fixed_handle_sse_response (response : MockResponse , ctx : Any , is_initialization : bool = False ) -> None :
185193 try :
186194 event_source = MockEventSource (response )
187- async for sse in event_source :
195+ async for _sse in event_source :
188196 # This code will never be reached due to the exception
189197 is_complete = False # Simulate event processing
190198 if is_complete :
0 commit comments