paperclipai/paperclip · error · RailwayError

railway_authorization_required

railway_authorization_required

Error message

Reconnect Railway to authorize API access.

What it means

Thrown by createRailwayClient (code railway_authorization_required, HTTP 401) when the supplied options.authorization does not match a Bearer token pattern (/^Bearer [^\r\n]+$/). Guards against calling the Railway API with an empty, malformed, or CR/LF-injected credential string.

Solutions

  1. Reconnect Railway to complete OAuth and store a valid access token
  2. Ensure the token is stored/passed with the 'Bearer ' prefix and trimmed of whitespace
  3. Check that the credential source (env var/DB row) is actually populated before creating the client
  4. Never interpolate unsanitized user input into the header (CRLF guard is intentional)

Example fix

// before
const client = createRailwayClient({ authorization: process.env.RAILWAY_TOKEN }); // undefined
// after
const token = process.env.RAILWAY_TOKEN?.trim();
if (!token) throw new Error("RAILWAY_TOKEN not configured");
const client = createRailwayClient({ authorization: `Bearer ${token}` });
Defensive patterns

Strategy: validation

Validate before calling

function hasValidRailwayAuthorization(auth) {
  return typeof auth === "string" && /^Bearer [^\r\n]+$/.test(auth);
}
// call before createRailwayClient
if (!hasValidRailwayAuthorization(opts.authorization)) throw new Error("missing Railway token");

Type guard

function isValidRailwayAuth(v) {
  return typeof v === "string" && v.startsWith("Bearer ") && v.length > 7 && !/[\r\n]/.test(v);
}

Try / catch

try {
  const client = createRailwayClient(opts);
} catch (e) {
  if (e?.code === "railway_authorization_required") {
    await initiateRailwayReconnect(conn); // token missing/malformed
  } else throw e;
}

Prevention

When it happens

Trigger: authorization is undefined/empty string; credential stored without the 'Bearer ' prefix; token containing newline characters; connection object not fully hydrated before client creation.

Common situations: Connection saved without completing OAuth token exchange; env var like RAILWAY_TOKEN missing so the prefix concat yields 'Bearer undefined'; copying a token with a trailing newline into config.

Related errors


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

Appendix: source

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

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

View on GitHub (pinned to 3f1d897a7c)