diff --git a/server/src/__tests__/cloud-upstreams.test.ts b/server/src/__tests__/cloud-upstreams.test.ts new file mode 100644 index 00000000000..bb7e84d92bb --- /dev/null +++ b/server/src/__tests__/cloud-upstreams.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest"; + +import { HttpError } from "../errors.js"; +import { cloudUpstreamRemoteFailureReport } from "../services/cloud-upstreams.js"; + +describe("cloud upstream remote failures", () => { + it("preserves the cloud response body and message on run reports", () => { + const body = { + error: "bad_request", + message: "entities[42].body must be an object", + errors: [{ path: "entities[42].body" }], + }; + + expect(cloudUpstreamRemoteFailureReport(new HttpError(400, "bad_request", body))).toEqual({ + error: "bad_request", + errorMessage: "entities[42].body must be an object", + details: body, + }); + }); + + it("falls back to the thrown error message for non-remote failures", () => { + expect(cloudUpstreamRemoteFailureReport(new Error("network failed"))).toEqual({ + error: "network failed", + }); + }); +}); diff --git a/server/src/services/cloud-upstreams.ts b/server/src/services/cloud-upstreams.ts index 4484484640e..cb4683e8788 100644 --- a/server/src/services/cloud-upstreams.ts +++ b/server/src/services/cloud-upstreams.ts @@ -28,7 +28,7 @@ import { projects, routines, } from "@paperclipai/db"; -import { badRequest, notFound } from "../errors.js"; +import { badRequest, HttpError, notFound } from "../errors.js"; import { companyPortabilityService } from "./company-portability.js"; const DEFAULT_SCOPES = ["upstream_import:preview", "upstream_import:write", "upstream_import:read"]; @@ -380,13 +380,14 @@ export function cloudUpstreamService(db: Db, options: { instanceId?: string } = return finalRun; } catch (error) { const failedAt = new Date(); + const failure = cloudUpstreamRemoteFailureReport(error); return updateRun(runId, { status: "failed", activeStep: "push", progressPercent: 100, events: [ ...initialEvents, - event(failedAt.toISOString(), "push", "failed", error instanceof Error ? error.message : String(error)), + event(failedAt.toISOString(), "push", "failed", failure.errorMessage ?? failure.error), ], report: { runId, @@ -394,7 +395,7 @@ export function cloudUpstreamService(db: Db, options: { instanceId?: string } = manifestHash: bundle.manifest.manifestHash, idempotencyKey: bundle.manifest.idempotencyKey, retryOfRunId: input.retryOfRunId ?? null, - error: error instanceof Error ? error.message : String(error), + ...failure, }, completedAt: failedAt, }); @@ -929,6 +930,32 @@ async function parseRemoteResponse(response: Response): Promise { return parsed; } +export function cloudUpstreamRemoteFailureReport(error: unknown): { + error: string; + errorMessage?: string; + details?: unknown; +} { + const fallback = error instanceof Error ? error.message : String(error); + if (!(error instanceof HttpError)) { + return { error: fallback }; + } + const remote = remoteErrorBody(error.details); + return { + error: remote.error ?? error.message, + ...(remote.message ? { errorMessage: remote.message } : {}), + ...(error.details !== undefined ? { details: error.details } : {}), + }; +} + +function remoteErrorBody(details: unknown): { error?: string; message?: string } { + const record = optionalObject(details); + if (!record) return {}; + return { + error: optionalString(record.error) ?? undefined, + message: optionalString(record.message) ?? undefined, + }; +} + async function postJson(url: string, body: unknown): Promise { const response = await fetch(url, { method: "POST",