paperclipai/paperclip · error · RailwayError
railway_target_mismatch
railway_target_mismatch
Error message
The service and environment do not belong to the selected Railway project.
What it means
Before any target-scoped operation (service-status, list-deployments, read-logs, redeploy, restart, rollback, run-command), `validateTarget` fetches project/environment/service/serviceInstance with the caller-supplied IDs and verifies they are mutually consistent. If any check fails (wrong project, wrong parent, or missing instance), it throws `railway_target_mismatch` with HTTP 403. This enforces that effects bind to the exact project/environment/service triple approved during connection consent.
Solutions
- Call paperclip-railway-list-services and paperclip-railway-list-environments for the intended projectId and use only IDs returned by those calls
- Re-fetch service-status for the correct triple to confirm the serviceInstance exists
- If IDs came from a previous session, re-list from Railway rather than reusing cached identifiers
- Reconnect the connection scoped to the correct workspace if the project belongs to a different workspace
Example fix
// before: assumed valid triple
await client.call("paperclip-railway-service-status", { projectId: p, environmentId: e, serviceId: s });
// after: discover IDs first
const services = await client.call("paperclip-railway-list-services", { projectId: p });
const envs = await client.call("paperclip-railway-list-environments", { projectId: p });
await client.call("paperclip-railway-service-status", { projectId: p, environmentId: envs.environments.edges[0].node.id, serviceId: services.project.services.edges[0].node.id }); Defensive patterns
Strategy: validation
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;
function targetIdsVerified(t: { projectId: string; environmentId: string; serviceId: string }, known: Set<string>) {
return [t.projectId, t.environmentId, t.serviceId].every((v) => UUID.test(v))
&& known.has(t.serviceId) && known.has(t.environmentId);
} Try / catch
try {
return await client.call(op, args);
} catch (e) {
if (isRailwayError(e) && e.code === "railway_target_mismatch") {
const services = await client.call("paperclip-railway-list-services", { projectId: args.projectId });
const envs = await client.call("paperclip-railway-list-environments", { projectId: args.projectId });
// rebuild args from returned IDs, then retry once
}
throw e;
} Prevention
- Never mix IDs across projects or environments; build each call's triple from one discovery pass
- Re-list services/environments instead of caching IDs across sessions
- Verify deletions in Railway before reusing old IDs
- For agents, feed IDs only from actual tool-call outputs, never generated values
When it happens
Trigger: Calling an operation whose projectId, environmentId, and serviceId do not form a valid triple — e.g. environmentId from a different project, serviceId from another project, IDs pointing at deleted resources, or an environment/service combination with no serviceInstance.
Common situations: Mixing IDs across projects or environments when composing tool calls from memory; an agent hallucinating plausible UUIDs; a service or environment deleted in Railway after IDs were cached; typos when transcribing IDs between calls.
Understand the failure class
Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.
Related errors
- railway_authorization_required
- Explicit skill input must reference a unique assigned…
- railway_api_authorization_required
- railway_invalid_arguments
- railway_ssh_host_key_invalid
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/677bd41034eeaa3d.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/railway.ts:234
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;
}
return {
async probe(workspaceId: string) {
if (!id.safeParse(workspaceId).success) throw new RailwayError("railway_workspace_required", "Choose an authorized Railway workspace before checking API access.", 400);
await query(RAILWAY_QUERIES.projects, { workspaceId, first: 1 });
},
async call(name: string, parameters: unknown): Promise<unknown> {
if (isRailwayToolBlocked(name)) throw new RailwayError("railway_action_blocked", "This Railway action cannot bind its effects to an approved target. Use redeploy, restart, or rollback for an existing deployment.", 403);View on GitHub (pinned to 3f1d897a7c)