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
- Read the wrapped status/code in the error details to identify the actual Railway failure (auth vs not-found vs 5xx).
- For auth failures, refresh/reconnect the Railway connection token and re-verify the API.
- For run-command failures, check the connection's railwaySsh credential refs are populated and the SSH key is valid.
- 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
- Keep Railway API tokens valid and re-verify on schedule.
- Ensure railwaySsh credentials are present before using run-command tools.
- Validate project/service identifiers passed to railway tools still exist.
- Implement backoff retry only for 429/5xx Railway statuses.
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
- error.message
- error.reasonCode
- github_attachment_canonical_api_request_failed
- railway_action_blocked
- railway_action_blocked
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)