paperclipai/paperclip · error · RailwayError
railway_operation_unconfirmed
railway_operation_unconfirmed
Error message
Railway did not confirm a resulting deployment. Inspect deployment status before retrying.
What it means
After the Railway GraphQL `deploymentRedeploy` mutation executes, the library validates that the returned `deploymentRedeploy.id` parses as a valid ID. If Railway accepted the request but returned no parseable resulting deployment ID, this `railway_operation_unconfirmed` error is thrown — the redeploy may or may not have actually started, so the caller must inspect deployment status before retrying to avoid duplicate deployments.
Solutions
- Do NOT immediately retry the mutation — call `railway_list-deployments` or `railway_deployment-status` to check whether a new deployment was already created.
- If no new deployment exists after a status check, retry the redeploy once.
- If the error repeats, verify the deployment still exists and is eligible via `railway_deployment-status`, then check Railway's status page for API incidents.
- Capture the raw response (enable logging) and report if the mutation payload shape changed.
Example fix
// before
try {
await railwayCall("railway_redeploy", args);
} catch (e) {
await railwayCall("railway_redeploy", args); // risk: duplicate deployment
}
// after
try {
await railwayCall("railway_redeploy", args);
} catch (e) {
const deployments = await railwayCall("railway_list-deployments", { projectId, environmentId, serviceId, first: 5 });
const alreadyRedeployed = deployments.deployments.edges.some((e2) => e2.node.createdAt > startedAt);
if (!alreadyRedeployed) await railwayCall("railway_redeploy", args);
} Defensive patterns
Strategy: try-catch
Validate before calling
const d = await railwayCall("railway_deployment-status", { projectId, environmentId, serviceId, deploymentId });
if (!d || d.canRedeploy !== true) throw new Error("Deployment not redeployable; skipping redeploy"); Type guard
function hasConfirmedRedeploy(result) {
return typeof result?.deploymentRedeploy?.id === "string" && result.deploymentRedeploy.id.length > 0;
} Try / catch
try {
await railwayCall("railway_redeploy", args);
} catch (e) {
if (e?.code === "railway_operation_unconfirmed") {
// verify before retrying — the mutation may have succeeded
const created = await pollForNewDeployment(projectId, environmentId, serviceId, startedAt);
if (!created) await railwayCall("railway_redeploy", args);
} else throw e;
} Prevention
- Never retry unconfirmed mutations without first checking for the resulting deployment.
- Record a start timestamp so you can detect deployments created by a possibly-successful mutation.
- Keep redeploy invocations single-flight per deployment (idempotency key/lock).
- Monitor Railway status for API incidents before assuming client-side bugs.
When it happens
Trigger: Calling `railway_redeploy` where the mutation returns null/undefined `deploymentRedeploy`, an object without an `id`, or an `id` failing the `id.safeParse` format check — e.g. Railway-side race where the deployment was deleted mid-mutation, transient API inconsistency, or an unexpected response shape from a Railway API change.
Common situations: Redeploying a deployment concurrently deleted by a teammate or automation; Railway API version drift changing the mutation payload; retry storms after a network failure where the second attempt hits an already-redeployed deployment; intermittent Railway API inconsistencies during incidents.
Understand the failure class
Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.
Related errors
- railway_deployment_ineligible
- A different semantic result was already committed
- Cannot build API path with an empty path segment.
- Challenge secret is required. Pass --token or --token-env.
- Completed warm transition template conflicts with its exact…
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/9b34312d5c1f5acb.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/railway.ts:289
const lines = data[args.kind === "build" ? "buildLogs" : "deploymentLogs"];
if (!Array.isArray(lines)) throw new RailwayError("railway_invalid_response", "Railway returned invalid log data.");
let bytes = 0;
let messageTruncated = false;
const bounded = [];
for (const line of lines.slice(0, args.limit)) {
const message = String(line.message ?? "");
if (message.length > 8192) messageTruncated = true;
const safe = redact({ timestamp: line.timestamp, severity: line.severity, message: message.slice(0, 8192) });
bytes += Buffer.byteLength(JSON.stringify(safe));
if (bytes > 64 * 1024) break;
bounded.push(safe);
}
result = { deploymentId: args.deploymentId, kind: args.kind, lines: bounded, truncated: messageTruncated || bounded.length < lines.length, limitReached: lines.length >= args.limit }; break;
}
case "redeploy":
if (!deployment?.canRedeploy) throw new RailwayError("railway_deployment_ineligible", "Railway does not allow this deployment to be redeployed.", 409);
result = await query(RAILWAY_QUERIES.redeploy, { deploymentId: args.deploymentId });
if (!id.safeParse(record(record(result).deploymentRedeploy).id).success) throw new RailwayError("railway_operation_unconfirmed", "Railway did not confirm a resulting deployment. Inspect deployment status before retrying.");
break;
case "restart":
result = await query(RAILWAY_QUERIES.restart, { deploymentId: args.deploymentId });
if (record(result).deploymentRestart !== true) throw new RailwayError("railway_operation_unconfirmed", "Railway did not confirm the restart. Inspect deployment status before retrying.");
result = { ...record(result), targetDeploymentId: args.deploymentId };
break;
case "rollback":
if (!deployment?.canRollback) throw new RailwayError("railway_deployment_ineligible", "Railway does not allow rollback to this deployment.", 409);
result = await query(RAILWAY_QUERIES.rollback, { deploymentId: args.deploymentId });
if (record(result).deploymentRollback !== true) throw new RailwayError("railway_operation_unconfirmed", "Railway did not confirm the rollback. Inspect deployment status before retrying.");
result = { ...record(result), targetDeploymentId: args.deploymentId };
break;
case "run-command":
if (!Array.isArray(deployment?.instances) || !deployment.instances.some((entry: { id: string }) => entry.id === args.deploymentInstanceId) || deployment.status !== "SUCCESS") throw new RailwayError("railway_target_mismatch", "The container instance is not part of the selected running deployment.", 403);
if (!options.runCommand) throw new RailwayError("railway_ssh_setup_required", "Configure Container access on this Railway connection before running commands.", 422);
result = await options.runCommand({ deploymentInstanceId: args.deploymentInstanceId, command: args.command, timeoutSeconds: args.timeoutSeconds, signal: options.signal }); break;
}
}View on GitHub (pinned to 3f1d897a7c)