{"record":{"id":"f57ce9d98c6002da","repo":"can1357/oh-my-pi","slug":"http-response-status-resuming-mcp-sse-stream-a","errorCode":null,"errorMessage":"HTTP ${response.status} resuming MCP SSE stream: auth refresh failed","messagePattern":"HTTP (.+?) resuming MCP SSE stream: auth refresh failed","errorType":"http","errorClass":"SSEResumeError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/mcp/transports/http.ts","lineNumber":355,"sourceCode":"\t * Failures throw {@link SSEResumeError} so `request()` never replays the\n\t * originating POST in response.\n\t */\n\tasync #fetchSSEResume(resume: SSEResumeState, signal: AbortSignal): Promise<Response> {\n\t\tif (resume.lastEventId === null) {\n\t\t\tthrow new SSEResumeError(\"SSE stream ended without a resumable event ID\");\n\t\t}\n\t\tawait waitForSSERetry(resume.retryMs, signal);\n\t\tconst generated: Record<string, string> = {\n\t\t\tAccept: \"text/event-stream\",\n\t\t\t\"Last-Event-ID\": resume.lastEventId,\n\t\t};\n\t\tif (this.#sessionId) generated[\"Mcp-Session-Id\"] = this.#sessionId;\n\t\tlet response = await this.#fetch({ method: \"GET\", signal }, generated);\n\t\tif (this.onAuthError && (response.status === 401 || response.status === 403)) {\n\t\t\tawait response.body?.cancel();\n\t\t\tconst newHeaders = await this.onAuthError();\n\t\t\tif (!newHeaders) {\n\t\t\t\tthrow new SSEResumeError(`HTTP ${response.status} resuming MCP SSE stream: auth refresh failed`);\n\t\t\t}\n\t\t\t// Persist refreshed headers so subsequent requests use them directly\n\t\t\tthis.config = { ...this.config, headers: newHeaders };\n\t\t\tresponse = await this.#fetch({ method: \"GET\", signal }, generated);\n\t\t}\n\t\tif (!response.ok) {\n\t\t\tconst text = await response.text().catch(() => \"\");\n\t\t\tthrow new SSEResumeError(`HTTP ${response.status} resuming MCP SSE stream: ${text}`);\n\t\t}\n\t\tconst contentType = response.headers.get(\"Content-Type\") ?? \"\";\n\t\tif (!contentType.includes(\"text/event-stream\") || !response.body) {\n\t\t\tawait response.body?.cancel();\n\t\t\tthrow new SSEResumeError(`MCP SSE resume returned unsupported Content-Type: ${contentType || \"(missing)\"}`);\n\t\t}\n\t\treturn response;\n\t}\n\n\t/** Route an SSE message (or batch) to the appropriate handler. */","sourceCodeStart":337,"sourceCodeEnd":373,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/mcp/transports/http.ts#L337-L373","documentation":"While resuming an MCP SSE stream (GET with Last-Event-ID), the server returned 401/403 and the registered onAuthError callback returned no new headers — meaning auth refresh failed or was declined. The transport throws SSEResumeError so the pending POST is never replayed (the server may have already executed it).","triggerScenarios":"SSE resume GET returns 401 or 403 with onAuthError wired, and the callback resolves to undefined/null (refresh flow exhausted, refresh token invalid, user declined re-auth).","commonSituations":"OAuth access token expired mid-stream and the refresh token is revoked; auth provider outage during token refresh; credentials configured for the wrong audience; long-lived MCP sessions outlasting token TTLs.","solutions":["Re-authenticate and reconnect the MCP transport with fresh credentials","Verify the auth provider's refresh flow works (check refresh token validity/expiry)","Increase token TTL or implement proactive refresh before expiry so streams don't die mid-session","Confirm the credentials' audience/scopes match the MCP server's requirements"],"exampleFix":"// before\nonAuthError: async () => expiredRefreshToken ? refresh(expiredRefreshToken) : undefined\n// after\nonAuthError: async () => {\n  const refreshed = await refreshWithRetry(refreshToken); // handles transient refresh failures\n  return refreshed ? { Authorization: `Bearer ${refreshed.accessToken}` } : undefined;\n}","handlingStrategy":"fallback","validationCode":"// Verify credentials are refreshable before starting long-lived streams:\nif (!refreshToken) throw new Error(\"cannot maintain MCP streams without a refreshable credential\");","typeGuard":null,"tryCatchPattern":"try {\n  await transport.request(method, params);\n} catch (err) {\n  if (err instanceof SSEResumeError && err.message.includes(\"auth refresh failed\")) {\n    await reauthenticateAndReconnect(); // do NOT replay the request\n  } else throw err;\n}","preventionTips":["Wire a robust onAuthError that actually returns fresh headers","Refresh tokens proactively before expiry, not only on 401","Never replay the original POST after this error — the server may have executed it","Validate refresh-token validity at connect time"],"tags":["mcp","sse","auth","oauth","token-expired"],"backgroundTag":"auth-refresh-failed","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}