{"record":{"id":"1d9685cd4e978d85","repo":"paperclipai/paperclip","slug":"railway-api-authorization-required","errorCode":"railway_api_authorization_required","errorMessage":"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.","messagePattern":"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\\.","errorType":"error_code","errorClass":"RailwayError","httpStatus":null,"severity":"error","filePath":"server/src/services/railway.ts","lineNumber":210,"sourceCode":"}\n\nexport function createRailwayClient(options: RailwayClientOptions) {\n  if (!/^Bearer [^\\r\\n]+$/.test(options.authorization)) throw new RailwayError(\"railway_authorization_required\", \"Reconnect Railway to authorize API access.\", 401);\n  const secret = options.authorization.slice(7);\n  const redact = (value: unknown) => JSON.parse(redactSensitiveText(JSON.stringify(value).split(secret).join(\"[REDACTED]\")));\n\n  async function query(document: string, variables: Record<string, unknown>): Promise<Record<string, any>> {\n    options.signal.throwIfAborted();\n    let response: Response;\n    try {\n      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 }) });\n    } catch (error) {\n      if (options.signal.aborted) throw options.signal.reason;\n      throw new RailwayError(\"railway_request_failed\", \"Railway could not be reached. A deployment request may have succeeded; inspect deployment status before retrying.\");\n    }\n    if (response.status === 401 || response.status === 403) {\n      await response.body?.cancel();\n      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);\n    }\n    if (!response.ok) {\n      await response.body?.cancel();\n      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.\");\n    }\n    const body = await boundedResponseText(response, options.signal);\n    let payload: Record<string, any>;\n    try { payload = record(JSON.parse(body)); }\n    catch { throw new RailwayError(\"railway_invalid_response\", \"Railway returned an invalid API response.\"); }\n    if (payload.errors) {\n      // Provider errors can echo variables, credentials or application secrets.\n      if (Array.isArray(payload.errors) && payload.errors.some((error) => [\"UNAUTHENTICATED\", \"FORBIDDEN\"].includes(error?.extensions?.code) || [\"Not Authorized\", \"Unauthorized\", \"Forbidden\"].includes(error?.message))) {\n        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);\n      }\n      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.\");\n    }\n    if (!payload.data || typeof payload.data !== \"object\") throw new RailwayError(\"railway_invalid_response\", \"Railway returned no API data.\");\n    return payload.data;","sourceCodeStart":192,"sourceCodeEnd":228,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/railway.ts#L192-L228","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Reconnect Railway granting access to the workspace/project the operation targets","Verify the token has API access on the Railway dashboard (tokens page)","Confirm the project ID belongs to the consented workspace","If Railway doesn't accept hosted tokens for API access, create and connect a personal API token"],"exampleFix":"// before\nconst client = createRailwayClient({ authorization: `Bearer ${hostedMcpToken}` });\nawait client.getProject(projectIdInOtherWorkspace); // 403\n// after\nconst apiToken = await getRailwayApiTokenWithWorkspaceAccess(workspaceId);\nconst client = createRailwayClient({ authorization: `Bearer ${apiToken}` });","handlingStrategy":"try-catch","validationCode":"async function tokenCanAccessWorkspace(auth, workspaceId) {\n  const res = await fetch(\"https://api.railway.com/graphql\", {\n    method: \"POST\",\n    headers: { \"content-type\": \"application/json\", authorization: auth },\n    body: JSON.stringify({ query: \"{ workspaceById(id: \\\"\" + workspaceId + \"\\\") { id } }\" })\n  });\n  return res.status !== 401 && res.status !== 403;\n}","typeGuard":null,"tryCatchPattern":"try {\n  await client.query(document, variables);\n} catch (e) {\n  if (e?.code === \"railway_api_authorization_required\") {\n    await reconnectRailwayWithWorkspaceScope(conn, requiredWorkspaceId);\n  } else throw e;\n}","preventionTips":["Grant workspace scope at connect time for every project you plan to operate on","Reconnect after Railway revokes or expires tokens","Keep separate connections per workspace rather than reusing one token","Test token validity with a cheap query before deployments"],"tags":["railway","authorization","forbidden","token"],"backgroundTag":"permission-denied","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-22T11:17:16.035Z"}