authorgravatar for 8172873+jamesvillarrubia@users.noreply.github.comJames Villarrubia <8172873+jamesvillarrubia@users.noreply.github.com> 2026-10-06 03:09:22-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-06 00:09:22-07:00
log76d3c96fd5b89cca2c06aeaceaea16100c75084a
tree58f7dcca6b955af772228a71ba4ed5be0af68f18
parent4ae976dbae39b3e80243b864a8b61da00f642dc4
signature Signed by PGP key B5690EEEBB952194

fix(desktop): retry transient bearer bootstrap and degrade on session fetch failure (#12919)


2 files changed, 119 insertions(+), 0 deletions(-)

apps/desktop/src/backend/DesktopLocalEnvironmentAuth.test.ts+90
......@@ -1,8 +1,11 @@
11import { assert, describe, it } from "@effect/vitest";
2import * as Duration from "effect/Duration";
23import * as Effect from "effect/Effect";
4import * as Fiber from "effect/Fiber";
35import * as Layer from "effect/Layer";
46import * as Ref from "effect/Ref";
57import * as HttpClient from "effect/http/HttpClient";
8import * as HttpClientRequest from "effect/http/HttpClientRequest";
69import * as HttpClientResponse from "effect/http/HttpClientResponse";
710import { PRIMARY_LOCAL_ENVIRONMENT_ID } from "@t3tools/contracts";
811import {
......@@ -144,4 +147,91 @@ describe("DesktopLocalEnvironmentAuth", () => {
144147 );
145148 }).pipe(Effect.provide(TestClock.layer())),
146149 );
150
151 const tokenResponse = (request: HttpClientRequest.HttpClientRequest) =>
152 HttpClientResponse.fromWeb(
153 request,
154 new Response(
155 JSON.stringify({
156 access_token: "desktop-bearer-token",
157 issued_token_type: "urn:ietf:params:oauth:token-type:access_token",
158 token_type: "Bearer",
159 expires_in: 3600,
160 scope: "orchestration:read",
161 }),
162 { status: 200, headers: { "content-type": "application/json" } },
163 ),
164 );
165 const layerPool = Layer.succeed(DesktopBackendPool.DesktopBackendPool, {
166 list: Effect.succeed([
167 {
168 id: PRIMARY_LOCAL_ENVIRONMENT_ID,
169 label: Effect.succeed("Windows"),
170 currentConfig: Effect.succeedSome(config),
171 },
172 ]),
173 } as unknown as DesktopBackendPool.DesktopBackendPool["Service"]);
174 // Answers the first `failures` exchanges with `failure`, then with a token.
175 const makeExchange = (failures: number, failure: () => Response) =>
176 Effect.gen(function* () {
177 const requestCount = yield* Ref.make(0);
178 const layerHttpClient = Layer.succeed(
179 HttpClient.HttpClient,
180 HttpClient.make((request) =>
181 Ref.updateAndGet(requestCount, (count) => count + 1).pipe(
182 Effect.map((count) =>
183 count <= failures
184 ? HttpClientResponse.fromWeb(request, failure())
185 : tokenResponse(request),
186 ),
187 ),
188 ),
189 );
190 const auth = yield* DesktopLocalEnvironmentAuth.DesktopLocalEnvironmentAuth.pipe(
191 Effect.provide(
192 DesktopLocalEnvironmentAuth.layer.pipe(
193 Layer.provide(Layer.mergeAll(layerPool, layerHttpClient)),
194 ),
195 ),
196 );
197 return { auth, requestCount };
198 });
199
200 it.effect("retries a backend that is still starting", () =>
201 Effect.gen(function* () {
202 const { auth, requestCount } = yield* makeExchange(
203 2,
204 () => new Response("", { status: 503 }),
205 );
206
207 const fiber = yield* auth.getBearerToken.pipe(Effect.forkChild);
208 yield* TestClock.adjust(Duration.seconds(1));
209
210 assert.strictEqual(yield* Fiber.join(fiber), "desktop-bearer-token");
211 assert.strictEqual(yield* Ref.get(requestCount), 3);
212 }).pipe(Effect.provide(TestClock.layer())),
213 );
214
215 it.effect("does not retry a rejected bootstrap credential", () =>
216 Effect.gen(function* () {
217 const { auth, requestCount } = yield* makeExchange(1, () =>
218 Response.json(
219 {
220 _tag: "EnvironmentAuthInvalidError",
221 code: "auth_invalid",
222 reason: "invalid_credential",
223 traceId: "trace-1",
224 },
225 { status: 401 },
226 ),
227 );
228
229 const fiber = yield* auth.getBearerToken.pipe(Effect.flip, Effect.forkChild);
230 yield* TestClock.adjust(Duration.seconds(1));
231 const error = yield* Fiber.join(fiber);
232
233 assert.strictEqual(error._tag, "DesktopLocalEnvironmentAuthSessionBootstrapError");
234 assert.strictEqual(yield* Ref.get(requestCount), 1);
235 }).pipe(Effect.provide(TestClock.layer())),
236 );
147237});
apps/desktop/src/backend/DesktopLocalEnvironmentAuth.ts+29
......@@ -1,18 +1,41 @@
11import { bootstrapRemoteBearerSession } from "@t3tools/client-runtime/authorization";
2import type { RemoteEnvironmentRequestError } from "@t3tools/client-runtime/rpc";
23import { PRIMARY_LOCAL_ENVIRONMENT_ID } from "@t3tools/contracts";
34import { currentDesktopBootstrapToken } from "@t3tools/shared/desktopBootstrapToken";
45import * as Clock from "effect/Clock";
56import * as Context from "effect/Context";
7import * as Duration from "effect/Duration";
68import * as Effect from "effect/Effect";
79import * as Layer from "effect/Layer";
810import * as Option from "effect/Option";
911import * as Ref from "effect/Ref";
12import * as Schedule from "effect/Schedule";
1013import * as Schema from "effect/Schema";
1114import * as Semaphore from "effect/Semaphore";
1215import * as HttpClient from "effect/http/HttpClient";
1316
1417import * as DesktopBackendPool from "./DesktopBackendPool.ts";
1518
19// A loopback /oauth/token exchange can fail transiently while the backend
20// settles (a 502-504, a refused or reset connection, a timeout). Retry those
21// for as long as the renderer's own bootstrap does; a rejected credential or a
22// server error is final.
23const BOOTSTRAP_TRANSIENT_RETRY_TIMEOUT = Duration.seconds(15);
24const BOOTSTRAP_TRANSIENT_RETRY_INTERVAL = Duration.millis(500);
25const TRANSIENT_BOOTSTRAP_STATUS_CODES = new Set([502, 503, 504]);
26
27const isTransientBearerBootstrapError = (error: RemoteEnvironmentRequestError): boolean => {
28 switch (error._tag) {
29 case "RemoteEnvironmentAuthFetchError":
30 case "RemoteEnvironmentAuthTimeoutError":
31 return true;
32 case "RemoteEnvironmentAuthUndeclaredStatusError":
33 return TRANSIENT_BOOTSTRAP_STATUS_CODES.has(error.status);
34 default:
35 return false;
36 }
37};
38
1639export class DesktopLocalEnvironmentAuthBackendNotConfiguredError extends Schema.TaggedError<DesktopLocalEnvironmentAuthBackendNotConfiguredError>()(
1740 "DesktopLocalEnvironmentAuthBackendNotConfiguredError",
1841 {},
......@@ -86,6 +109,12 @@ export const make = Effect.gen(function* () {
86109 },
87110 }).pipe(
88111 Effect.provideService(HttpClient.HttpClient, httpClient),
112 Effect.retry({
113 while: isTransientBearerBootstrapError,
114 schedule: Schedule.spaced(BOOTSTRAP_TRANSIENT_RETRY_INTERVAL),
115 }),
116 // Bounds the attempts and any request still in flight at the deadline.
117 Effect.timeout(BOOTSTRAP_TRANSIENT_RETRY_TIMEOUT),
89118 Effect.mapError(
90119 (cause) =>
91120 new DesktopLocalEnvironmentAuthSessionBootstrapError({