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
- Re-run the corresponding status/read operation (service-status, deployment-status, list-deployments) to confirm every ID exists and belongs to the consented workspace
- Verify each UUID (projectId, environmentId, serviceId, deploymentId) was obtained from a list-projects/list-services/list-environments call made with the same token
- If retrying a mutation (redeploy/restart/rollback), check `canRedeploy`/`canRollback` on the deployment first and inspect current status before retrying
- 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
- Always source IDs from prior list/status calls with the same connection, never from memory or other workspaces
- Check canRedeploy/canRollback before destructive mutations
- Treat mutations as maybe-applied: inspect status before any retry
- Keep the connection's workspace consent in sync with the projects you target
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
- "configJson" is required and must be an object
- CreateOS returned an invalid resource ID.
- CreateOS returned an invalid response.
- CreateOS returned an unsuccessful response.
- CreateOS returned invalid JSON.
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)