From 44ef045614019e46a6897704683a72d279a964d7 Mon Sep 17 00:00:00 2001 From: Mariano <132747814+mbelinky@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:42:28 +0000 Subject: [PATCH] fix(canvas): port remaining iOS branch stability fixes (#18228) * fix(canvas): prevent snapshot disconnects on proxied gateways (cherry picked from commit 2a3c9f746a65f3301c0cfe58ebe6596fed06230f) * fix(canvas): accept url alias for present and navigate (cherry picked from commit 674ee86a0b776cbb738add1920a4031246125312) --------- Co-authored-by: Nimrod Gutman --- .../OpenClawKit/GatewayNodeSession.swift | 8 +++- src/agents/tools/canvas-tool.ts | 14 +++++-- src/gateway/server-constants.ts | 6 ++- src/infra/canvas-host-url.ts | 39 ++++++++++++++++--- 4 files changed, 55 insertions(+), 12 deletions(-) diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayNodeSession.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayNodeSession.swift index 6311b4632cb..d0303f7e997 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayNodeSession.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayNodeSession.swift @@ -85,7 +85,13 @@ public actor GatewayNodeSession { latch.resume(result) } timeoutTask = Task.detached { - try? await Task.sleep(nanoseconds: UInt64(timeout) * 1_000_000) + do { + try await Task.sleep(nanoseconds: UInt64(timeout) * 1_000_000) + } catch { + // Expected when invoke finishes first and cancels the timeout task. + return + } + guard !Task.isCancelled else { return } timeoutLogger.info("node invoke timeout fired id=\(request.id, privacy: .public)") latch.resume(BridgeInvokeResponse( id: request.id, diff --git a/src/agents/tools/canvas-tool.ts b/src/agents/tools/canvas-tool.ts index 44ddea30fcc..6a57a3c0736 100644 --- a/src/agents/tools/canvas-tool.ts +++ b/src/agents/tools/canvas-tool.ts @@ -87,8 +87,13 @@ export function createCanvasTool(): AnyAgentTool { height: typeof params.height === "number" ? params.height : undefined, }; const invokeParams: Record = {}; - if (typeof params.target === "string" && params.target.trim()) { - invokeParams.url = params.target.trim(); + // Accept both `target` and `url` for present to match common caller expectations. + // `target` remains the canonical field for CLI compatibility. + const presentTarget = + readStringParam(params, "target", { trim: true }) ?? + readStringParam(params, "url", { trim: true }); + if (presentTarget) { + invokeParams.url = presentTarget; } if ( Number.isFinite(placement.x) || @@ -105,7 +110,10 @@ export function createCanvasTool(): AnyAgentTool { await invoke("canvas.hide", undefined); return jsonResult({ ok: true }); case "navigate": { - const url = readStringParam(params, "url", { required: true }); + // Support `target` as an alias so callers can reuse the same field across present/navigate. + const url = + readStringParam(params, "url", { trim: true }) ?? + readStringParam(params, "target", { required: true, trim: true, label: "url" }); await invoke("canvas.navigate", { url }); return jsonResult({ ok: true }); } diff --git a/src/gateway/server-constants.ts b/src/gateway/server-constants.ts index 03107331fed..d33c6fa7bc2 100644 --- a/src/gateway/server-constants.ts +++ b/src/gateway/server-constants.ts @@ -1,5 +1,7 @@ -export const MAX_PAYLOAD_BYTES = 8 * 1024 * 1024; // cap incoming frame size (~8 MiB; fits ~5,000,000 decoded bytes as base64 + JSON overhead) -export const MAX_BUFFERED_BYTES = 16 * 1024 * 1024; // per-connection send buffer limit (2x max payload) +// Keep server maxPayload aligned with gateway client maxPayload so high-res canvas snapshots +// don't get disconnected mid-invoke with "Max payload size exceeded". +export const MAX_PAYLOAD_BYTES = 25 * 1024 * 1024; +export const MAX_BUFFERED_BYTES = 50 * 1024 * 1024; // per-connection send buffer limit (2x max payload) const DEFAULT_MAX_CHAT_HISTORY_MESSAGES_BYTES = 6 * 1024 * 1024; // keep history responses comfortably under client WS limits let maxChatHistoryMessagesBytes = DEFAULT_MAX_CHAT_HISTORY_MESSAGES_BYTES; diff --git a/src/infra/canvas-host-url.ts b/src/infra/canvas-host-url.ts index b8272c58539..c9776aac5e9 100644 --- a/src/infra/canvas-host-url.ts +++ b/src/infra/canvas-host-url.ts @@ -25,14 +25,25 @@ const normalizeHost = (value: HostSource, rejectLoopback: boolean) => { return trimmed; }; -const parseHostHeader = (value: HostSource) => { +type ParsedHostHeader = { + host: string; + port?: number; +}; + +const parseHostHeader = (value: HostSource): ParsedHostHeader => { if (!value) { - return ""; + return { host: "" }; } try { - return new URL(`http://${String(value).trim()}`).hostname; + const parsed = new URL(`http://${String(value).trim()}`); + const portRaw = parsed.port.trim(); + const port = portRaw ? Number.parseInt(portRaw, 10) : undefined; + return { + host: parsed.hostname, + port: Number.isFinite(port) ? port : undefined, + }; } catch { - return ""; + return { host: "" }; } }; @@ -54,13 +65,29 @@ export function resolveCanvasHostUrl(params: CanvasHostUrlParams) { (parseForwardedProto(params.forwardedProto)?.trim() === "https" ? "https" : "http"); const override = normalizeHost(params.hostOverride, true); - const requestHost = normalizeHost(parseHostHeader(params.requestHost), !!override); + const parsedRequestHost = parseHostHeader(params.requestHost); + const requestHost = normalizeHost(parsedRequestHost.host, !!override); const localAddress = normalizeHost(params.localAddress, Boolean(override || requestHost)); const host = override || requestHost || localAddress; if (!host) { return undefined; } + + // When the websocket is proxied over HTTPS (for example Tailscale Serve), the gateway's + // internal listener still runs on 18789. In that case, expose the public port instead of + // advertising the internal one back to clients. + let exposedPort = port; + if (!override && requestHost && port === 18789) { + if (parsedRequestHost.port && parsedRequestHost.port > 0) { + exposedPort = parsedRequestHost.port; + } else if (scheme === "https") { + exposedPort = 443; + } else if (scheme === "http") { + exposedPort = 80; + } + } + const formatted = host.includes(":") ? `[${host}]` : host; - return `${scheme}://${formatted}:${port}`; + return `${scheme}://${formatted}:${exposedPort}`; }