feat: add idle timeout for StreamableHTTP sessions (#1994)
This commit is contained in:
parent
67458948ae
commit
23a615783e
1 changed files with 135 additions and 20 deletions
|
|
@ -313,3 +313,80 @@ async def test_unknown_session_id_returns_404():
|
|||
assert error_data["id"] == "server-error"
|
||||
assert error_data["error"]["code"] == INVALID_REQUEST
|
||||
assert error_data["error"]["message"] == "Session not found"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_idle_session_is_reaped():
|
||||
"""After idle timeout fires, the session returns 404."""
|
||||
app = Server("test-idle-reap")
|
||||
manager = StreamableHTTPSessionManager(app=app, session_idle_timeout=0.05)
|
||||
|
||||
async with manager.run():
|
||||
sent_messages: list[Message] = []
|
||||
|
||||
async def mock_send(message: Message):
|
||||
sent_messages.append(message)
|
||||
|
||||
scope = {
|
||||
"type": "http",
|
||||
"method": "POST",
|
||||
"path": "/mcp",
|
||||
"headers": [(b"content-type", b"application/json")],
|
||||
}
|
||||
|
||||
async def mock_receive(): # pragma: no cover
|
||||
return {"type": "http.request", "body": b"", "more_body": False}
|
||||
|
||||
await manager.handle_request(scope, mock_receive, mock_send)
|
||||
|
||||
session_id = None
|
||||
for msg in sent_messages: # pragma: no branch
|
||||
if msg["type"] == "http.response.start": # pragma: no branch
|
||||
for header_name, header_value in msg.get("headers", []): # pragma: no branch
|
||||
if header_name.decode().lower() == MCP_SESSION_ID_HEADER.lower():
|
||||
session_id = header_value.decode()
|
||||
break
|
||||
if session_id: # pragma: no branch
|
||||
break
|
||||
|
||||
assert session_id is not None, "Session ID not found in response headers"
|
||||
|
||||
# Wait for the 50ms idle timeout to fire and cleanup to complete
|
||||
await anyio.sleep(0.1)
|
||||
|
||||
# Verify via public API: old session ID now returns 404
|
||||
response_messages: list[Message] = []
|
||||
|
||||
async def capture_send(message: Message):
|
||||
response_messages.append(message)
|
||||
|
||||
scope_with_session = {
|
||||
"type": "http",
|
||||
"method": "POST",
|
||||
"path": "/mcp",
|
||||
"headers": [
|
||||
(b"content-type", b"application/json"),
|
||||
(b"mcp-session-id", session_id.encode()),
|
||||
],
|
||||
}
|
||||
|
||||
await manager.handle_request(scope_with_session, mock_receive, capture_send)
|
||||
|
||||
response_start = next(
|
||||
(msg for msg in response_messages if msg["type"] == "http.response.start"),
|
||||
None,
|
||||
)
|
||||
assert response_start is not None
|
||||
assert response_start["status"] == 404
|
||||
|
||||
|
||||
def test_session_idle_timeout_rejects_non_positive():
|
||||
with pytest.raises(ValueError, match="positive number"):
|
||||
StreamableHTTPSessionManager(app=Server("test"), session_idle_timeout=-1)
|
||||
with pytest.raises(ValueError, match="positive number"):
|
||||
StreamableHTTPSessionManager(app=Server("test"), session_idle_timeout=0)
|
||||
|
||||
|
||||
def test_session_idle_timeout_rejects_stateless():
|
||||
with pytest.raises(RuntimeError, match="not supported in stateless"):
|
||||
StreamableHTTPSessionManager(app=Server("test"), session_idle_timeout=30, stateless=True)
|
||||
|
|
|
|||
Loading…
Reference in a new issue