Preserve cloud upstream error details

This commit is contained in:
Dotta committed 2026-05-19 14:26:12 -05:00
1 parent 47a09db8c3
commit ca42a6ed5c
2 files changed
+56 -3

No files matched your search

@@ -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",
});
});
});
+30 -3
View File
@@ -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<unknown> {
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<T>(url: string, body: unknown): Promise<T> {
const response = await fetch(url, {
method: "POST",