这是 DEV 夏季 Bug 清除大赛(Summer Bug Smash: Clear the Lineup,由 Sentry 赞助)的参赛提交。 项目概览 Sentry MCP 是一个模型上下文协议(Model Context Protocol)服务器,它把 Sentry 的错误与性能数据开放给 AI 助手使用。仓库里附带了一个小型 CLI 测试客户端(packages/mcp-test-client),通过真实的大语言模型驱动服务器,从而端到端地验证所有工具。本次修复落在该测试客户端的 `runAgent` 函数中,并关闭了由 Sentry 创始人提交的 issue #500。 Bug 定位:错误被“无响应”三个字吞掉了 当客户端遇到提供商错误——无效 API 密钥、触发限流或 5xx 状态码——它只会打印一行 `(No response generated)` 然后继续执行,真正的原因完全丢失。 根源是仓库锁定的 Vercel AI SDK(ai@6)的一个反直觉行为:`streamText` 在请求失败时并不会抛出异常。它会以零个数据块结束 `textStream`,并且只通过 `onError` 回调上报失败,而这段代码从未设置过该回调。于是 `for await` 循环空转结束,`runAgent` 永远进不了 `catch` 分支,Sentry span 始终保持 OK 状态,任何信息都没有被捕获。客户端最终落入那行误导性的“无响应”提示。这个失败对用户不可见,对 Sentry 同样不可见。 动手修复之前,我先针对已安装的 SDK 版本做了探针验证:`textStream` 确实在不抛异常的情况下产出零块,而 `onError` 会携带一个 `statusCode: 401` 的 `APICallError` 触发。issue #500 曾猜测错误可能挂在 `result.errorPromise` 上,但在当前 SDK 版本中该属性并不存在,因此这次修复没有采用这个猜测。 修复方案:接住 onError,让错误走回 catch 客户端现在会先捕获 SDK 通过 `onError` 上报的错误,等流排空后再把错误抛出,让它流经既有的 `catch` 分支: ```typescript let streamError: unknown; const result = await streamText({ model: languageModel, system: SYSTEM_PROMPT, messages: [{ role: "user", content: userPrompt }], tools, stopWhen: stepCountIs(maxSteps), experimental_telemetry: { isEnabled: true }, onError: ({ error }) => { // 提供商错误会在这里到达,而不是作为抛出的异常。 streamError = error; }, }); // ...等待流排空... if (streamError) { throw streamError; } ``` 这样,真实的错误原因会被带进已有的错误处理链路:Sentry span 被标记为失败、原始错误信息得以保留、用户不会再看到那条空洞的“无响应”提示。一次原本静默消失的提供商故障,现在可以被完整地观测和追踪了。
![]()
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.