paperclipai/paperclip · error · ToolGatewayHttpError

error.code

error.code

Error message

error.message

What it means

This is not an independent error but a rethrow path: when an MCP tools/call against a remote server fails, the gateway wraps RailwayError instances into a ToolGatewayHttpError preserving the original HTTP status, message, and reason code (error.code), plus connection/catalog/execution details. The 'message' and 'code' fields here are dynamic — they carry whatever the underlying Railway API/SSH client produced.

Solutions

  1. Read the wrapped status/code in the error details to identify the actual Railway failure (auth vs not-found vs 5xx).
  2. For auth failures, refresh/reconnect the Railway connection token and re-verify the API.
  3. For run-command failures, check the connection's railwaySsh credential refs are populated and the SSH key is valid.
  4. Retry after resolving; transient 5xx/rate-limit responses can be retried with backoff.

Example fix

// before
try { await runRailwayTool(conn, tool, args); } catch { /* opaque failure */ }
// after
try {
  await runRailwayTool(conn, tool, args);
} catch (e) {
  if (e.status === 401 || e.status === 403) await reconnectRailway(conn.id); // refresh token
  else throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

const status = conn.config?.railwayApiStatus;
if (status !== "available") await verifyRailwayApi(conn.id); // avoid most RailwayError causes up front

Type guard

const isRailwayFailure = (e) => e instanceof RailwayError || (typeof e.code === "string" && e.code.startsWith("railway_"));

Try / catch

try {
  return await gateway.invoke({ method: "tools/call", params: { name: railwayTool, arguments: args } });
} catch (e) {
  if (e.status === 401 || e.status === 403) await refreshRailwayToken(e.details.connectionId);
  else if (e.status >= 500 || e.status === 429) await sleep(backoff()); // retry transient
  else throw e;
}

Prevention

When it happens

Trigger: Any railway_* tool execution that throws a RailwayError — e.g. Railway GraphQL API returns a non-2xx status, the API token is invalid, or the SSH command execution for railway_run-command fails — is caught here and rethrown with its original status/message/code.

Common situations: Expired or revoked Railway API token (401/403 from GraphQL); SSH credentials missing/wrong for run-command; Railway project/service ID referenced by the tool no longer exists (404); Railway API rate limiting or outage (5xx).

Related errors


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

Appendix: source

Thrown at server/src/services/tool-gateway.ts:6121

      const sourceTemplateKey =
        typeof connection.config.sourceTemplateKey === "string"
          ? connection.config.sourceTemplateKey
          : null;
      const result = normalizeMcpToolResult(
        payloadRecord.result,
        "mcp_http",
        false,
        sourceTemplateKey,
      );
      await markRemoteConnectionHealth(
        connection,
        "ok",
        "Remote MCP server responded to tools/call.",
      );
      return { result, headerSummary, execution };
    } catch (error) {
      if (error instanceof RailwayError) {
        throw new ToolGatewayHttpError(error.status, error.message, error.code, { connectionId: connection.id, catalogEntryId: entry.id, execution });
      }
      if (error instanceof ToolGatewayHttpError) {
        throw new ToolGatewayHttpError(
          error.status,
          error.message,
          error.reasonCode,
          {
            ...error.details,
            execution: error.details.execution ?? execution,
          },
        );
      }
      if (error instanceof Error && error.name === "AbortError") {
        await markRemoteConnectionHealth(
          connection,
          "error",
          "Remote MCP tool call timed out.",
        );

View on GitHub (pinned to 3f1d897a7c)