From 6d2eb6e22ae6993268509b206d0e51e4914d536a Mon Sep 17 00:00:00 2001 From: Sai-karthik Date: Fri, 17 Jul 2026 07:40:07 +0000 Subject: [PATCH] fix matchmaking gateway failures --- scripts/course-matchmaking-routes.test.ts | 13 +++++- src/routes/services.ts | 51 ++++++++++++++++++----- 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/scripts/course-matchmaking-routes.test.ts b/scripts/course-matchmaking-routes.test.ts index e38271a..b7129d4 100644 --- a/scripts/course-matchmaking-routes.test.ts +++ b/scripts/course-matchmaking-routes.test.ts @@ -1,11 +1,15 @@ import assert from "node:assert/strict"; import { serviceRoutes } from "../src/routes/services.js"; +import { config } from "../src/config.js"; const originalFetch = globalThis.fetch; -const calls: Array<{ url: string; body: Record }> = []; +const calls: Array<{ url: string; body: Record; headers: Headers }> = []; globalThis.fetch = (async (input, init) => { const body = JSON.parse(String(init?.body ?? "{}")) as Record; - calls.push({ url: String(input), body }); + calls.push({ url: String(input), body, headers: new Headers(init?.headers) }); + if (body.action === "force_forbidden") { + return new Response(JSON.stringify({ detail: "Invalid A2A auth token" }), { status: 403, headers: { "content-type": "application/json" } }); + } return new Response(JSON.stringify({ task_id: "a2a-task", status: "completed", messages: [] }), { status: 200, headers: { "content-type": "application/json" } }); }) as typeof fetch; const app = serviceRoutes({ skipAuth: true }); @@ -24,6 +28,11 @@ try { const matchmakingResponse = await app.request("http://backend.test/matchmaking/a2a", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ action: "mark_applied", params: { opportunity_id: "job-1" } }) }); assert.equal(matchmakingResponse.status, 200); assert.ok(calls[0]?.url.endsWith("/a2a/tasks")); + assert.equal(calls[0]?.headers.get("authorization"), config.a2aAllowedKey ? `Bearer ${config.a2aAllowedKey}` : null); + + const forbiddenResponse = await app.request("http://backend.test/matchmaking/a2a", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ action: "force_forbidden" }) }); + assert.equal(forbiddenResponse.status, 502); + assert.match(await forbiddenResponse.text(), /matchmaking service authentication failed/); } finally { globalThis.fetch = originalFetch; } diff --git a/src/routes/services.ts b/src/routes/services.ts index 4d22b5a..320ec87 100644 --- a/src/routes/services.ts +++ b/src/routes/services.ts @@ -389,17 +389,48 @@ async function callMatchmakingA2a(body: Record, userId: string) action: action === "get_feed" ? "get_scout_feed" : action, user_id: getString(body.user_id) ?? userId, }; - const res = await fetch(target, { - method: "POST", - headers: { - "content-type": "application/json", - ...(config.a2aAllowedKey ? { authorization: `Bearer ${config.a2aAllowedKey}` } : {}), - }, - body: JSON.stringify(payload), - }); + const startedAt = Date.now(); + let res: Response; + try { + res = await fetch(target, { + method: "POST", + headers: { + "content-type": "application/json", + ...(config.a2aAllowedKey ? { authorization: `Bearer ${config.a2aAllowedKey}` } : {}), + }, + body: JSON.stringify(payload), + }); + } catch (err) { + log.warn({ err, service: "matchmaking", action, userId, durationMs: Date.now() - startedAt }, "matchmaking gateway request failed"); + throw new HTTPException(502, { message: "matchmaking service is unavailable" }); + } const text = await res.text(); - const result = text ? JSON.parse(text) as Record : {}; - if (!res.ok) throw new HTTPException(res.status as never, { message: text || "matchmaking request failed" }); + let result: Record = {}; + if (text) { + try { + const parsed = JSON.parse(text); + result = isRecord(parsed) ? parsed : { value: parsed }; + } catch { + result = { detail: text }; + } + } + if (!res.ok) { + const upstreamAuthFailure = res.status === 401 || res.status === 403; + log.warn({ + service: "matchmaking", + action, + userId, + upstreamStatus: res.status, + durationMs: Date.now() - startedAt, + }, "matchmaking gateway upstream rejected request"); + // A downstream service credential failure is infrastructure, not the + // browser user's auth. Returning 502 prevents an unnecessary Clerk token + // refresh/retry that would simply repeat the same rejected request. + throw new HTTPException((upstreamAuthFailure ? 502 : res.status) as never, { + message: upstreamAuthFailure ? "matchmaking service authentication failed" : text || "matchmaking request failed", + }); + } + log.info({ service: "matchmaking", action, userId, upstreamStatus: res.status, durationMs: Date.now() - startedAt }, "matchmaking gateway request completed"); return { action: String(payload.action || action), result }; }