fix(apps): bind IPv6 loopback too — blank apps in-app

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).
This commit is contained in:
Gagan 2026-07-04 17:30:55 +05:30
parent 7f15f67273
commit c81739f9f7

View file

@ -54,6 +54,11 @@ const MIME_TYPES: Record<string, string> = {
// ---------------------------------------------------------------------------
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<void> | null = null;
let watcher: FSWatcher | null = null;
let serverError: string | null = null;
@ -708,8 +713,9 @@ export async function init(): Promise<void> {
try {
await fsp.mkdir(APPS_DIR, { recursive: true });
await startWatcher();
const expressApp = createApp();
await new Promise<void>((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<void> {
reject(error);
});
});
// Dual-stack loopback: also listen on ::1 (see server6 note above).
// Best-effort — some machines have IPv6 disabled.
await new Promise<void>((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<void> {
}
eventClients.clear();
const s6 = server6;
server6 = null;
if (s6) {
await new Promise<void>((resolve) => {
s6.close(() => resolve());
});
}
const s = server;
server = null;
if (!s) return;