fix: add gateway connection debug output

This commit is contained in:
Peter Steinberger
2026-01-08 02:51:28 +01:00
parent 1ebde4dc24
commit 6aa6c837e7
8 changed files with 133 additions and 44 deletions

View File

@@ -28,6 +28,11 @@ vi.mock("../infra/tailnet.js", () => ({
}));
vi.mock("./client.js", () => ({
describeGatewayCloseCode: (code: number) => {
if (code === 1000) return "normal closure";
if (code === 1006) return "abnormal closure (no close frame)";
return undefined;
},
GatewayClient: class {
constructor(opts: {
url?: string;

View File

@@ -1,7 +1,11 @@
import { randomUUID } from "node:crypto";
import { loadConfig, resolveGatewayPort } from "../config/config.js";
import {
type ClawdbotConfig,
loadConfig,
resolveGatewayPort,
} from "../config/config.js";
import { pickPrimaryTailnetIPv4 } from "../infra/tailnet.js";
import { GatewayClient } from "./client.js";
import { describeGatewayCloseCode, GatewayClient } from "./client.js";
import { PROTOCOL_VERSION } from "./protocol/index.js";
export type CallGatewayOptions = {
@@ -21,14 +25,26 @@ export type CallGatewayOptions = {
maxProtocol?: number;
};
export async function callGateway<T = unknown>(
opts: CallGatewayOptions,
): Promise<T> {
const timeoutMs = opts.timeoutMs ?? 10_000;
const config = loadConfig();
export type GatewayConnectionDetails = {
url: string;
urlSource: string;
bindMode: string;
preferTailnet: boolean;
tailnetIPv4?: string;
isRemoteMode: boolean;
remoteUrl?: string;
urlOverride?: string;
localUrl: string;
remoteFallbackNote?: string;
message: string;
};
export function buildGatewayConnectionDetails(
opts: { url?: string; config?: ClawdbotConfig } = {},
): GatewayConnectionDetails {
const config = opts.config ?? loadConfig();
const isRemoteMode = config.gateway?.mode === "remote";
const remote = isRemoteMode ? config.gateway?.remote : undefined;
const authToken = config.gateway?.auth?.token;
const localPort = resolveGatewayPort(config);
const tailnetIPv4 = pickPrimaryTailnetIPv4();
const bindMode = config.gateway?.bind ?? "loopback";
@@ -47,6 +63,56 @@ export async function callGateway<T = unknown>(
? remote.url.trim()
: undefined;
const url = urlOverride || remoteUrl || localUrl;
const urlSource = urlOverride
? "cli --url"
: remoteUrl
? "config gateway.remote.url"
: preferTailnet && tailnetIPv4
? `local tailnet ${tailnetIPv4}`
: "local loopback";
const remoteFallbackNote =
isRemoteMode && !urlOverride && !remoteUrl
? "gateway.mode=remote but gateway.remote.url is missing; using local URL."
: undefined;
const bindDetail =
!urlOverride && !remoteUrl ? `Bind: ${bindMode}` : undefined;
const message = [
`Gateway target: ${url}`,
`Source: ${urlSource}`,
bindDetail,
remoteFallbackNote ? `Note: ${remoteFallbackNote}` : undefined,
]
.filter(Boolean)
.join("\n");
return {
url,
urlSource,
bindMode,
preferTailnet,
tailnetIPv4,
isRemoteMode,
remoteUrl,
urlOverride,
localUrl,
remoteFallbackNote,
message,
};
}
export async function callGateway<T = unknown>(
opts: CallGatewayOptions,
): Promise<T> {
const timeoutMs = opts.timeoutMs ?? 10_000;
const config = loadConfig();
const details = buildGatewayConnectionDetails({
url: opts.url,
config,
});
const isRemoteMode = details.isRemoteMode;
const remote = isRemoteMode ? config.gateway?.remote : undefined;
const authToken = config.gateway?.auth?.token;
const url = details.url;
const token =
(typeof opts.token === "string" && opts.token.trim().length > 0
? opts.token.trim()
@@ -67,38 +133,11 @@ export async function callGateway<T = unknown>(
(typeof remote?.password === "string" && remote.password.trim().length > 0
? remote.password.trim()
: undefined);
const urlSource = urlOverride
? "cli --url"
: remoteUrl
? "config gateway.remote.url"
: preferTailnet && tailnetIPv4
? `local tailnet ${tailnetIPv4}`
: "local loopback";
const remoteFallbackNote =
isRemoteMode && !urlOverride && !remoteUrl
? "Note: gateway.mode=remote but gateway.remote.url is missing; using local URL."
: undefined;
const bindDetail =
!urlOverride && !remoteUrl
? `Bind: ${bindMode}`
: undefined;
const connectionDetails = [
`Gateway target: ${url}`,
`Source: ${urlSource}`,
bindDetail,
remoteFallbackNote,
]
.filter(Boolean)
.join("\n");
const connectionDetails = details.message;
const formatCloseError = (code: number, reason: string) => {
const reasonText = reason?.trim() || "no close reason";
const hint =
code === 1006
? "abnormal closure (no close frame)"
: code === 1000
? "normal closure"
: "";
const hint = describeGatewayCloseCode(code) ?? "";
const suffix = hint ? ` ${hint}` : "";
return `gateway closed (${code}${suffix}): ${reasonText}\n${connectionDetails}`;
};

View File

@@ -36,6 +36,17 @@ export type GatewayClientOptions = {
onGap?: (info: { expected: number; received: number }) => void;
};
export const GATEWAY_CLOSE_CODE_HINTS: Readonly<Record<number, string>> = {
1000: "normal closure",
1006: "abnormal closure (no close frame)",
1008: "policy violation",
1012: "service restart",
};
export function describeGatewayCloseCode(code: number): string | undefined {
return GATEWAY_CLOSE_CODE_HINTS[code];
}
export class GatewayClient {
private ws: WebSocket | null = null;
private opts: GatewayClientOptions;