Skip to content

Commit a2d00a3

Browse files
committed
fix: keep the /api prefix when proxying /api/logs/*
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 <anna.effort@ibm.com>
1 parent b5f8b47 commit a2d00a3

2 files changed

Lines changed: 40 additions & 3 deletions

File tree

server/src/routes/proxy/catch-all.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,19 @@ import { upstreamAuthHeader } from "../../lib/upstream-auth.js";
2626

2727
const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS", "TRACE"]);
2828

29+
// mcpgateway mounts every router at the root except log_search, which declares
30+
// `prefix="/api/logs"`. Stripping this route's own `/api` for those paths would
31+
// forward /api/logs/* to /logs/* upstream, which 404s. Keep the prefix instead.
32+
// Verified against mcpgateway: /api/logs is the only such router.
33+
const UPSTREAM_API_PREFIXES = ["logs/"];
34+
35+
/** Browser `/api/<wildcard>` -> the path FastAPI actually serves. */
36+
function toUpstreamPath(wildcard: string): string {
37+
return UPSTREAM_API_PREFIXES.some((prefix) => wildcard.startsWith(prefix))
38+
? `/api/${wildcard}`
39+
: `/${wildcard}`;
40+
}
41+
2942
// Inbound headers that must never reach upstream verbatim: bff_sid/bff_csrf
3043
// (Cookie) are BFF-only secrets; the rest are infra/auth headers mcpgateway
3144
// trusts for request-URL construction (Forwarded/X-Forwarded-*, including
@@ -125,10 +138,9 @@ export default async function catchAllProxyRoute(fastify: FastifyInstance): Prom
125138
"/api/*",
126139
{ preHandler: [fastify.sessionAuth, csrfIfUnsafe] },
127140
async (request: FastifyRequest, reply: FastifyReply) => {
128-
// Wildcard capture excludes the leading '/api/'; FastAPI routes are
129-
// mounted at root, so reattach a single leading slash.
141+
// Wildcard capture excludes the leading '/api/'; see toUpstreamPath.
130142
const wildcard = (request.params as Record<string, string>)["*"] ?? "";
131-
const upstreamPath = `/${wildcard}`;
143+
const upstreamPath = toUpstreamPath(wildcard);
132144
const bearerToken = request.session!.bearerToken;
133145

134146
const sessionId = request.session!.sessionId;

server/test/proxy.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,31 @@ describe("ALL /api/*", () => {
111111
expect(lastRequest?.authorization).toBe("Bearer test-bearer-token");
112112
});
113113

114+
it("keeps the /api prefix for /api/logs/*, which mcpgateway mounts under /api", async () => {
115+
const app = await buildApp();
116+
const { cookie } = await seedSession(app);
117+
118+
const response = await app.fastify.inject({
119+
method: "GET",
120+
url: "/api/logs/activity?limit=100",
121+
headers: { cookie },
122+
});
123+
124+
expect(response.statusCode).toBe(200);
125+
// Stripping the prefix here would forward /logs/activity, which 404s.
126+
expect(lastRequest?.path).toBe("/api/logs/activity?limit=100");
127+
expect(lastRequest?.authorization).toBe("Bearer test-bearer-token");
128+
});
129+
130+
it("does not treat a non-logs path beginning with the same letters as /api-mounted", async () => {
131+
const app = await buildApp();
132+
const { cookie } = await seedSession(app);
133+
134+
await app.fastify.inject({ method: "GET", url: "/api/logsearch", headers: { cookie } });
135+
136+
expect(lastRequest?.path).toBe("/logsearch");
137+
});
138+
114139
it("never lets the browser override the injected Authorization header", async () => {
115140
const app = await buildApp();
116141
const { cookie } = await seedSession(app);

0 commit comments

Comments
 (0)