60e0ffc959
Upgrade checks / Notify on failure (push) Has been cancelled
Upgrade checks / Close issue on success (push) Has been cancelled
Schema Crash Test / Real-world schema crash test (232K schemas) (push) Has been cancelled
Run static analysis / static_analysis (push) Has been cancelled
Tests / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Tests / Tests with lowest-direct dependencies (push) Has been cancelled
Tests / MCP conformance tests (push) Has been cancelled
Tests / Integration tests (push) Has been cancelled
Tests / Package install smoke (push) Has been cancelled
Upgrade checks / Static analysis (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Upgrade checks / Integration tests (push) Has been cancelled
Update MCPServerConfig Schema / update-config-schema (push) Has been cancelled
Update SDK Documentation / update-sdk-docs (push) Has been cancelled
37 lines
1.4 KiB
Python
37 lines
1.4 KiB
Python
"""Regression tests for graceful streamable-HTTP shutdown (issue #3025)."""
|
|
|
|
from unittest.mock import AsyncMock
|
|
|
|
from fastmcp.server import FastMCP
|
|
|
|
|
|
async def test_lifespan_terminates_active_transports_before_task_group_cancel():
|
|
"""Active streamable-HTTP transports must be terminated during lifespan
|
|
shutdown so streaming responses end cleanly instead of being aborted by
|
|
the session manager's task-group cancel.
|
|
|
|
See PrefectHQ/fastmcp#3025: without graceful termination, Uvicorn logs
|
|
"ASGI callable returned without completing response." on CTRL+C while a
|
|
client holds an SSE GET stream open.
|
|
"""
|
|
server = FastMCP(name="ShutdownTest")
|
|
app = server.http_app(path="/mcp")
|
|
|
|
fake_transport = AsyncMock()
|
|
fake_transport.terminate = AsyncMock()
|
|
|
|
async with app.router.lifespan_context(app):
|
|
# The session manager is constructed inside the lifespan; once it has
|
|
# started, register a fake active transport as if a client were
|
|
# holding an SSE stream open.
|
|
sm = None
|
|
for route in app.router.routes:
|
|
endpoint = getattr(route, "endpoint", None)
|
|
sm = getattr(endpoint, "session_manager", None)
|
|
if sm is not None:
|
|
break
|
|
assert sm is not None, "streamable-http session manager not initialised"
|
|
sm._server_instances["fake-session"] = fake_transport
|
|
|
|
fake_transport.terminate.assert_awaited_once()
|