authorgravatar for 7121943+kvnloo@users.noreply.github.comKevin Rajan <7121943+kvnloo@users.noreply.github.com> 2026-10-08 15:31:53-05:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-08 20:31:53+00:00
log580948708b66f2a971199670f76cb6b43119e9a4
treeb2e6fc44156b12f69ae8a90ef6a2032220c19623
parentaf3d842a001d030ed528396bf59cf02bbf373cd0
signature Signed by PGP key B5690EEEBB952194

fix(shared): bound cloudflared download with 10-minute timeout (#14139)

Co-authored-by: Kevin Rajan <kevin@kvnloo.dev>

2 files changed, 57 insertions(+), 1 deletions(-)

packages/shared/src/relayClient.test.ts+50-1
......@@ -6,7 +6,6 @@ import * as Deferred from "effect/Deferred";
66import * as Effect from "effect/Effect";
77import * as Fiber from "effect/Fiber";
88import * as Hex from "effect/encoding/Hex";
9import * as Fiber from "effect/Fiber";
109import * as FileSystem from "effect/FileSystem";
1110import * as Layer from "effect/Layer";
1211import * as PlatformError from "effect/PlatformError";
......@@ -57,6 +56,16 @@ const layerHttpClient = (bytes: Uint8Array) =>
5756 ),
5857 );
5958
59// Records each request and never responds, simulating a wedged endpoint.
60const layerStalledHttpClient = (requests: Array<unknown>) =>
61 Layer.succeed(
62 HttpClient.HttpClient,
63 HttpClient.make((request) => {
64 requests.push(request);
65 return Effect.never;
66 }),
67 );
68
6069// Answers `cloudflared version` with the version recorded for that path, which
6170// defaults to the pinned release; tests change it to simulate other binaries.
6271const layerSpawner = (commands: Array<string>, versions: Record<string, string> = {}) =>
......@@ -767,4 +776,44 @@ describe("RelayClient", () => {
767776 expect(renames.attempts).toBe(1);
768777 }).pipe(Effect.scoped, Effect.provide(layerInstallRuntime(platform))),
769778 );
779
780 it.effect("fails a stalled download after the download timeout", () => {
781 const requests: Array<unknown> = [];
782 return Effect.gen(function* () {
783 const fileSystem = yield* FileSystem.FileSystem;
784 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
785 prefix: "t3-cloudflared-test-",
786 });
787 const manager = yield* RelayClient.makeCloudflaredRelayClient({
788 baseDir,
789 releaseAsset: {
790 url: "https://example.test/cloudflared",
791 sha256: "00".repeat(32),
792 archive: "binary",
793 },
794 });
795
796 const child = yield* Effect.forkChild(manager.install);
797 // Spin until the wedged download is in flight, so the clock
798 // adjustment below cannot run before the timeout is armed.
799 while (requests.length === 0) {
800 yield* Effect.yieldNow;
801 }
802 // The download timeout is 10 minutes; advance past it.
803 yield* TestClock.adjust("11 minutes");
804 const error = yield* Fiber.join(child).pipe(Effect.flip);
805 expect(error).toBeInstanceOf(RelayClient.RelayClientInstallError);
806 expect(error.reason).toBe("download_failed");
807 }).pipe(
808 Effect.scoped,
809 Effect.provide(
810 Layer.mergeAll(
811 NodeServices.layer,
812 layerStalledHttpClient(requests),
813 layerSpawner([]),
814 layerHostRuntime({ PATH: "" }),
815 ),
816 ),
817 );
818 });
770819});
packages/shared/src/relayClient.ts+7
......@@ -30,6 +30,11 @@ export const CLOUDFLARED_VERSION = "2026.10.0";
3030export const CLOUDFLARED_MIN_VERSION = "2025.6.1";
3131const CLOUDFLARED_PATH_ENV_NAME = "T3CODE_CLOUDFLARED_PATH";
3232
33// Generous bound for a ~40MB binary download; without it a stalled
34// connection parks the install forever (same unbounded-wait family as the
35// other relay/cloud network calls).
36const CLOUDFLARED_DOWNLOAD_TIMEOUT = "10 minutes";
37
3338export type RelayClientExecutableSource = "override" | "managed" | "path";
3439
3540export type RelayClientStatus =
......@@ -441,6 +446,7 @@ export const makeCloudflaredRelayClient = Effect.fn("cloudflared.make")(function
441446 ) {
442447 yield* report("downloading");
443448 const response = yield* httpClient.execute(HttpClientRequest.get(asset.url)).pipe(
449 Effect.timeout(CLOUDFLARED_DOWNLOAD_TIMEOUT),
444450 Effect.flatMap(HttpClientResponse.filterStatusOk),
445451 Effect.mapError(
446452 (cause) =>
......@@ -453,6 +459,7 @@ export const makeCloudflaredRelayClient = Effect.fn("cloudflared.make")(function
453459 );
454460 const bytes = new Uint8Array(
455461 yield* response.arrayBuffer.pipe(
462 Effect.timeout(CLOUDFLARED_DOWNLOAD_TIMEOUT),
456463 Effect.mapError(
457464 (cause) =>
458465 new RelayClientInstallError({