Log plugin bridge 502 causes

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-05-19 14:26:51 -05:00
1 parent 84c85b930b
commit 19c01ada4e
2 files changed
+119

No files matched your search

@@ -42,6 +42,7 @@ async function createApp(
jobDeps?: unknown;
toolDeps?: unknown;
bridgeDeps?: unknown;
captureJsonContext?: (context: unknown, body: unknown) => void;
} = {},
) {
const [{ pluginRoutes }, { errorHandler }] = await Promise.all([
@@ -56,6 +57,16 @@ async function createApp(
const app = express();
app.use(express.json());
if (routeOverrides.captureJsonContext) {
app.use((_req, res, next) => {
const originalJson = res.json.bind(res);
res.json = ((body: unknown) => {
routeOverrides.captureJsonContext?.((res as any).__errorContext, body);
return originalJson(body);
}) as typeof res.json;
next();
});
}
app.use((req, _res, next) => {
req.actor = actor as typeof req.actor;
next();
@@ -627,6 +638,40 @@ describe.sequential("plugin tool and bridge authz", () => {
});
});
it("attaches worker bridge errors to the HTTP logger context", async () => {
readyPlugin();
const call = vi.fn().mockRejectedValue(new Error("missing source_objects column"));
const captured: Array<{ context: any; body: unknown }> = [];
const { app } = await createApp(boardActor(), {}, {
bridgeDeps: {
workerManager: { call },
},
captureJsonContext: (context, body) => {
captured.push({ context, body });
},
});
const res = await request(app)
.post(`/api/plugins/${pluginId}/data/source-objects`)
.send({ companyId: companyA });
expect(res.status).toBe(502);
expect(res.body).toMatchObject({
code: "UNKNOWN",
message: "missing source_objects column",
});
expect(captured.at(-1)?.context?.error).toMatchObject({
message: "missing source_objects column",
details: {
pluginId,
pluginKey: "paperclip.example",
bridgeMethod: "getData",
dataKey: "source-objects",
bridgeCode: "UNKNOWN",
},
});
});
it("rejects manual job triggers for non-admin board users", async () => {
const scheduler = { triggerJob: vi.fn() };
const jobStore = { getJobByIdForPlugin: vi.fn() };
+74
View File
@@ -1021,6 +1021,34 @@ export function pluginRoutes(
};
}
function attachPluginBridgeErrorContext(
req: Request,
res: Response,
err: unknown,
bridgeError: PluginBridgeErrorResponse,
metadata: Record<string, unknown>,
): void {
const rootError = err instanceof Error ? err : new Error(String(err));
(res as any).__errorContext = {
error: {
message: bridgeError.message,
stack: rootError.stack,
name: rootError.name,
details: {
...metadata,
bridgeCode: bridgeError.code,
bridgeDetails: bridgeError.details,
},
},
method: req.method,
url: req.originalUrl,
reqBody: req.body,
reqParams: req.params,
reqQuery: req.query,
};
(res as any).err = rootError;
}
/**
* POST /api/plugins/:pluginId/bridge/data
*
@@ -1072,6 +1100,11 @@ export function pluginRoutes(
code: "WORKER_UNAVAILABLE",
message: `Plugin is not ready (current status: ${plugin.status})`,
};
attachPluginBridgeErrorContext(req, res, new Error(bridgeError.message), bridgeError, {
pluginId: plugin.id,
pluginKey: plugin.pluginKey,
bridgeMethod: "getData",
});
res.status(502).json(bridgeError);
return;
}
@@ -1098,6 +1131,12 @@ export function pluginRoutes(
res.json({ data: result });
} catch (err) {
const bridgeError = mapRpcErrorToBridgeError(err);
attachPluginBridgeErrorContext(req, res, err, bridgeError, {
pluginId: plugin.id,
pluginKey: plugin.pluginKey,
bridgeMethod: "getData",
dataKey: body.key,
});
res.status(502).json(bridgeError);
}
});
@@ -1153,6 +1192,11 @@ export function pluginRoutes(
code: "WORKER_UNAVAILABLE",
message: `Plugin is not ready (current status: ${plugin.status})`,
};
attachPluginBridgeErrorContext(req, res, new Error(bridgeError.message), bridgeError, {
pluginId: plugin.id,
pluginKey: plugin.pluginKey,
bridgeMethod: "performAction",
});
res.status(502).json(bridgeError);
return;
}
@@ -1179,6 +1223,12 @@ export function pluginRoutes(
res.json({ data: result });
} catch (err) {
const bridgeError = mapRpcErrorToBridgeError(err);
attachPluginBridgeErrorContext(req, res, err, bridgeError, {
pluginId: plugin.id,
pluginKey: plugin.pluginKey,
bridgeMethod: "performAction",
actionKey: body.key,
});
res.status(502).json(bridgeError);
}
});
@@ -1235,6 +1285,12 @@ export function pluginRoutes(
code: "WORKER_UNAVAILABLE",
message: `Plugin is not ready (current status: ${plugin.status})`,
};
attachPluginBridgeErrorContext(req, res, new Error(bridgeError.message), bridgeError, {
pluginId: plugin.id,
pluginKey: plugin.pluginKey,
bridgeMethod: "getData",
dataKey: key,
});
res.status(502).json(bridgeError);
return;
}
@@ -1260,6 +1316,12 @@ export function pluginRoutes(
res.json({ data: result });
} catch (err) {
const bridgeError = mapRpcErrorToBridgeError(err);
attachPluginBridgeErrorContext(req, res, err, bridgeError, {
pluginId: plugin.id,
pluginKey: plugin.pluginKey,
bridgeMethod: "getData",
dataKey: key,
});
res.status(502).json(bridgeError);
}
});
@@ -1312,6 +1374,12 @@ export function pluginRoutes(
code: "WORKER_UNAVAILABLE",
message: `Plugin is not ready (current status: ${plugin.status})`,
};
attachPluginBridgeErrorContext(req, res, new Error(bridgeError.message), bridgeError, {
pluginId: plugin.id,
pluginKey: plugin.pluginKey,
bridgeMethod: "performAction",
actionKey: key,
});
res.status(502).json(bridgeError);
return;
}
@@ -1337,6 +1405,12 @@ export function pluginRoutes(
res.json({ data: result });
} catch (err) {
const bridgeError = mapRpcErrorToBridgeError(err);
attachPluginBridgeErrorContext(req, res, err, bridgeError, {
pluginId: plugin.id,
pluginKey: plugin.pluginKey,
bridgeMethod: "performAction",
actionKey: key,
});
res.status(502).json(bridgeError);
}
});