paperclipai/paperclip · error · RailwayError

railway_api_error

railway_api_error

Error message

Railway could not complete the request. Check target IDs, resource permissions, and deployment eligibility. Inspect status before retrying a mutation.

What it means

This client wraps Railway's GraphQL API (backboard.railway.com). After an HTTP 200 response, if the payload contains a top-level `errors` array whose entries are not recognized as auth-denial codes (UNAUTHENTICATED/FORBIDDEN/Not Authorized/Unauthorized/Forbidden), the client throws `railway_api_error`. It is a catch-all for provider-side GraphQL execution failures: bad IDs, resources the token can't see, or mutations Railway refuses (e.g. deployment not eligible). The generic message intentionally avoids echoing error details because provider errors can leak variables and secrets.

Solutions

  1. Re-run the corresponding status/read operation (service-status, deployment-status, list-deployments) to confirm every ID exists and belongs to the consented workspace
  2. Verify each UUID (projectId, environmentId, serviceId, deploymentId) was obtained from a list-projects/list-services/list-environments call made with the same token
  3. If retrying a mutation (redeploy/restart/rollback), check `canRedeploy`/`canRollback` on the deployment first and inspect current status before retrying
  4. If IDs are confirmed valid, reconnect the Railway connection to refresh workspace/project access, then retry

Example fix

// before: blind mutation retry
await client.call("paperclip-railway-redeploy", args);
// after: inspect status first
const d = await client.call("paperclip-railway-deployment-status", args);
if (!d.canRedeploy) throw new Error("deployment not redeployable");
await client.call("paperclip-railway-redeploy", args);
Defensive patterns

Strategy: try-catch

Validate before calling

const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const ok = UUID.test(args.projectId) && UUID.test(args.environmentId) && UUID.test(args.serviceId);

Type guard

function isRailwayError(e: unknown): e is RailwayError {
  return e instanceof RailwayError && typeof (e as RailwayError).code === "string";
}

Try / catch

try {
  return await client.call(name, args);
} catch (e) {
  if (isRailwayError(e) && e.code === "railway_api_error") {
    const status = await client.call(statusOpFor(name), args); // inspect before retry
    return retryOnceOrThrow(status);
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling any operation (list-projects, service-status, redeploy, etc.) when Railway returns `errors` with codes/messages outside the auth-denial allowlist — e.g. a projectId that doesn't exist, a deleted service, an invalid pagination cursor, or a mutation rejected for deployment-state reasons.

Common situations: Stale or hand-copied UUIDs pointing at deleted Railway resources; cross-workspace IDs used with a token scoped to a different workspace; a redeploy/rollback attempted on a deployment Railway considers ineligible; Railway schema or error-message changes that no longer match the auth allowlist.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/cd0fc0bdaf18690c. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/railway.ts:225

    }
    if (response.status === 401 || response.status === 403) {
      await response.body?.cancel();
      throw new RailwayError("railway_api_authorization_required", "Railway rejected API access. Reconnect with access to the required workspace or project. Hosted connection tokens are used only if Railway accepts them for API access.", response.status);
    }
    if (!response.ok) {
      await response.body?.cancel();
      throw new RailwayError(response.status === 429 ? "railway_rate_limited" : "railway_api_unavailable", response.status === 429 ? "Railway is rate limiting requests. Wait before trying again." : "Railway is unavailable. Check deployment status before retrying a deployment operation.");
    }
    const body = await boundedResponseText(response, options.signal);
    let payload: Record<string, any>;
    try { payload = record(JSON.parse(body)); }
    catch { throw new RailwayError("railway_invalid_response", "Railway returned an invalid API response."); }
    if (payload.errors) {
      // Provider errors can echo variables, credentials or application secrets.
      if (Array.isArray(payload.errors) && payload.errors.some((error) => ["UNAUTHENTICATED", "FORBIDDEN"].includes(error?.extensions?.code) || ["Not Authorized", "Unauthorized", "Forbidden"].includes(error?.message))) {
        throw new RailwayError("railway_api_authorization_required", "Railway denied this API request. Use IDs from a workspace selected during consent, or reconnect to grant access to the required workspace.", 403);
      }
      throw new RailwayError("railway_api_error", "Railway could not complete the request. Check target IDs, resource permissions, and deployment eligibility. Inspect status before retrying a mutation.");
    }
    if (!payload.data || typeof payload.data !== "object") throw new RailwayError("railway_invalid_response", "Railway returned no API data.");
    return payload.data;
  }

  async function validateTarget(args: Record<string, any>) {
    const data = await query(RAILWAY_QUERIES.target, { projectId: args.projectId, environmentId: args.environmentId, serviceId: args.serviceId });
    if (data.project?.id !== args.projectId || data.environment?.id !== args.environmentId || data.environment?.projectId !== args.projectId || data.service?.id !== args.serviceId || data.service?.projectId !== args.projectId || data.serviceInstance?.environmentId !== args.environmentId || data.serviceInstance?.serviceId !== args.serviceId) {
      throw new RailwayError("railway_target_mismatch", "The service and environment do not belong to the selected Railway project.", 403);
    }
    return data.serviceInstance;
  }

  async function validateDeployment(args: Record<string, any>) {
    const data = await query(RAILWAY_QUERIES.deployment, { deploymentId: args.deploymentId });
    const d = record(data.deployment);
    if (d.id !== args.deploymentId || d.projectId !== args.projectId || d.environmentId !== args.environmentId || d.serviceId !== args.serviceId) throw new RailwayError("railway_target_mismatch", "The deployment does not belong to the selected Railway target.", 403);
    return d;

View on GitHub (pinned to 3f1d897a7c)