Skip to content

Commit 4ce6ec4

Browse files
Fix type annotations and rename reproduction script
- Add proper type annotations to all mock classes and methods - Fix Pyright type checking errors - Rename test_resource_leak_reproduction.py to resource_leak_reproduction.py to prevent pytest from treating it as a test file - Remove unused AsyncIterator import
1 parent 37b19c6 commit 4ce6ec4

File tree

2 files changed

+32
-24
lines changed

2 files changed

+32
-24
lines changed
File renamed without changes.

tests/client/test_streamable_http_resource_leak.py

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
This can cause connection pool exhaustion in production.
1414
"""
1515

16+
from typing import Any
17+
1618
import pytest
1719

1820
from mcp.client.streamable_http import StreamableHTTPTransport
@@ -21,43 +23,49 @@
2123
class 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

3945
class 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

5359
class 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

Comments
 (0)