paperclipai/paperclip · error · RailwayError

railway_workspace_required

railway_workspace_required

Error message

No authorized Railway workspace was found. Reconnect Railway and select a workspace to enable direct operations.

What it means

Thrown (code railway_workspace_required, HTTP 403) when workspace discovery succeeds but no workspace in the response matches the expected workspace ID schema, or the workspaces array is absent. The connection is authenticated, but no usable authorized workspace exists to scope direct Railway operations.

Solutions

  1. Reconnect Railway and explicitly select a workspace during consent, as the message instructs
  2. Ensure the connected Railway account actually belongs to at least one workspace
  3. Verify workspace membership on the Railway dashboard (accept any pending invites)
  4. If shapes changed, update the workspace ID validation/adapter

Example fix

// before
const ws = data.workspaces?.find(w => id.safeParse(w?.id).success)?.id;
if (!ws) throw new RailwayError("railway_workspace_required", ...);
// after (user side: create/select a workspace first)
await ensureRailwayWorkspace(account); // create workspace via dashboard if none
const ws = data.workspaces?.find(w => id.safeParse(w?.id).success)?.id;
Defensive patterns

Strategy: validation

Validate before calling

async function railwayWorkspaceConfigured(conn) {
  const ws = await probeRailwayWorkspaces(conn); // raw discovery
  return Array.isArray(ws) && ws.some(w => typeof w?.id === "string" && w.id.length > 0);
}

Type guard

function hasAuthorizedWorkspace(data) {
  return Array.isArray(data?.workspaces) &&
    data.workspaces.some(w => w != null && typeof w.id === "string");
}

Try / catch

try {
  const ws = await client.workspaceId();
} catch (e) {
  if (e?.code === "railway_workspace_required") {
    throw new ConfigurationError("Railway connection has no workspace; reconnect and select one.");
  } else throw e;
}

Prevention

When it happens

Trigger: Account has zero workspaces; the only workspaces have missing/malformed IDs failing zod id.safeParse; connection was created for a different account than expected; workspaces field renamed in the MCP response.

Common situations: New Railway account with no workspace yet; revoked workspace membership; Railway MCP response-shape change; user connected with a personal token scoped to an empty org.

Understand the failure class

Background: EmptyResultError / "no results found": when an API or scraper succeeds but returns zero rows — this error's family across 9 libraries.

Related errors


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

Appendix: source

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

  let response = await list(headers);
  if (response.status === 400) {
    await response.body?.cancel();
    response = await list(await initializeMcpHttpSession({ send, headers, requestId: "paperclip-railway-workspace-probe" }));
  }
  if (!response.ok) {
    await response.body?.cancel();
    throw new RailwayError("railway_workspace_discovery_failed", "Railway's hosted connection is connected, but workspace access could not be checked. Refresh actions to try again.");
  }
  const body = await boundedResponseText(response, options.signal);
  let data: Record<string, any>;
  try {
    const payload = record(parseMcpHttpResponseBody(body, response.headers.get("content-type")));
    const result = record(payload.result);
    if (payload.error || result.isError) throw new Error("Workspace discovery failed");
    data = record(result.structuredContent ?? JSON.parse(result.content?.find((item: any) => item.type === "text")?.text ?? "{}"));
  } catch { throw new RailwayError("railway_workspace_discovery_failed", "Railway could not list authorized workspaces. Refresh actions or reconnect and select a workspace."); }
  const workspaceId = Array.isArray(data.workspaces) ? data.workspaces.find((workspace) => id.safeParse(workspace?.id).success)?.id : undefined;
  if (!workspaceId) throw new RailwayError("railway_workspace_required", "No authorized Railway workspace was found. Reconnect Railway and select a workspace to enable direct operations.", 403);
  return workspaceId;
}

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) {

View on GitHub (pinned to 3f1d897a7c)