From 152a4e611dd129c95b5f555d52e7bb38a9836aa3 Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Tue, 20 Oct 2026 10:32:59 +0000 Subject: [PATCH] fix(mcp): treat an unparseable session response as a transient auth failure Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- apps/mcp/src/server/auth/index.test.ts | 42 ++++++++++++++++++++++++++ apps/mcp/src/server/auth/index.ts | 9 ++++-- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/apps/mcp/src/server/auth/index.test.ts b/apps/mcp/src/server/auth/index.test.ts index 7adb52c4..62698e81 100644 --- a/apps/mcp/src/server/auth/index.test.ts +++ b/apps/mcp/src/server/auth/index.test.ts @@ -239,6 +239,48 @@ describe("MCP authentication", () => { expect(warn).not.toHaveBeenCalled() }) + it("surfaces a 200 session body that fails the session contract as TransientAuthError", async () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}) + const error = vi.spyOn(console, "error").mockImplementation(() => {}) + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue( + new Response(JSON.stringify({}), { + status: 200, + headers: { "content-type": "application/json" }, + }), + ), + ) + + await expect( + validateApiKey("sm_badbody_key_0123456789abcd", API_URL), + ).rejects.toThrow(TransientAuthError) + expect(error).toHaveBeenCalledTimes(1) + expect(error.mock.calls[0]?.[0]).toBe( + "Auth backend unavailable during API key validation: Invalid session response", + ) + expect(warn).not.toHaveBeenCalled() + }) + + it("surfaces a non-JSON 200 session body as TransientAuthError", async () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}) + const error = vi.spyOn(console, "error").mockImplementation(() => {}) + vi.stubGlobal( + "fetch", + vi + .fn() + .mockResolvedValue( + new Response("upstream error", { status: 200 }), + ), + ) + + await expect( + validateApiKey("sm_htmlbody_key_0123456789abc", API_URL), + ).rejects.toThrow(TransientAuthError) + expect(error).toHaveBeenCalledTimes(1) + expect(warn).not.toHaveBeenCalled() + }) + it("surfaces a session-endpoint timeout as TransientAuthError", async () => { const warn = vi.spyOn(console, "warn").mockImplementation(() => {}) const error = vi.spyOn(console, "error").mockImplementation(() => {}) diff --git a/apps/mcp/src/server/auth/index.ts b/apps/mcp/src/server/auth/index.ts index b1e56bf9..a68d7c9f 100644 --- a/apps/mcp/src/server/auth/index.ts +++ b/apps/mcp/src/server/auth/index.ts @@ -44,9 +44,13 @@ export async function fetchSession( ) } - const result = sessionInfoSchema.safeParse(await response.json()) + const body = await response.json().catch((error: unknown) => { + if (error instanceof SyntaxError) return undefined + throw error + }) + const result = sessionInfoSchema.safeParse(body) if (!result.success) { - throw new Error("Invalid session response") + throw new TransientAuthError("Invalid session response") } return result.data @@ -86,6 +90,7 @@ const TRANSIENT_ERROR_NAMES = new Set([ const TRANSIENT_JOSE_CODES = new Set(["ERR_JWKS_TIMEOUT", "ERR_JOSE_GENERIC"]) function transientAuthErrorFor(error: unknown): TransientAuthError | null { + if (error instanceof TransientAuthError) return error const status = (error as { status?: unknown } | null)?.status if (typeof status === "number" && status !== 401 && status !== 403) { return new TransientAuthError(`Session endpoint returned ${status}`, status)