Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 6804566

Browse files
test: Add regression test for stateless request memory cleanup
This test verifies that PR #1116's fix properly cleans up transport resources for stateless requests. The test mocks StreamableHTTPServerTransport to track when _terminate_session() is called and ensures it's invoked for each stateless request to prevent memory leaks. Without the fix (commenting out the _terminate_session() call), this test fails, confirming it properly detects the memory leak issue. Github-Issue:#1116
1 parent b8cb367 commit 6804566

File tree

1 file changed

+83
-0
lines changed

1 file changed

+83
-0
lines changed

tests/server/test_streamable_http_manager.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,3 +197,86 @@ async def mock_receive():
197197
"Session ID should be removed from _server_instances after an exception"
198198
)
199199
assert not manager._server_instances, "No sessions should be tracked after the only session crashes"
200+
201+
202+
@pytest.mark.anyio
203+
async def test_stateless_requests_cleanup_transport_resources():
204+
"""Test that stateless requests properly clean up transport resources."""
205+
from contextlib import asynccontextmanager
206+
from unittest.mock import patch
207+
208+
app = Server("test-stateless-cleanup-server")
209+
manager = StreamableHTTPSessionManager(app=app, stateless=True)
210+
211+
# Track created transports and their termination
212+
created_transports = []
213+
214+
# Mock the transport class to track termination
215+
with patch("mcp.server.streamable_http_manager.StreamableHTTPServerTransport") as MockTransport:
216+
# Create a mock transport instance
217+
def create_transport(*args, **kwargs):
218+
transport = AsyncMock()
219+
transport._terminated = False
220+
221+
# Track when terminate is called
222+
async def mock_terminate():
223+
transport._terminated = True
224+
225+
transport._terminate_session = mock_terminate
226+
transport.handle_request = AsyncMock()
227+
228+
# Mock the connect context manager
229+
@asynccontextmanager
230+
async def mock_connect():
231+
yield (AsyncMock(), AsyncMock())
232+
233+
transport.connect = mock_connect
234+
235+
created_transports.append(transport)
236+
return transport
237+
238+
MockTransport.side_effect = create_transport
239+
240+
async with manager.run():
241+
# Mock app.run to return quickly
242+
mock_mcp_run = AsyncMock(return_value=None)
243+
app.run = mock_mcp_run
244+
245+
# Send a stateless request
246+
sent_messages = []
247+
248+
async def mock_send(message):
249+
sent_messages.append(message)
250+
251+
scope = {
252+
"type": "http",
253+
"method": "POST",
254+
"path": "/mcp",
255+
"headers": [
256+
(b"content-type", b"application/json"),
257+
(b"accept", b"application/json, text/event-stream"),
258+
],
259+
}
260+
261+
async def mock_receive():
262+
return {
263+
"type": "http.request",
264+
"body": b'{"jsonrpc": "2.0", "method": "test", "id": 1}',
265+
"more_body": False,
266+
}
267+
268+
# Send multiple requests
269+
num_requests = 3
270+
for _ in range(num_requests):
271+
await manager.handle_request(scope, mock_receive, mock_send)
272+
# Give async tasks time to complete
273+
await anyio.sleep(0.1)
274+
275+
# Verify each transport was created
276+
assert len(created_transports) == num_requests, (
277+
f"Expected {num_requests} transports, got {len(created_transports)}"
278+
)
279+
280+
# This is the key assertion - each transport should have been terminated
281+
for i, transport in enumerate(created_transports):
282+
assert transport._terminated, f"Transport {i} was not terminated"

0 commit comments

Comments
 (0)