fix(desktop): harden gateway proxy startup

This commit is contained in:
Xubin Ren
2026-06-06 00:19:31 +08:00
parent dae86edba4
commit 5606653f47
+65 -3
View File
@@ -1,7 +1,7 @@
import { spawn, type ChildProcess } from "node:child_process"; import { spawn, type ChildProcess } from "node:child_process";
import { randomBytes } from "node:crypto"; import { randomBytes } from "node:crypto";
import { createWriteStream, existsSync } from "node:fs"; import { createWriteStream, existsSync } from "node:fs";
import { mkdir, writeFile } from "node:fs/promises"; import { mkdir, rm, writeFile } from "node:fs/promises";
import http from "node:http"; import http from "node:http";
import path from "node:path"; import path from "node:path";
import { pathToFileURL } from "node:url"; import { pathToFileURL } from "node:url";
@@ -46,6 +46,9 @@ const APP_HOST = "app";
const HOST_SOCKET_PROTOCOL = "nanobot-host:"; const HOST_SOCKET_PROTOCOL = "nanobot-host:";
const HOST_SOCKET_HOST = "engine"; const HOST_SOCKET_HOST = "engine";
const SAFE_EXTERNAL_PROTOCOLS = new Set(["https:", "http:", "mailto:"]); const SAFE_EXTERNAL_PROTOCOLS = new Set(["https:", "http:", "mailto:"]);
const GATEWAY_REQUEST_TIMEOUT_MS = 12_000;
const GATEWAY_REQUEST_RETRIES = 2;
const GATEWAY_RETRY_DELAY_MS = 80;
protocol.registerSchemesAsPrivileged([ protocol.registerSchemesAsPrivileged([
{ {
@@ -273,6 +276,30 @@ async function fetchGateway(
headers?: Headers | Record<string, string>; headers?: Headers | Record<string, string>;
method: string; method: string;
}, },
): Promise<Response> {
let lastError: unknown;
for (let attempt = 0; attempt <= GATEWAY_REQUEST_RETRIES; attempt += 1) {
try {
return await fetchGatewayOnce(current, requestPath, init);
} catch (error) {
lastError = error;
if (!isTransientGatewayError(error) || attempt >= GATEWAY_REQUEST_RETRIES) {
break;
}
await new Promise((resolve) => setTimeout(resolve, GATEWAY_RETRY_DELAY_MS));
}
}
throw lastError instanceof Error ? lastError : new Error("gateway request failed");
}
async function fetchGatewayOnce(
current: HostRuntime,
requestPath: string,
init: {
body?: ArrayBuffer;
headers?: Headers | Record<string, string>;
method: string;
},
): Promise<Response> { ): Promise<Response> {
const body = init.body ? Buffer.from(init.body) : undefined; const body = init.body ? Buffer.from(init.body) : undefined;
const headers: http.OutgoingHttpHeaders = {}; const headers: http.OutgoingHttpHeaders = {};
@@ -288,6 +315,12 @@ async function fetchGateway(
if (body) headers["content-length"] = String(body.length); if (body) headers["content-length"] = String(body.length);
return await new Promise<Response>((resolve, reject) => { return await new Promise<Response>((resolve, reject) => {
let settled = false;
const fail = (error: Error) => {
if (settled) return;
settled = true;
reject(error);
};
const req = http.request( const req = http.request(
{ {
socketPath: current.socketPath, socketPath: current.socketPath,
@@ -299,6 +332,8 @@ async function fetchGateway(
const chunks: Buffer[] = []; const chunks: Buffer[] = [];
res.on("data", (chunk: Buffer) => chunks.push(chunk)); res.on("data", (chunk: Buffer) => chunks.push(chunk));
res.on("end", () => { res.on("end", () => {
if (settled) return;
settled = true;
const responseHeaders = new Headers(); const responseHeaders = new Headers();
for (const [key, value] of Object.entries(res.headers)) { for (const [key, value] of Object.entries(res.headers)) {
if (Array.isArray(value)) { if (Array.isArray(value)) {
@@ -317,16 +352,36 @@ async function fetchGateway(
}); });
}, },
); );
req.on("error", reject); req.setTimeout(GATEWAY_REQUEST_TIMEOUT_MS, () => {
req.destroy(new Error(`gateway request timed out after ${GATEWAY_REQUEST_TIMEOUT_MS}ms`));
});
req.on("error", fail);
if (body) req.write(body); if (body) req.write(body);
req.end(); req.end();
}); });
} }
function isTransientGatewayError(error: unknown): boolean {
const code = typeof error === "object" && error !== null
? (error as { code?: unknown }).code
: undefined;
if (
code === "ECONNRESET" ||
code === "ECONNREFUSED" ||
code === "EPIPE" ||
code === "ETIMEDOUT"
) {
return true;
}
const message = error instanceof Error ? error.message : "";
return message.includes("socket hang up") || message.includes("timed out");
}
async function startGateway(): Promise<HostRuntime> { async function startGateway(): Promise<HostRuntime> {
const root = repoRoot(); const root = repoRoot();
const dirs = await ensureAppDirs(); const dirs = await ensureAppDirs();
const socketPath = engineSocketPath(); const socketPath = engineSocketPath();
await rm(socketPath, { force: true });
const secret = randomBytes(32).toString("base64url"); const secret = randomBytes(32).toString("base64url");
const python = pythonExecutable(); const python = pythonExecutable();
const args = [ const args = [
@@ -460,11 +515,18 @@ async function proxyToGateway(request: Request): Promise<Response> {
if (request.method !== "GET" && request.method !== "HEAD") { if (request.method !== "GET" && request.method !== "HEAD") {
init.body = await request.arrayBuffer(); init.body = await request.arrayBuffer();
} }
const response = await fetchGateway( let response: Response;
try {
response = await fetchGateway(
runtime, runtime,
`${requestUrl.pathname}${requestUrl.search}`, `${requestUrl.pathname}${requestUrl.search}`,
init, init,
); );
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(`gateway proxy request failed: ${message}`);
return new Response("Engine unavailable", { status: 503 });
}
if (requestUrl.pathname !== "/webui/bootstrap" || !response.ok) { if (requestUrl.pathname !== "/webui/bootstrap" || !response.ok) {
return response; return response;
} }