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

  1. Do NOT immediately retry the mutation — call `railway_list-deployments` or `railway_deployment-status` to check whether a new deployment was already created.
  2. If no new deployment exists after a status check, retry the redeploy once.
  3. 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.
  4. 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

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


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)