From 4cf2e448f9d2e33153ca899b0f7677b46acde335 Mon Sep 17 00:00:00 2001 From: hsteude Date: Tue, 26 May 2026 11:42:05 +0200 Subject: [PATCH 1/4] fix: prevent UI proxy session load hangs --- app-prefixable/dev.ts | 5 +++++ app-prefixable/src/context/global-events.tsx | 10 +++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app-prefixable/dev.ts b/app-prefixable/dev.ts index e453ef55..727b0d15 100644 --- a/app-prefixable/dev.ts +++ b/app-prefixable/dev.ts @@ -94,6 +94,9 @@ const server = Bun.serve<{ target: string }>({ if (isApiPath(strippedPath)) { const target = new URL(strippedPath + url.search, API_URL) const headers = new Headers(req.headers) + headers.delete("host") + headers.delete("connection") + headers.delete("content-length") // SSE requests - just pass through the response body directly if (strippedPath.startsWith("/event")) { @@ -102,6 +105,7 @@ const server = Bun.serve<{ target: string }>({ const response = await fetch(target.toString(), { method: req.method, headers, + signal: req.signal, }) if (!response.ok) { @@ -140,6 +144,7 @@ const server = Bun.serve<{ target: string }>({ method: req.method, headers, body, + signal: req.signal, }) return withNoStoreHeaders(normalizeProxiedResponse(response)) } catch (e) { diff --git a/app-prefixable/src/context/global-events.tsx b/app-prefixable/src/context/global-events.tsx index 686dd4d9..6fd21bc0 100644 --- a/app-prefixable/src/context/global-events.tsx +++ b/app-prefixable/src/context/global-events.tsx @@ -10,6 +10,8 @@ import { createStore, produce } from "solid-js/store" import { useServer } from "./server" import { createSSEParser } from "../utils/sse" +const MAX_GLOBAL_EVENT_CONNECTIONS = 16 + /** * Alert priority: permission (highest) > question > busy */ @@ -567,18 +569,20 @@ export function GlobalEventsProvider(props: ParentProps & { return } - const wanted = new Set(current.dirs) + const wanted = new Set(current.dirs.slice(0, MAX_GLOBAL_EVENT_CONNECTIONS + 1)) if (current.active) wanted.delete(current.active) + const limitedWanted = new Set([...wanted].slice(0, MAX_GLOBAL_EVENT_CONNECTIONS)) + // Disconnect directories we no longer need (including newly-active project) for (const dir of [...connections.keys()]) { - if (!wanted.has(dir)) { + if (!limitedWanted.has(dir)) { disconnectDirectory(dir) } } // Connect to new inactive directories - for (const dir of wanted) { + for (const dir of limitedWanted) { if (!connections.has(dir)) { connectToDirectory(dir) } From 3b05d6236df51f71d1b9641bf84922b1dd25bd1e Mon Sep 17 00:00:00 2001 From: hsteude Date: Tue, 26 May 2026 11:46:17 +0200 Subject: [PATCH 2/4] fix: strip dev proxy hop-by-hop headers --- app-prefixable/dev.ts | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/app-prefixable/dev.ts b/app-prefixable/dev.ts index 727b0d15..e359c0f3 100644 --- a/app-prefixable/dev.ts +++ b/app-prefixable/dev.ts @@ -48,6 +48,31 @@ function withNoStoreHeaders(response: Response) { }) } +function stripHopByHopHeaders(headers: Headers) { + const connection = headers.get("connection") + if (connection) { + for (const name of connection.split(",")) { + const trimmed = name.trim() + if (trimmed) headers.delete(trimmed) + } + } + + for (const name of [ + "connection", + "content-length", + "host", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailer", + "transfer-encoding", + "upgrade", + ]) { + headers.delete(name) + } +} + const server = Bun.serve<{ target: string }>({ port: PORT, idleTimeout: 0, // Disable timeout for SSE connections @@ -94,9 +119,7 @@ const server = Bun.serve<{ target: string }>({ if (isApiPath(strippedPath)) { const target = new URL(strippedPath + url.search, API_URL) const headers = new Headers(req.headers) - headers.delete("host") - headers.delete("connection") - headers.delete("content-length") + stripHopByHopHeaders(headers) // SSE requests - just pass through the response body directly if (strippedPath.startsWith("/event")) { From 20f9def06f6a4dc2a4260b4d05d75b9a59a45162 Mon Sep 17 00:00:00 2001 From: hsteude Date: Tue, 26 May 2026 15:59:28 +0200 Subject: [PATCH 3/4] fix: enforce proxy connection cap on reconnect --- app-prefixable/dev.ts | 13 ++++++++++++- app-prefixable/src/context/global-events.tsx | 20 ++++++++++++++------ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/app-prefixable/dev.ts b/app-prefixable/dev.ts index e359c0f3..f05a4b75 100644 --- a/app-prefixable/dev.ts +++ b/app-prefixable/dev.ts @@ -73,6 +73,17 @@ function stripHopByHopHeaders(headers: Headers) { } } +function normalizeDevProxiedResponse(response: Response) { + const normalized = normalizeProxiedResponse(response) + const headers = new Headers(normalized.headers) + stripHopByHopHeaders(headers) + return new Response(normalized.body, { + status: normalized.status, + statusText: normalized.statusText, + headers, + }) +} + const server = Bun.serve<{ target: string }>({ port: PORT, idleTimeout: 0, // Disable timeout for SSE connections @@ -169,7 +180,7 @@ const server = Bun.serve<{ target: string }>({ body, signal: req.signal, }) - return withNoStoreHeaders(normalizeProxiedResponse(response)) + return withNoStoreHeaders(normalizeDevProxiedResponse(response)) } catch (e) { console.error("[Proxy] API error:", e) return withNoStoreHeaders(new Response("API proxy error", { status: 502 })) diff --git a/app-prefixable/src/context/global-events.tsx b/app-prefixable/src/context/global-events.tsx index 6fd21bc0..754ff666 100644 --- a/app-prefixable/src/context/global-events.tsx +++ b/app-prefixable/src/context/global-events.tsx @@ -159,6 +159,12 @@ export function GlobalEventsProvider(props: ParentProps & { }, 5000)) } + function wantedDirectories(dirs: string[], active: string | undefined) { + const wanted = new Set(dirs.slice(0, MAX_GLOBAL_EVENT_CONNECTIONS + 1)) + if (active) wanted.delete(active) + return new Set([...wanted].slice(0, MAX_GLOBAL_EVENT_CONNECTIONS)) + } + function connectToDirectory(dir: string) { if (connections.has(dir)) return // Don't connect when a remote server is active @@ -351,9 +357,8 @@ export function GlobalEventsProvider(props: ParentProps & { if (disposed) return // Don't reconnect when a remote server is active — local projects don't exist there if (!activeServer().isDefault) return - const active = props.activeDirectory() - const wanted = props.projects().some((p) => p.worktree === dir) - if (wanted && dir !== active) { + const wanted = wantedDirectories(props.projects().map((p) => p.worktree), props.activeDirectory()) + if (wanted.has(dir)) { connectToDirectory(dir) return } @@ -569,10 +574,13 @@ export function GlobalEventsProvider(props: ParentProps & { return } - const wanted = new Set(current.dirs.slice(0, MAX_GLOBAL_EVENT_CONNECTIONS + 1)) - if (current.active) wanted.delete(current.active) + const limitedWanted = wantedDirectories(current.dirs, current.active) - const limitedWanted = new Set([...wanted].slice(0, MAX_GLOBAL_EVENT_CONNECTIONS)) + for (const [dir, timer] of reconnectTimers) { + if (limitedWanted.has(dir)) continue + clearTimeout(timer) + reconnectTimers.delete(dir) + } // Disconnect directories we no longer need (including newly-active project) for (const dir of [...connections.keys()]) { From bb6bd556ebb708562bb71fb1bdf5cbbb955c7d1d Mon Sep 17 00:00:00 2001 From: hsteude Date: Tue, 26 May 2026 16:22:03 +0200 Subject: [PATCH 4/4] fix: clean up capped event reconnects --- app-prefixable/src/context/global-events.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app-prefixable/src/context/global-events.tsx b/app-prefixable/src/context/global-events.tsx index 754ff666..c4823812 100644 --- a/app-prefixable/src/context/global-events.tsx +++ b/app-prefixable/src/context/global-events.tsx @@ -576,10 +576,9 @@ export function GlobalEventsProvider(props: ParentProps & { const limitedWanted = wantedDirectories(current.dirs, current.active) - for (const [dir, timer] of reconnectTimers) { + for (const [dir] of [...reconnectTimers]) { if (limitedWanted.has(dir)) continue - clearTimeout(timer) - reconnectTimers.delete(dir) + disconnectDirectory(dir) } // Disconnect directories we no longer need (including newly-active project)