fix(FN-2539): preserve dashboard API compatibility and harden proxy wildcard routing
- Replace app/api.ts with a compatibility barrel that re-exports the legacy client API surface from app/api/legacy.ts
- Move the existing dashboard API implementation into app/api/legacy.ts and update static-analysis tests to include the new module path
- Update proxy wildcard routing to use /proxy/:nodeId/{*splat}, keeping explicit proxy handlers ahead of the fallback route
- Reject proxying to local nodes with a consistent 400 response and expand proxy route tests plus route-ordering compatibility notes
This commit is contained in:
@@ -10,11 +10,12 @@ const agentsViewPath = path.join(__dirname, "../components/AgentsView.tsx");
|
|||||||
const agentDetailViewPath = path.join(__dirname, "../components/AgentDetailView.tsx");
|
const agentDetailViewPath = path.join(__dirname, "../components/AgentDetailView.tsx");
|
||||||
const agentRunHistoryPath = path.join(__dirname, "../components/AgentRunHistory.tsx");
|
const agentRunHistoryPath = path.join(__dirname, "../components/AgentRunHistory.tsx");
|
||||||
const apiPath = path.join(__dirname, "../api.ts");
|
const apiPath = path.join(__dirname, "../api.ts");
|
||||||
|
const apiLegacyPath = path.join(__dirname, "../api/legacy.ts");
|
||||||
|
|
||||||
const agentsViewContent = fs.readFileSync(agentsViewPath, "utf-8");
|
const agentsViewContent = fs.readFileSync(agentsViewPath, "utf-8");
|
||||||
const agentDetailViewContent = fs.readFileSync(agentDetailViewPath, "utf-8");
|
const agentDetailViewContent = fs.readFileSync(agentDetailViewPath, "utf-8");
|
||||||
const agentRunHistoryContent = fs.readFileSync(agentRunHistoryPath, "utf-8");
|
const agentRunHistoryContent = fs.readFileSync(agentRunHistoryPath, "utf-8");
|
||||||
const apiContent = fs.readFileSync(apiPath, "utf-8");
|
const apiContent = `${fs.readFileSync(apiPath, "utf-8")}\n${fs.existsSync(apiLegacyPath) ? fs.readFileSync(apiLegacyPath, "utf-8") : ""}`;
|
||||||
|
|
||||||
describe("Agent runs UI — static analysis", () => {
|
describe("Agent runs UI — static analysis", () => {
|
||||||
describe("startAgentRun API function", () => {
|
describe("startAgentRun API function", () => {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
7029
packages/dashboard/app/api/legacy.ts
Normal file
7029
packages/dashboard/app/api/legacy.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -206,14 +206,24 @@ describe("Proxy routes", () => {
|
|||||||
expect(res.body).toEqual({ error: "Node not found" });
|
expect(res.body).toEqual({ error: "Node not found" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns 400 when node is local (no url)", async () => {
|
it("returns 400 when node is local", async () => {
|
||||||
const node = createMockRemoteNode({ type: "local", url: undefined });
|
const node = createMockRemoteNode({ type: "local", url: undefined });
|
||||||
mockGetNode.mockResolvedValue(node);
|
mockGetNode.mockResolvedValue(node);
|
||||||
|
|
||||||
const res = await get(app, "/api/proxy/local-node/some-endpoint");
|
const res = await get(app, "/api/proxy/local-node/some-endpoint");
|
||||||
|
|
||||||
expect(res.status).toBe(400);
|
expect(res.status).toBe(400);
|
||||||
expect(res.body).toEqual({ error: "Node has no URL" });
|
expect(res.body).toEqual({ error: "Cannot proxy to local node" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 400 when local node has a URL", async () => {
|
||||||
|
const node = createMockRemoteNode({ type: "local", url: "http://local:4040" });
|
||||||
|
mockGetNode.mockResolvedValue(node);
|
||||||
|
|
||||||
|
const res = await get(app, "/api/proxy/local-node/some-endpoint");
|
||||||
|
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
expect(res.body).toEqual({ error: "Cannot proxy to local node" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns 502 on connection error (TypeError)", async () => {
|
it("returns 502 on connection error (TypeError)", async () => {
|
||||||
|
|||||||
@@ -2065,6 +2065,8 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
|
|
||||||
// Registrar mount order is part of the API contract. Keep specific routes
|
// Registrar mount order is part of the API contract. Keep specific routes
|
||||||
// before generic parameter/wildcard routes to preserve Express precedence.
|
// before generic parameter/wildcard routes to preserve Express precedence.
|
||||||
|
// Proxy registrar must remain last so explicit /proxy handlers stay ahead
|
||||||
|
// of the fallback /proxy/:nodeId/{*splat} wildcard route.
|
||||||
const routeContext = {
|
const routeContext = {
|
||||||
router,
|
router,
|
||||||
store,
|
store,
|
||||||
|
|||||||
@@ -226,9 +226,10 @@ export function registerProxyRoutes(ctx: ApiRoutesContext): void {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic wildcard proxy route — forwards any HTTP request to a remote node.
|
* Generic wildcard proxy route — forwards any HTTP request to a remote node.
|
||||||
|
* Keep this after specific /proxy routes so they retain precedence.
|
||||||
* Matches /api/proxy/:nodeId/*
|
* Matches /api/proxy/:nodeId/*
|
||||||
*/
|
*/
|
||||||
router.all("/proxy/:nodeId/*splat", async (req: Request, res: Response) => {
|
router.all("/proxy/:nodeId/{*splat}", async (req: Request, res: Response) => {
|
||||||
const nodeId = req.params.nodeId as string;
|
const nodeId = req.params.nodeId as string;
|
||||||
const splat = req.params.splat as string | string[];
|
const splat = req.params.splat as string | string[];
|
||||||
const remainingPath = Array.isArray(splat) ? splat.join("/") : splat;
|
const remainingPath = Array.isArray(splat) ? splat.join("/") : splat;
|
||||||
@@ -245,6 +246,11 @@ export function registerProxyRoutes(ctx: ApiRoutesContext): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (node.type === "local") {
|
||||||
|
res.status(400).json({ error: "Cannot proxy to local node" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!node.url) {
|
if (!node.url) {
|
||||||
res.status(400).json({ error: "Node has no URL" });
|
res.status(400).json({ error: "Node has no URL" });
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user