From 36eadecea7b51a2d9593a290e269e98494f47f93 Mon Sep 17 00:00:00 2001 From: codedogQBY <1369175442@qq.com> Date: Mon, 25 May 2026 01:34:25 +0800 Subject: [PATCH] fix(mobile): cap TCP fallback listen with 5s timeout so reader can't silently hang MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Symptom: on a dev client where neither @dr.pogodin/react-native-static-server nor react-native-tcp-socket native modules are linked into the binary, the reader spins forever on a sepia background with no error in Metro logs after the Lighttpd 8s timeout warning. Root cause: _startNativeServer caps Lighttpd.start() at 8s and falls through to _startTcpFallback on timeout. But _startTcpFallback's server.listen() callback never fires when the tcp-socket native binding is missing, and the promise sits unresolved. ReaderScreen's await startFileServer() inherits the hang. Fix: wrap server.listen()/server.on("error") in a settle helper with a 5s timeout that closes the server and rejects with a clear actionable message ("rebuild with expo run:ios"). Lighttpd already had the same safety net. This is a dev-environment guardrail — in a properly built binary both servers return in well under 1s. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/lib/reader/local-file-server.ts | 50 ++++++++++++++----- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/packages/app-expo/src/lib/reader/local-file-server.ts b/packages/app-expo/src/lib/reader/local-file-server.ts index c86c0b7c..73274b00 100644 --- a/packages/app-expo/src/lib/reader/local-file-server.ts +++ b/packages/app-expo/src/lib/reader/local-file-server.ts @@ -199,21 +199,45 @@ async function _startTcpFallback(cleanRoot: string): Promise { socket.on("error", () => socket.destroy()); }); - server.on("error", (err: Error) => reject(err)); + // Without a timeout, if the tcp-socket native module isn't linked into the + // dev client, `listen`'s callback never fires and the reader sits on a + // spinner forever with no error. Cap it so ReaderScreen can surface a real + // failure (Lighttpd already has the same safety net at 8s). + let settled = false; + const finish = (work: () => void) => { + if (settled) return; + settled = true; + clearTimeout(timeoutHandle); + work(); + }; + const timeoutHandle = setTimeout(() => { + finish(() => { + try { server.close(); } catch {} + reject( + new Error( + "TCP fallback listen timeout (5s) — react-native-tcp-socket native module likely not linked into the dev client. Rebuild with `expo run:ios`.", + ), + ); + }); + }, 5000); + + server.on("error", (err: Error) => finish(() => reject(err))); server.listen({ port: 0, host: "127.0.0.1" }, () => { - const addr = server.address(); - const port = addr && typeof addr === "object" && "port" in addr ? addr.port : null; - if (!port) { - reject(new Error("Server address unavailable")); - return; - } - const url = `http://127.0.0.1:${port}`; - _tcpServer = server; - _serverDocRoot = cleanRoot; - _serverUrl = url; - console.log(`[FileServer] TCP fallback started: ${url} (root: ${cleanRoot})`); - resolve(url); + finish(() => { + const addr = server.address(); + const port = addr && typeof addr === "object" && "port" in addr ? addr.port : null; + if (!port) { + reject(new Error("Server address unavailable")); + return; + } + const url = `http://127.0.0.1:${port}`; + _tcpServer = server; + _serverDocRoot = cleanRoot; + _serverUrl = url; + console.log(`[FileServer] TCP fallback started: ${url} (root: ${cleanRoot})`); + resolve(url); + }); }); }); }