diff --git a/apps/desktop/src/electron/ElectronProtocol.test.ts b/apps/desktop/src/electron/ElectronProtocol.test.ts index 2db85dafc4da..e5298404995c 100644 --- a/apps/desktop/src/electron/ElectronProtocol.test.ts +++ b/apps/desktop/src/electron/ElectronProtocol.test.ts @@ -141,6 +141,43 @@ describe("ElectronProtocol", () => { }).pipe(Effect.provide(ElectronProtocol.layer)), ); + it.effect("buffers large GET responses to prevent stream truncation", () => + Effect.gen(function* () { + let handler: ((request: Request) => Promise) | undefined; + handleMock.mockImplementation((_scheme, nextHandler) => { + handler = nextHandler; + }); + + // Simulate a large JS bundle (1 MiB) that would be truncated if the + // ReadableStream from net.fetch were forwarded without buffering. + const largePayload = "x".repeat(1024 * 1024); + netFetchMock.mockResolvedValue( + new Response(largePayload, { + headers: { "content-type": "application/javascript" }, + }), + ); + + const response = yield* Effect.scoped( + Effect.gen(function* () { + const protocol = yield* ElectronProtocol.ElectronProtocol; + yield* protocol.registerDesktopProtocol({ + scheme: "t3code-dev", + targetOrigin: new URL("http://127.0.0.1:3773/"), + backendOrigin: new URL("http://127.0.0.1:3774/"), + clerkFrontendApiHostname: undefined, + }); + return yield* Effect.promise(() => + handler!(new Request("t3code-dev://app/assets/bundle.js")), + ); + }), + ); + + const text = yield* Effect.promise(() => response.text()); + assert.equal(text.length, largePayload.length); + assert.equal(text, largePayload); + }).pipe(Effect.provide(ElectronProtocol.layer)), + ); + it.effect("preserves protocol registration failures", () => Effect.gen(function* () { const cause = new Error("protocol registration failed"); diff --git a/apps/desktop/src/electron/ElectronProtocol.ts b/apps/desktop/src/electron/ElectronProtocol.ts index 11459c9ef7a8..cef0c40aea97 100644 --- a/apps/desktop/src/electron/ElectronProtocol.ts +++ b/apps/desktop/src/electron/ElectronProtocol.ts @@ -179,7 +179,24 @@ async function proxyRequest( request.method === "GET" || request.method === "HEAD" ? await fetchWithTransientRetry(targetUrl.toString(), init) : await Electron.net.fetch(targetUrl.toString(), init); - return withContentSecurityPolicy(response, contentSecurityPolicy); + + // Buffer the full response body before re-wrapping. Electron's net.fetch + // returns a ReadableStream that can be truncated when forwarded directly + // into a new Response inside a protocol.handle callback, which causes + // large JS bundles to arrive incomplete and fail with SyntaxError. + const body = + response.body && (request.method === "GET" || request.method === "HEAD") + ? await response.arrayBuffer() + : response.body; + + return withContentSecurityPolicy( + new Response(body, { + status: response.status, + statusText: response.statusText, + headers: response.headers, + }), + contentSecurityPolicy, + ); } const TRANSIENT_FETCH_RETRY_DELAYS_MS = [0, 50, 150] as const;