From a2d00a33fc83c4c6cf91bb80468feb9695eca436 Mon Sep 17 00:00:00 2001 From: Anna Effort Date: Fri, 21 Aug 2026 17:55:47 -0700 Subject: [PATCH] fix: keep the /api prefix when proxying /api/logs/* MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The catch-all proxy strips its own /api before forwarding, on the assumption that mcpgateway mounts every router at the root. log_search declares prefix="/api/logs", so /api/logs/activity was forwarded to /logs/activity and 404d. It is the only router in mcpgateway mounted under /api; the other 29 sit at the root. No caller had exercised an /api/logs/* route through the BFF before — the other five log routes exist only as generated URL builders with no live callers — so this surfaced as soon as the activity feed asked for real data. Signed-off-by: Anna Effort --- server/src/routes/proxy/catch-all.ts | 18 +++++++++++++++--- server/test/proxy.test.ts | 25 +++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/server/src/routes/proxy/catch-all.ts b/server/src/routes/proxy/catch-all.ts index 0cc0312..bfa7e04 100644 --- a/server/src/routes/proxy/catch-all.ts +++ b/server/src/routes/proxy/catch-all.ts @@ -26,6 +26,19 @@ import { upstreamAuthHeader } from "../../lib/upstream-auth.js"; const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS", "TRACE"]); +// mcpgateway mounts every router at the root except log_search, which declares +// `prefix="/api/logs"`. Stripping this route's own `/api` for those paths would +// forward /api/logs/* to /logs/* upstream, which 404s. Keep the prefix instead. +// Verified against mcpgateway: /api/logs is the only such router. +const UPSTREAM_API_PREFIXES = ["logs/"]; + +/** Browser `/api/` -> the path FastAPI actually serves. */ +function toUpstreamPath(wildcard: string): string { + return UPSTREAM_API_PREFIXES.some((prefix) => wildcard.startsWith(prefix)) + ? `/api/${wildcard}` + : `/${wildcard}`; +} + // Inbound headers that must never reach upstream verbatim: bff_sid/bff_csrf // (Cookie) are BFF-only secrets; the rest are infra/auth headers mcpgateway // trusts for request-URL construction (Forwarded/X-Forwarded-*, including @@ -125,10 +138,9 @@ export default async function catchAllProxyRoute(fastify: FastifyInstance): Prom "/api/*", { preHandler: [fastify.sessionAuth, csrfIfUnsafe] }, async (request: FastifyRequest, reply: FastifyReply) => { - // Wildcard capture excludes the leading '/api/'; FastAPI routes are - // mounted at root, so reattach a single leading slash. + // Wildcard capture excludes the leading '/api/'; see toUpstreamPath. const wildcard = (request.params as Record)["*"] ?? ""; - const upstreamPath = `/${wildcard}`; + const upstreamPath = toUpstreamPath(wildcard); const bearerToken = request.session!.bearerToken; const sessionId = request.session!.sessionId; diff --git a/server/test/proxy.test.ts b/server/test/proxy.test.ts index ed4ed3d..f8e2202 100644 --- a/server/test/proxy.test.ts +++ b/server/test/proxy.test.ts @@ -111,6 +111,31 @@ describe("ALL /api/*", () => { expect(lastRequest?.authorization).toBe("Bearer test-bearer-token"); }); + it("keeps the /api prefix for /api/logs/*, which mcpgateway mounts under /api", async () => { + const app = await buildApp(); + const { cookie } = await seedSession(app); + + const response = await app.fastify.inject({ + method: "GET", + url: "/api/logs/activity?limit=100", + headers: { cookie }, + }); + + expect(response.statusCode).toBe(200); + // Stripping the prefix here would forward /logs/activity, which 404s. + expect(lastRequest?.path).toBe("/api/logs/activity?limit=100"); + expect(lastRequest?.authorization).toBe("Bearer test-bearer-token"); + }); + + it("does not treat a non-logs path beginning with the same letters as /api-mounted", async () => { + const app = await buildApp(); + const { cookie } = await seedSession(app); + + await app.fastify.inject({ method: "GET", url: "/api/logsearch", headers: { cookie } }); + + expect(lastRequest?.path).toBe("/logsearch"); + }); + it("never lets the browser override the injected Authorization header", async () => { const app = await buildApp(); const { cookie } = await seedSession(app);