paperclipai/paperclip · error · RailwayError

railway_api_authorization_required

railway_api_authorization_required

Error message

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.

What it means

Thrown by query() (code railway_api_authorization_required, HTTP status preserved as 401/403) when the Railway GraphQL API rejects the request's authorization. The hosted connection token may not be accepted for direct API access; the user must reconnect with a token that has access to the target workspace/project.

Solutions

  1. Reconnect Railway granting access to the workspace/project the operation targets
  2. Verify the token has API access on the Railway dashboard (tokens page)
  3. Confirm the project ID belongs to the consented workspace
  4. If Railway doesn't accept hosted tokens for API access, create and connect a personal API token

Example fix

// before
const client = createRailwayClient({ authorization: `Bearer ${hostedMcpToken}` });
await client.getProject(projectIdInOtherWorkspace); // 403
// after
const apiToken = await getRailwayApiTokenWithWorkspaceAccess(workspaceId);
const client = createRailwayClient({ authorization: `Bearer ${apiToken}` });
Defensive patterns

Strategy: try-catch

Validate before calling

async function tokenCanAccessWorkspace(auth, workspaceId) {
  const res = await fetch("https://api.railway.com/graphql", {
    method: "POST",
    headers: { "content-type": "application/json", authorization: auth },
    body: JSON.stringify({ query: "{ workspaceById(id: \"" + workspaceId + "\") { id } }" })
  });
  return res.status !== 401 && res.status !== 403;
}

Try / catch

try {
  await client.query(document, variables);
} catch (e) {
  if (e?.code === "railway_api_authorization_required") {
    await reconnectRailwayWithWorkspaceScope(conn, requiredWorkspaceId);
  } else throw e;
}

Prevention

When it happens

Trigger: API responds 401 (invalid/expired token) or 403 (token valid but lacking workspace/project access); hosted-connection token intentionally not usable for the GraphQL API; target project belongs to a workspace not covered by consent.

Common situations: Token scoped to workspace A used on project in workspace B; Railway revoked the token; connection created before a project was added to the granted scope; using hosted MCP token where a personal API token is required.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


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

Appendix: source

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

}

export function createRailwayClient(options: RailwayClientOptions) {
  if (!/^Bearer [^\r\n]+$/.test(options.authorization)) throw new RailwayError("railway_authorization_required", "Reconnect Railway to authorize API access.", 401);
  const secret = options.authorization.slice(7);
  const redact = (value: unknown) => JSON.parse(redactSensitiveText(JSON.stringify(value).split(secret).join("[REDACTED]")));

  async function query(document: string, variables: Record<string, unknown>): Promise<Record<string, any>> {
    options.signal.throwIfAborted();
    let response: Response;
    try {
      response = await options.request(RAILWAY_API_URL, { method: "POST", redirect: "error", signal: options.signal, headers: { "content-type": "application/json", Authorization: options.authorization }, body: JSON.stringify({ query: document, variables }) });
    } catch (error) {
      if (options.signal.aborted) throw options.signal.reason;
      throw new RailwayError("railway_request_failed", "Railway could not be reached. A deployment request may have succeeded; inspect deployment status before retrying.");
    }
    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;

View on GitHub (pinned to 3f1d897a7c)