From 4bedec5e01031509f5f1a0d57c0d97bc7259168a Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 28 Oct 2026 19:11:14 +0000 Subject: [PATCH 1/3] test(web): reproduce #26604, desktop auth bootstrap gives up on an unreachable backend The session request fails with a transport error (no HTTP response) on the first attempt, as fetch does while the backend is still restarting. resolveInitialServerAuthGateState() should retry within the bootstrap budget and resolve; on main it rejects with PrimaryEnvironmentRequestError (HTTP 500). Co-Authored-By: Claude Opus 5.5 --- apps/web/src/authBootstrap.test.ts | 49 ++++++++++++++++++++++++++++ apps/web/test/environmentHttpTest.ts | 6 ++-- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/apps/web/src/authBootstrap.test.ts b/apps/web/src/authBootstrap.test.ts index 65c6f8edd3424..0f0cda6c3278b 100644 --- a/apps/web/src/authBootstrap.test.ts +++ b/apps/web/src/authBootstrap.test.ts @@ -8,6 +8,7 @@ import { import { createBrowserHistory } from "@tanstack/react-router"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; +import { HttpClientError, HttpClientRequest } from "effect/http"; import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; import { installEnvironmentHttpTest } from "../test/environmentHttpTest"; @@ -165,6 +166,54 @@ describe("resolveInitialServerAuthGateState", () => { expect(testApi.calls.browserSession).toEqual([{ credential: "desktop-bootstrap-token" }]); }); + it("retries the desktop bootstrap while the backend is still unreachable", async () => { + const nextSession = sequence( + unauthenticatedSession(DESKTOP_AUTH), + authenticatedSession(DESKTOP_AUTH), + ); + const testApi = await installAuthApi({ + session: nextSession, + browserSession: () => Effect.succeed(browserSession(["orchestration:read", "access:write"])), + }); + // What FetchHttpClient yields when nothing listens on the backend port. + const connectionRefused = new HttpClientError.HttpClientError({ + reason: new HttpClientError.TransportError({ + request: HttpClientRequest.get("http://localhost:3773/api/auth/session"), + cause: new TypeError("Failed to fetch"), + }), + }); + let unreachableAttempts = 1; + __setPrimaryHttpRunnerForTests((effect) => { + if (unreachableAttempts > 0) { + unreachableAttempts -= 1; + return Promise.reject(connectionRefused); + } + return testApi.run(effect); + }); + + const testWindow = installTestBrowser("http://localhost/"); + testWindow.desktopBridge = { + getLocalEnvironmentBootstraps: () => [ + { + id: "primary", + label: "Windows", + httpBaseUrl: "http://localhost:3773", + wsBaseUrl: "ws://localhost:3773", + bootstrapToken: "desktop-bootstrap-token", + }, + ], + } as unknown as DesktopBridge; + + const { resolveInitialServerAuthGateState } = await import("./environments/primary"); + + vi.useFakeTimers({ toFake: ["setTimeout", "Date"] }); + await Promise.all([ + expect(resolveInitialServerAuthGateState()).resolves.toEqual({ status: "authenticated" }), + vi.advanceTimersByTimeAsync(1_000), + ]); + expect(testApi.calls.browserSession).toEqual([{ credential: "desktop-bootstrap-token" }]); + }); + it("takes a pairing token from the location hash and strips it immediately", async () => { const testWindow = installTestBrowser("http://localhost/#token=pairing-token"); const { takePairingTokenFromUrl } = await import("./environments/primary"); diff --git a/apps/web/test/environmentHttpTest.ts b/apps/web/test/environmentHttpTest.ts index f7bd2a6025290..cfb4f572b5047 100644 --- a/apps/web/test/environmentHttpTest.ts +++ b/apps/web/test/environmentHttpTest.ts @@ -22,7 +22,7 @@ import { HttpApiTest } from "effect/http-api"; import * as HttpApiBuilder from "effect/http-api/HttpApiBuilder"; import { PrimaryEnvironmentHttpClient } from "../src/environments/primary/httpClient"; -import { __setPrimaryHttpRunnerForTests } from "../src/lib/runtime"; +import { __setPrimaryHttpRunnerForTests, type PrimaryHttpEffectRunner } from "../src/lib/runtime"; type BrowserSessionHandler = ( payload: AuthBrowserSessionRequest, @@ -123,10 +123,12 @@ export async function installEnvironmentHttpTest(scenario: EnvironmentHttpTestSc ); const runtime = ManagedRuntime.make(Layer.succeed(PrimaryEnvironmentHttpClient, client)); - __setPrimaryHttpRunnerForTests((effect) => runtime.runPromise(effect)); + const run: PrimaryHttpEffectRunner = (effect) => runtime.runPromise(effect); + __setPrimaryHttpRunnerForTests(run); return { calls, + run, async dispose() { __setPrimaryHttpRunnerForTests(); await runtime.dispose(); From 3866aa31f3ec05496ab8df7d811eaa3334a6599d Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 28 Oct 2026 19:15:29 +0000 Subject: [PATCH 2/3] fix(web): desktop auth bootstrap keeps retrying while the backend restarts A renderer reload while the local backend was down showed the fatal "fetch-session-state (HTTP 500)" screen on the first connection-refused attempt. fromCause reports a transport failure with no response as status 500, so the bootstrap retry never saw it as transient. Treat a wrapped HttpClientError TransportError as transient; real HTTP 500 responses still fail fast. Fixes pingdotgg/t3code#26604 Co-Authored-By: Claude Opus 5.5 --- apps/web/src/environments/primary/auth.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/web/src/environments/primary/auth.ts b/apps/web/src/environments/primary/auth.ts index daa0894abcbd6..c4ed06174de35 100644 --- a/apps/web/src/environments/primary/auth.ts +++ b/apps/web/src/environments/primary/auth.ts @@ -293,7 +293,7 @@ function waitForBootstrapRetry(delayMs: number): Promise { function isTransientBootstrapError(error: unknown): boolean { if (isPrimaryEnvironmentRequestError(error)) { - return TRANSIENT_BOOTSTRAP_STATUS_CODES.has(error.status); + return TRANSIENT_BOOTSTRAP_STATUS_CODES.has(error.status) || isTransportError(error.cause); } if (error instanceof TypeError) { @@ -303,6 +303,9 @@ function isTransientBootstrapError(error: unknown): boolean { return error instanceof DOMException && error.name === "AbortError"; } +const isTransportError = (error: unknown) => + HttpClientError.isHttpClientError(error) && error.reason._tag === "TransportError"; + async function bootstrapServerAuth(urlCredential: string | null): Promise { const currentSession = await fetchSessionState(); if (currentSession.authenticated && !urlCredential) { From 0887edaccb25cade461d64afdb3761a320fb1666 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 28 Oct 2026 19:21:37 +0000 Subject: [PATCH 3/3] refactor(web): isTransportError is a function declaration like its neighbours Co-Authored-By: Claude Opus 5.5 --- apps/web/src/authBootstrap.test.ts | 2 +- apps/web/src/environments/primary/auth.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/web/src/authBootstrap.test.ts b/apps/web/src/authBootstrap.test.ts index 0f0cda6c3278b..f444c64cb16ad 100644 --- a/apps/web/src/authBootstrap.test.ts +++ b/apps/web/src/authBootstrap.test.ts @@ -196,7 +196,7 @@ describe("resolveInitialServerAuthGateState", () => { getLocalEnvironmentBootstraps: () => [ { id: "primary", - label: "Windows", + label: "Local", httpBaseUrl: "http://localhost:3773", wsBaseUrl: "ws://localhost:3773", bootstrapToken: "desktop-bootstrap-token", diff --git a/apps/web/src/environments/primary/auth.ts b/apps/web/src/environments/primary/auth.ts index c4ed06174de35..98f53229c65d1 100644 --- a/apps/web/src/environments/primary/auth.ts +++ b/apps/web/src/environments/primary/auth.ts @@ -303,8 +303,9 @@ function isTransientBootstrapError(error: unknown): boolean { return error instanceof DOMException && error.name === "AbortError"; } -const isTransportError = (error: unknown) => - HttpClientError.isHttpClientError(error) && error.reason._tag === "TransportError"; +function isTransportError(error: unknown): boolean { + return HttpClientError.isHttpClientError(error) && error.reason._tag === "TransportError"; +} async function bootstrapServerAuth(urlCredential: string | null): Promise { const currentSession = await fetchSessionState();