can1357/oh-my-pi · error · SSEResumeError

HTTP ${response.status} resuming MCP SSE stream: auth refres

Error message

HTTP ${response.status} resuming MCP SSE stream: auth refresh failed

What it means

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).

Source

Thrown at packages/coding-agent/src/mcp/transports/http.ts:355

	 * Failures throw {@link SSEResumeError} so `request()` never replays the
	 * originating POST in response.
	 */
	async #fetchSSEResume(resume: SSEResumeState, signal: AbortSignal): Promise<Response> {
		if (resume.lastEventId === null) {
			throw new SSEResumeError("SSE stream ended without a resumable event ID");
		}
		await waitForSSERetry(resume.retryMs, signal);
		const generated: Record<string, string> = {
			Accept: "text/event-stream",
			"Last-Event-ID": resume.lastEventId,
		};
		if (this.#sessionId) generated["Mcp-Session-Id"] = this.#sessionId;
		let response = await this.#fetch({ method: "GET", signal }, generated);
		if (this.onAuthError && (response.status === 401 || response.status === 403)) {
			await response.body?.cancel();
			const newHeaders = await this.onAuthError();
			if (!newHeaders) {
				throw new SSEResumeError(`HTTP ${response.status} resuming MCP SSE stream: auth refresh failed`);
			}
			// Persist refreshed headers so subsequent requests use them directly
			this.config = { ...this.config, headers: newHeaders };
			response = await this.#fetch({ method: "GET", signal }, generated);
		}
		if (!response.ok) {
			const text = await response.text().catch(() => "");
			throw new SSEResumeError(`HTTP ${response.status} resuming MCP SSE stream: ${text}`);
		}
		const contentType = response.headers.get("Content-Type") ?? "";
		if (!contentType.includes("text/event-stream") || !response.body) {
			await response.body?.cancel();
			throw new SSEResumeError(`MCP SSE resume returned unsupported Content-Type: ${contentType || "(missing)"}`);
		}
		return response;
	}

	/** Route an SSE message (or batch) to the appropriate handler. */

View on GitHub (pinned to 9690622007)

Solutions

  1. Re-authenticate and reconnect the MCP transport with fresh credentials
  2. Verify the auth provider's refresh flow works (check refresh token validity/expiry)
  3. Increase token TTL or implement proactive refresh before expiry so streams don't die mid-session
  4. Confirm the credentials' audience/scopes match the MCP server's requirements

Example fix

// before
onAuthError: async () => expiredRefreshToken ? refresh(expiredRefreshToken) : undefined
// after
onAuthError: async () => {
  const refreshed = await refreshWithRetry(refreshToken); // handles transient refresh failures
  return refreshed ? { Authorization: `Bearer ${refreshed.accessToken}` } : undefined;
}
Defensive patterns

Strategy: fallback

Validate before calling

// Verify credentials are refreshable before starting long-lived streams:
if (!refreshToken) throw new Error("cannot maintain MCP streams without a refreshable credential");

Try / catch

try {
  await transport.request(method, params);
} catch (err) {
  if (err instanceof SSEResumeError && err.message.includes("auth refresh failed")) {
    await reauthenticateAndReconnect(); // do NOT replay the request
  } else throw err;
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/f57ce9d98c6002da. Report an issue: GitHub.