paperclipai/paperclip · error · HttpError

error.message

Error message

error.message

What it means

This is a pass-through failure: the Railway tool-connection SSH configuration endpoint calls svc.configureRailwaySsh, and when the service rejects with a RailwayError the route re-throws it as an HttpError carrying the Railway service's own status and message. The generic message 'error.message' represents whatever the Railway integration reported (e.g. invalid token, SSH key rejected, resource missing). Any non-RailwayError exception is re-thrown unchanged and becomes a 500.

Solutions

  1. Read the HTTP status and message returned by the route — it mirrors the underlying RailwayError — and fix the reported Railway-side cause.
  2. Re-authenticate the tool connection: update the stored Railway token, then retry the SSH configuration call.
  3. Verify the Railway project/service/environment IDs in the connection still exist and are accessible to the token.
  4. If the error is not a RailwayError (500), inspect server logs for the raw exception in configureRailwaySsh.

Example fix

// before
await api.post(`/api/tool-connections/${id}/railway/ssh`, { action: "grant", grantId });
// after
try {
  await api.post(`/api/tool-connections/${id}/railway/ssh`, { action: "grant", grantId });
} catch (e) {
  if (e.status === 401 || e.status === 403) {
    await updateToolConnectionCredentials(id, freshRailwayToken); // then retry
  } else throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

const conn = await getConnection(id);
if (!conn.credentials?.railwayToken) throw new Error("Configure Railway credentials first");

Type guard

null

Try / catch

try { await api.post(`/tool-connections/${id}/railway/ssh`, body); }
catch (e) {
  // e.status and e.message mirror the RailwayError
  if (e.status === 401 || e.status === 403) await refreshRailwayToken(id);
  else if (e.status === 404) await verifyRailwayResourceExists(conn);
  else throw e;
}

Prevention

When it happens

Trigger: POST /tool-connections/:connectionId/railway/ssh with a body that configureRailwaySsh rejects — Railway API returns an error (bad credentials, missing service, invalid grant/action), or an unexpected exception inside the service.

Common situations: Expired or revoked Railway API token stored on the tool connection; configuring SSH against a deleted Railway project/environment; requesting an action (grant/revoke) the token lacks scope for; network failure to Railway surfacing as a raw error.

Related errors


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

Appendix: source

Thrown at server/src/routes/tool-access.ts:1690

    );
    if (!connection) return;
    res.json(connection);
  });

  router.get("/tool-connections/:connectionId/services", async (req, res) => {
    const connection = await getAccessibleResource(req, res, svc.getConnection(req.params.connectionId as string), "Tool connection not found");
    if (!connection) return;
    await assertToolConnectionConfigureAccess(req, connection);
    res.json(await svc.listComposioServices(connection.id, getActorInfo(req)));
  });

  router.post("/tool-connections/:connectionId/railway/ssh", validate(configureRailwaySshSchema), async (req, res) => {
    assertBoard(req);
    const connection = await getAccessibleResource(req, res, svc.getConnection(req.params.connectionId as string), "Tool connection not found");
    if (!connection) return;
    await assertToolConnectionConfigureAccess(req, connection);
    const setup = await svc.configureRailwaySsh(connection.id, connection.companyId, req.body, getActorInfo(req)).catch((error) => {
      if (error instanceof RailwayError) throw new HttpError(error.status, error.message);
      throw error;
    });
    await logActivity(db, { companyId: connection.companyId, actorType: "user", actorId: req.actor.userId ?? "board", action: "tool_connection.railway_ssh_updated", entityType: "tool_connection", entityId: connection.id, details: { action: req.body.action, grantId: req.body.grantId, enabled: setup?.enabled ?? false } });
    res.json(setup);
  });

  router.post("/tool-connections/:connectionId/services/:toolkitSlug/connect", async (req, res) => {
    const connection = await getAccessibleResource(req, res, svc.getConnection(req.params.connectionId as string), "Tool connection not found");
    if (!connection) return;
    await assertToolConnectionConfigureAccess(req, connection);
    const body = req.body && typeof req.body === "object" ? req.body as Record<string, unknown> : {};
    const result = await svc.startComposioServiceConnect(connection.id, req.params.toolkitSlug as string, {
      ...(typeof body.authConfigId === "string" ? { authConfigId: body.authConfigId } : {}),
      ...(typeof body.callbackUrl === "string" ? { callbackUrl: body.callbackUrl } : {}),
    });
    await logActivity(db, {
      companyId: connection.companyId,
      actorType: "user",

View on GitHub (pinned to 3f1d897a7c)