From c81739f9f723f20ecbebb44611737c6e8887de02 Mon Sep 17 00:00:00 2001 From: Gagan Date: Sat, 4 Jul 2026 17:30:55 +0530 Subject: [PATCH] =?UTF-8?q?fix(apps):=20bind=20IPv6=20loopback=20too=20?= =?UTF-8?q?=E2=80=94=20blank=20apps=20in-app?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit macOS's resolver maps *.apps.localhost to ::1 only, so Electron's iframe connects to [::1]:3210 while the server listened on 127.0.0.1 only — connection refused, blank app, while external browsers succeeded via their own IPv4 fallback. Listen on both loopback addresses (::1 best-effort for IPv6-disabled machines). --- apps/x/packages/core/src/apps/server.ts | 28 ++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/apps/x/packages/core/src/apps/server.ts b/apps/x/packages/core/src/apps/server.ts index f9a39785..cc58a69e 100644 --- a/apps/x/packages/core/src/apps/server.ts +++ b/apps/x/packages/core/src/apps/server.ts @@ -54,6 +54,11 @@ const MIME_TYPES: Record = { // --------------------------------------------------------------------------- let server: Server | null = null; +// IPv6 loopback listener. REQUIRED on macOS: the OS resolver maps +// *.apps.localhost to ::1 (only), so Electron's iframe connects to [::1]:3210 — +// binding just 127.0.0.1 makes in-app requests fail (blank app) while external +// browsers succeed via their own IPv4 fallback. +let server6: Server | null = null; let startPromise: Promise | null = null; let watcher: FSWatcher | null = null; let serverError: string | null = null; @@ -708,8 +713,9 @@ export async function init(): Promise { try { await fsp.mkdir(APPS_DIR, { recursive: true }); await startWatcher(); + const expressApp = createApp(); await new Promise((resolve, reject) => { - const s = createApp().listen(APPS_PORT, '127.0.0.1', () => { + const s = expressApp.listen(APPS_PORT, '127.0.0.1', () => { server = s; serverError = null; console.log(`[Apps] server on 127.0.0.1:${APPS_PORT} (host suffix ${APPS_HOST_SUFFIX}), dir ${APPS_DIR}`); @@ -724,6 +730,18 @@ export async function init(): Promise { reject(error); }); }); + // Dual-stack loopback: also listen on ::1 (see server6 note above). + // Best-effort — some machines have IPv6 disabled. + await new Promise((resolve) => { + const s6 = expressApp.listen(APPS_PORT, '::1', () => { + server6 = s6; + resolve(); + }); + s6.on('error', (error: NodeJS.ErrnoException) => { + console.warn(`[Apps] IPv6 loopback listen failed (${error.code}); continuing IPv4-only`); + resolve(); + }); + }); } catch (error) { serverError = error instanceof Error ? error.message : String(error); await shutdown(); @@ -753,6 +771,14 @@ export async function shutdown(): Promise { } eventClients.clear(); + const s6 = server6; + server6 = null; + if (s6) { + await new Promise((resolve) => { + s6.close(() => resolve()); + }); + } + const s = server; server = null; if (!s) return;