RuntimeError: Unexpected ASGI message ‘http.response.start’ sent, after response already completed.

该报错通常出现在有状态 StreamableHTTPServerTransport 处理通知时:服务端已经完成 202 Accepted 响应,但随后向已关闭的会话 writer 转发通知失败,外层异常处理又尝试发送 500 ,从而触发第二轮 http.response.start 。优先排查 se

快速结论:该报错通常出现在有状态 StreamableHTTPServerTransport 处理通知时:服务端已经完成 202 Accepted 响应,但随后向已关闭的会话 writer 转发通知失败,外层异常处理又尝试发送 500,从而触发第二轮 http.response.start。优先排查 session writer 是否已关闭,以及异常路径是否在响应完成后再次写响应。

适用环境:Issue 中确认可复现于 mcp==2.3.0,并在生产环境的 mcp==2.2.0 观察过相同异常链;复现环境为 Python 3.11、anyio 4.14、starlette 1.0、uvicorn 0.46。发布线为 2.x 当前稳定版。

最快修复方案:暂无确认的一步修复方案。Issue 被关闭为 #3631 的重复项,尚未在本 Issue 中合并已验证的修复。可优先尝试社区建议的方向:在通知路径的 writer.send(session_message) 周围捕获 anyio.ClosedResourceError,或在 _handle_post_request 中跟踪响应是否已完成,已完成后不要再次构建并发送 500,仅记录异常。

注意事项:上述修复方向来自 Issue 评论中的建议和分支,并未在本 Issue 中确认为已合入上游的最终方案。对调用方而言 202 已经返回,实际可见影响主要是未处理的服务器异常和误导性的错误遥测,而不是 HTTP 响应本身变化。修改异常处理逻辑时需避免掩盖真正需要在响应未完成时返回 500 的错误。

问题场景

使用 MCP Python SDK 的 StreamableHTTPServerTransport 有状态模式下,服务端收到通知(例如 notifications/initialized)时,如果会话的 incoming writer 已关闭,_handle_post_request 会先发送并完成 202 Accepted,随后调用 writer.send(session_message) 转发通知。writer 关闭会抛出 anyio.ClosedResourceError,外层异常处理尝试用同一个 ASGI send 构造 500 响应,Uvicorn 因响应已完成而拒绝第二次 http.response.start。报告中说明可在不涉及 FastMCP 应用行为的情况下直接复现,并在生产环境观察到相同异常链。

报错原文

anyio.ClosedResourceError
  at streamable_http.py:629
    await writer.send(session_message)

During handling of the above exception:

RuntimeError: Unexpected ASGI message 'http.response.start' sent, after response already completed.
  at streamable_http.py:724
    await response(scope, receive, send)

原因分析

最可能的原因是通知转发发生在 202 Accepted 已经完成之后:_handle_post_request 先完成确认响应,再向会话 writer 发送通知。当 session writer 已关闭时,writer.send() 抛出 anyio.ClosedResourceError,外层异常处理没有识别“响应已完成”这一状态,仍然尝试发送 500,导致 Uvicorn 抛出第二个 http.response.start 相关的 RuntimeError。Issue 中也提到这可能与 #2741 的会话生命周期行为有关,但本报告聚焦于通知已被确认后再次发送 ASGI 响应的问题。

环境排查

  • 确认 MCP Python SDK 发布线:2.x 当前稳定版;已在 mcp==2.3.0 复现,并在 mcp==2.2.0 生产环境观察到相同异常链。
  • 确认 Python 版本:复现记录为 Python 3.11。
  • 确认 anyio 版本:复现记录为 anyio 4.14。
  • 确认 Starlette 版本:复现记录为 starlette 1.0。
  • 确认 Uvicorn 版本:复现记录为 uvicorn 0.46。
  • 确认是否为有状态 StreamableHTTPServerTransport,并检查会话 incoming writer 是否在通知到达前已经关闭。
  • 确认异常是否发生在通知路径:先完成 202,再调用 writer.send(session_message),随后进入外层异常处理并尝试第二个响应。

解决步骤

  1. 先确认当前使用的 MCP Python SDK 版本和发布线,并核对是否与 Issue 中的 mcp==2.3.0 或生产环境的 mcp==2.2.0 一致。
  2. 检查服务端异常栈中是否同时出现 anyio.ClosedResourceError 和 RuntimeError: Unexpected ASGI message 'http.response.start' sent, after response already completed.,并确认前者发生在 writer.send(session_message),后者发生在外层异常处理尝试发送响应时。
  3. 确认触发路径为通知处理:_handle_post_request 已经完成 202 Accepted,之后通知转发失败。
  4. 可优先尝试在通知路径的 writer.send(session_message) 周围捕获 anyio.ClosedResourceError,避免继续落入外层的 500 响应分支。
  5. 可优先尝试在 _handle_post_request 中跟踪响应是否已完成;在 except 路径中如果响应已经完成,则跳过构建和发送 500,仅使用 logger.exception() 记录异常,未完成路径保持原逻辑不变。
  6. 如果采用修复分支或补丁,建议补充回归测试:在 tests/server/test_streamable_http_router.py 中使用内存 transport,关闭 session stream,发送 notifications/initialized,断言只出现一次 http.response.start。
  7. 关注上游 #3631,因为本 Issue 已被关闭为该问题的重复项;确认最终修复是否合入目标版本。

验证方法

复现相同通知路径时,服务端应只完成一次 202 Accepted 响应,不再出现第二次 http.response.start,也不再抛出 RuntimeError: Unexpected ASGI message 'http.response.start' sent, after response already completed.。如果采用“响应完成后仅记录异常”的方案,日志中可能仍会看到 anyio.ClosedResourceError 或相应异常记录,但不应再产生未处理的第二个响应异常。回归测试中应断言单次 http.response.start。

参考来源

modelcontextprotocol/python-sdk #3651

modelcontextprotocol/python-sdk #3631

modelcontextprotocol/python-sdk #2741

GamsGo AI

AI 工具推荐

想把多个 AI 模型放在一个入口?

GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。

了解 GamsGo AI

推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。

这个方案解决了吗?

celebrityanime
celebrityanime
文章: 28973

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注