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
- Reconnect Railway to complete OAuth and store a valid access token
- Ensure the token is stored/passed with the 'Bearer ' prefix and trimmed of whitespace
- Check that the credential source (env var/DB row) is actually populated before creating the client
- 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
- Trim tokens and always prefix with 'Bearer ' when storing
- Fail fast on missing env vars instead of interpolating undefined
- Complete the OAuth flow before persisting the connection
- Never build auth headers from unsanitized user input (CRLF)
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
- railway_target_mismatch
- 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/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)