fix(gateway): preserve canvas tls urls

This commit is contained in:
Vincent Koc
2026-05-04 01:12:41 -07:00
parent f0537e93fb
commit be21d64d08
5 changed files with 65 additions and 1 deletions

View File

@@ -63,6 +63,7 @@ Docs: https://docs.openclaw.ai
- Control UI/Talk: make failed Talk startup errors dismissable and clear the stale Talk error state when dismissed, so missing realtime voice provider configuration does not leave a permanent chat banner. Fixes #77071. Thanks @ijoshdavis.
- Control UI/Talk: stop and clear failed realtime Talk sessions when dismissing runtime error banners, so the next Talk click starts a fresh session instead of only stopping the stale one. Thanks @vincentkoc.
- Control UI/Talk: retry from a failed realtime Talk session on the next Talk click instead of requiring a separate stale-session stop click first. Thanks @vincentkoc.
- Canvas host: preserve the Gateway TLS scheme in browser canvas host URLs and startup mount logs, so direct HTTPS gateways do not advertise insecure canvas links. Thanks @vincentkoc.
- Google Chat: create an isolated Google auth transport per auth client, so google-auth-library interceptor mutations do not accumulate across webhook verification and access-token clients. Thanks @vincentkoc.
- Control UI/performance: cap long-task and long-animation-frame diagnostics in the shared event log, so slow-render telemetry does not evict gateway/plugin events from the Debug and Overview views. Thanks @vincentkoc.
- Gateway/startup: log the canvas host mount only after the HTTP server has bound, so startup logs no longer report the canvas host as mounted before it can serve requests.

View File

@@ -29,6 +29,7 @@ export function attachGatewayWsHandlers(params: GatewayWsRuntimeParams) {
port: params.port,
gatewayHost: params.gatewayHost,
canvasHostEnabled: params.canvasHostEnabled,
canvasHostScheme: params.canvasHostScheme,
canvasHostServerPort: params.canvasHostServerPort,
resolvedAuth: params.resolvedAuth,
getResolvedAuth: params.getResolvedAuth,

View File

@@ -1329,6 +1329,7 @@ export async function startGatewayServer(
}
const { attachGatewayWsHandlers } = await import("./server-ws-runtime.js");
const canvasHostScheme = gatewayTls.enabled ? "https" : "http";
attachGatewayWsHandlers({
wss,
clients,
@@ -1336,6 +1337,7 @@ export async function startGatewayServer(
port,
gatewayHost: bindHost ?? undefined,
canvasHostEnabled: Boolean(canvasHost),
canvasHostScheme,
canvasHostServerPort,
resolvedAuth,
getResolvedAuth,
@@ -1357,7 +1359,7 @@ export async function startGatewayServer(
await startListening();
if (canvasHost?.rootDir) {
logCanvas.info(
`canvas host mounted at http://${bindHost}:${port}${CANVAS_HOST_PATH}/ (root ${canvasHost.rootDir})`,
`canvas host mounted at ${canvasHostScheme}://${bindHost}:${port}${CANVAS_HOST_PATH}/ (root ${canvasHost.rootDir})`,
);
}
startupTrace.mark("http.bound");

View File

@@ -111,6 +111,63 @@ describe("attachGatewayWsConnectionHandler", () => {
);
});
it("uses the gateway TLS scheme for canvas host URLs", async () => {
const listeners = new Map<string, (...args: unknown[]) => void>();
const wss = {
on: vi.fn((event: string, handler: (...args: unknown[]) => void) => {
listeners.set(event, handler);
}),
} as unknown as WebSocketServer;
const socket = Object.assign(new EventEmitter(), {
_socket: {
remoteAddress: "127.0.0.1",
remotePort: 1234,
localAddress: "127.0.0.1",
localPort: 5678,
},
send: vi.fn(),
close: vi.fn(),
});
const upgradeReq = {
headers: { host: "gateway.example.com" },
socket: { localAddress: "127.0.0.1" },
};
attachGatewayWsConnectionHandler({
wss,
clients: new Set(),
preauthConnectionBudget: { release: vi.fn() } as never,
port: 18789,
canvasHostEnabled: true,
canvasHostScheme: "https",
resolvedAuth: createResolvedAuth("token"),
gatewayMethods: [],
events: [],
refreshHealthSnapshot: vi.fn(async () => ({}) as never),
logGateway: createLogger() as never,
logHealth: createLogger() as never,
logWsControl: createLogger() as never,
extraHandlers: {},
broadcast: vi.fn(),
buildRequestContext: () =>
({
unsubscribeAllSessionEvents: vi.fn(),
nodeRegistry: { unregister: vi.fn() },
nodeUnsubscribeAll: vi.fn(),
}) as never,
});
const onConnection = listeners.get("connection");
expect(onConnection).toBeTypeOf("function");
onConnection?.(socket, upgradeReq);
await waitForLazyMessageHandler();
const passed = attachGatewayWsMessageHandlerMock.mock.calls[0]?.[0] as {
canvasHostUrl?: string;
};
expect(passed.canvasHostUrl).toBe("https://gateway.example.com:443");
});
it("rejects late client registration after a pre-connect socket close", async () => {
const listeners = new Map<string, (...args: unknown[]) => void>();
const wss = {

View File

@@ -124,6 +124,7 @@ export type GatewayWsSharedHandlerParams = {
port: number;
gatewayHost?: string;
canvasHostEnabled: boolean;
canvasHostScheme?: "http" | "https";
canvasHostServerPort?: number;
resolvedAuth: ResolvedGatewayAuth;
getResolvedAuth?: () => ResolvedGatewayAuth;
@@ -199,6 +200,7 @@ export function attachGatewayWsConnectionHandler(params: AttachGatewayWsConnecti
port,
gatewayHost,
canvasHostEnabled,
canvasHostScheme,
canvasHostServerPort,
resolvedAuth,
getResolvedAuth = () => resolvedAuth,
@@ -253,6 +255,7 @@ export function attachGatewayWsConnectionHandler(params: AttachGatewayWsConnecti
requestHost: upgradeReq.headers.host,
forwardedProto: upgradeReq.headers["x-forwarded-proto"],
localAddress: upgradeReq.socket?.localAddress,
scheme: canvasHostScheme,
});
logWs("in", "open", { connId, remoteAddr, remotePort, localAddr, localPort, endpoint });