From a31ec5a2623edd6339f82655b77c4d6f63ca2648 Mon Sep 17 00:00:00 2001 From: Adamulek123 Date: Wed, 16 Sep 2026 07:29:20 +0200 Subject: [PATCH 1/9] fix(server): bound foreign-body parse in pair probe --- apps/server/src/cli/pair.test.ts | 45 ++++++++++++++++++++++++++++++++ apps/server/src/cli/pair.ts | 24 ++++++++++++++--- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/apps/server/src/cli/pair.test.ts b/apps/server/src/cli/pair.test.ts index dd15c41fdd91..ae49bda03020 100644 --- a/apps/server/src/cli/pair.test.ts +++ b/apps/server/src/cli/pair.test.ts @@ -288,4 +288,49 @@ describe("t3 pair", () => { assert.include(rendered, "No running T3 Code server found."); }).pipe(Effect.provide(NodeServices.layer)), ); + + it.effect("does not decode an oversized stranger body as a server descriptor", () => + Effect.acquireUseRelease( + Effect.callback((resume) => { + const server = NodeHttp.createServer((request, response) => { + if (request.url === "/.well-known/t3/environment") { + response.writeHead(200, { "content-type": "application/json" }); + // Valid JSON shape, far beyond any real descriptor: discovery must + // classify the occupant without a Schema decode, not pair with it. + response.end(JSON.stringify({ padding: "x".repeat(128 * 1024) })); + return; + } + response.writeHead(404); + response.end(); + }); + server.listen(0, "127.0.0.1", () => resume(Effect.succeed(server))); + }), + (server) => + Effect.gen(function* () { + const address = server.address(); + if (address === null || typeof address === "string") { + return Effect.die(new Error("Expected a TCP address")); + } + const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-pair-big-test-")); + const statePath = NodePath.join(baseDir, "userdata", "server-runtime.json"); + yield* persistServerRuntimeState({ + path: statePath, + state: yield* makePersistedServerRuntimeState({ + config: { host: "127.0.0.1", devUrl: undefined }, + port: address.port, + }), + }); + + const error = yield* provideCliTestLayers( + runCli(["pair", "--base-dir", baseDir]).pipe(Effect.flip), + ); + + const rendered = String( + typeof error === "object" && error !== null && "cause" in error ? error.cause : error, + ); + assert.include(rendered, "No running T3 Code server found."); + }), + (server) => Effect.sync(() => server.close()), + ).pipe(Effect.provide(NodeServices.layer)), + ); }); diff --git a/apps/server/src/cli/pair.ts b/apps/server/src/cli/pair.ts index 493e6b719416..d67857d17a86 100644 --- a/apps/server/src/cli/pair.ts +++ b/apps/server/src/cli/pair.ts @@ -59,6 +59,9 @@ import { baseDirFlag, DurationFromString } from "./config.ts"; const WELL_KNOWN_ENVIRONMENT_PATH = "/.well-known/t3/environment"; const PAIR_PROBE_TIMEOUT = Duration.millis(2_500); +// The environment descriptor is a few hundred bytes; anything larger served +// from the well-known path cannot be a T3 server. +const MAX_PROBE_BODY_BYTES = 64 * 1024; // Tailscale provisions an HTTPS certificate on the first request to a fresh // serve mapping, which can take a few seconds. const TAILSCALE_PROBE_ATTEMPTS = 5; @@ -218,14 +221,29 @@ const probeEnvironmentDescriptor = ( // Bad-gateway family means a proxy (Tailscale Serve) answered for a // backend that is gone — a stale mapping, not a live occupant. Treating // it as unreachable lets `t3 pair --tailscale` repair its own mapping - // after the server's port changed. + // after the server's port changed. Drain the body so the pooled + // connection is reusable for the re-configured mapping. if (response.status === 502 || response.status === 503 || response.status === 504) { + yield* Effect.ignore(response.text); return { _tag: "unreachable" } as const; } // Anything else that answered HTTP but not with a valid descriptor is - // some other service. + // some other service. Refuse to buffer + decode a stranger's body blind: + // the descriptor is a few hundred bytes of JSON, so non-JSON content or + // anything over the cap is classified without a Schema decode. + const contentType = response.headers["content-type"] ?? ""; + if (!contentType.includes("json")) { + return { _tag: "not-a-t3-server" } as const; + } const descriptor = yield* HttpClientResponse.filterStatusOk(response).pipe( - Effect.flatMap(HttpClientResponse.schemaBodyJson(ExecutionEnvironmentDescriptor)), + Effect.flatMap((ok) => ok.text), + Effect.flatMap((body) => + body.length > MAX_PROBE_BODY_BYTES + ? Effect.fail({ _tag: "not-a-t3-server" } as const) + : Schema.decodeUnknownEffect(Schema.fromJsonString(ExecutionEnvironmentDescriptor))( + body, + ).pipe(Effect.mapError(() => ({ _tag: "not-a-t3-server" }) as const)), + ), Effect.mapError(() => ({ _tag: "not-a-t3-server" }) as const), ); return { _tag: "descriptor", descriptor } as const; From 1af5d21105da67440e38faa10aa270753f1f54e2 Mon Sep 17 00:00:00 2001 From: Adamulek123 Date: Wed, 16 Sep 2026 07:40:50 +0200 Subject: [PATCH 2/9] test(server): pin the pair probe body cap with a schema-valid oversized body --- apps/server/src/cli/pair.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/server/src/cli/pair.test.ts b/apps/server/src/cli/pair.test.ts index ae49bda03020..446d95dc36ca 100644 --- a/apps/server/src/cli/pair.test.ts +++ b/apps/server/src/cli/pair.test.ts @@ -295,9 +295,10 @@ describe("t3 pair", () => { const server = NodeHttp.createServer((request, response) => { if (request.url === "/.well-known/t3/environment") { response.writeHead(200, { "content-type": "application/json" }); - // Valid JSON shape, far beyond any real descriptor: discovery must - // classify the occupant without a Schema decode, not pair with it. - response.end(JSON.stringify({ padding: "x".repeat(128 * 1024) })); + // A schema-valid descriptor padded far beyond any real one: + // discovery must reject it on size without a Schema decode + // (without the cap this decodes fine and pairs), not pair with it. + response.end(JSON.stringify({ ...testDescriptor, padding: "x".repeat(128 * 1024) })); return; } response.writeHead(404); From 116ae825d03f669882d89de8e4f075fd474284ef Mon Sep 17 00:00:00 2001 From: Adamulek123 Date: Fri, 18 Sep 2026 23:47:59 +0200 Subject: [PATCH 3/9] fix(server): bound pair probe body reads in bytes with read timeout Stream the descriptor and 502/503/504 drain through a byte-counted read capped at MAX_PROBE_BODY_BYTES with PAIR_PROBE_TIMEOUT, before decoding to text. Normalize the JSON content-type check. Covers uppercase content types, slow-drip bodies, and multibyte UTF-8 oversize. --- apps/server/src/cli/pair.test.ts | 145 +++++++++++++++++++++++++++++++ apps/server/src/cli/pair.ts | 52 ++++++++--- 2 files changed, 184 insertions(+), 13 deletions(-) diff --git a/apps/server/src/cli/pair.test.ts b/apps/server/src/cli/pair.test.ts index 446d95dc36ca..44f7ba3c6e47 100644 --- a/apps/server/src/cli/pair.test.ts +++ b/apps/server/src/cli/pair.test.ts @@ -334,4 +334,149 @@ describe("t3 pair", () => { (server) => Effect.sync(() => server.close()), ).pipe(Effect.provide(NodeServices.layer)), ); + + it.effect("rejects a descriptor that exceeds the cap in UTF-8 bytes only", () => + Effect.acquireUseRelease( + Effect.callback((resume) => { + const server = NodeHttp.createServer((request, response) => { + if (request.url === "/.well-known/t3/environment") { + response.writeHead(200, { "content-type": "application/json" }); + // "é" is two bytes in UTF-8 but one UTF-16 code unit: this body + // is ~80 KiB on the wire yet under 64 KiB in string length, so a + // string-length check would accept and pair with it. The probe + // must enforce the cap in bytes, before decoding. + response.end(JSON.stringify({ ...testDescriptor, label: "é".repeat(40 * 1024) })); + return; + } + response.writeHead(404); + response.end(); + }); + server.listen(0, "127.0.0.1", () => resume(Effect.succeed(server))); + }), + (server) => + Effect.gen(function* () { + const address = server.address(); + if (address === null || typeof address === "string") { + return Effect.die(new Error("Expected a TCP address")); + } + const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-pair-utf8-test-")); + const statePath = NodePath.join(baseDir, "userdata", "server-runtime.json"); + yield* persistServerRuntimeState({ + path: statePath, + state: yield* makePersistedServerRuntimeState({ + config: { host: "127.0.0.1", devUrl: undefined }, + port: address.port, + }), + }); + + const error = yield* provideCliTestLayers( + runCli(["pair", "--base-dir", baseDir]).pipe(Effect.flip), + ); + + const rendered = String( + typeof error === "object" && error !== null && "cause" in error ? error.cause : error, + ); + assert.include(rendered, "No running T3 Code server found."); + }), + (server) => Effect.sync(() => server.close()), + ).pipe(Effect.provide(NodeServices.layer)), + ); + + it.effect("pairs when the descriptor arrives with an uppercase JSON content type", () => + Effect.acquireUseRelease( + Effect.callback((resume) => { + const server = NodeHttp.createServer((request, response) => { + if (request.url === "/.well-known/t3/environment") { + response.writeHead(200, { "content-type": "Application/JSON; charset=utf-8" }); + response.end(JSON.stringify(testDescriptor)); + return; + } + response.writeHead(404); + response.end(); + }); + server.listen(0, "127.0.0.1", () => resume(Effect.succeed(server))); + }), + (server) => + Effect.gen(function* () { + const address = server.address(); + if (address === null || typeof address === "string") { + return Effect.die(new Error("Expected a TCP address")); + } + const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-pair-ctype-test-")); + const statePath = NodePath.join(baseDir, "userdata", "server-runtime.json"); + yield* persistServerRuntimeState({ + path: statePath, + state: yield* makePersistedServerRuntimeState({ + config: { host: "127.0.0.1", devUrl: undefined }, + port: address.port, + }), + }); + + const output = yield* captureStdout(runCli(["pair", "--base-dir", baseDir])); + + assert.include(output, "Pairing with pair-test ("); + assert.include(output, "/pair#token="); + }), + (server) => Effect.sync(() => server.close()), + ).pipe(Effect.provide(NodeServices.layer)), + ); + + it.live("times out a slow-drip stranger body instead of hanging discovery", () => + Effect.acquireUseRelease( + Effect.callback((resume) => { + const server = NodeHttp.createServer((request, response) => { + if (request.url === "/.well-known/t3/environment") { + response.writeHead(200, { "content-type": "application/json" }); + // A never-ending drip that stays under the size cap: discovery + // must give up via the body timeout rather than hang on the open + // stream. + response.write(`{"environmentId":`); + const timer = setInterval(() => { + if (response.destroyed) { + clearInterval(timer); + return; + } + response.write(" "); + }, 200); + timer.unref(); + response.on("close", () => clearInterval(timer)); + return; + } + response.writeHead(404); + response.end(); + }); + server.listen(0, "127.0.0.1", () => resume(Effect.succeed(server))); + }), + (server) => + Effect.gen(function* () { + const address = server.address(); + if (address === null || typeof address === "string") { + return Effect.die(new Error("Expected a TCP address")); + } + const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-pair-drip-test-")); + const statePath = NodePath.join(baseDir, "userdata", "server-runtime.json"); + yield* persistServerRuntimeState({ + path: statePath, + state: yield* makePersistedServerRuntimeState({ + config: { host: "127.0.0.1", devUrl: undefined }, + port: address.port, + }), + }); + + const error = yield* provideCliTestLayers( + runCli(["pair", "--base-dir", baseDir]).pipe(Effect.flip), + ); + + const rendered = String( + typeof error === "object" && error !== null && "cause" in error ? error.cause : error, + ); + assert.include(rendered, "No running T3 Code server found."); + }), + (server) => + Effect.sync(() => { + server.closeAllConnections(); + server.close(); + }), + ).pipe(Effect.provide(NodeServices.layer)), + ); }); diff --git a/apps/server/src/cli/pair.ts b/apps/server/src/cli/pair.ts index d67857d17a86..eacfb9c004ee 100644 --- a/apps/server/src/cli/pair.ts +++ b/apps/server/src/cli/pair.ts @@ -31,6 +31,7 @@ import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as References from "effect/References"; import * as Schema from "effect/Schema"; +import * as Stream from "effect/Stream"; import { Command, Flag, GlobalFlag } from "effect/unstable/cli"; import { FetchHttpClient, @@ -207,7 +208,32 @@ type EnvironmentProbeResult = | { readonly _tag: "unreachable" } | { readonly _tag: "not-a-t3-server" }; -const probeEnvironmentDescriptor = ( +const readBoundedProbeBody = (ok: HttpClientResponse.HttpClientResponse) => + ok.stream.pipe( + Stream.runFoldEffect( + () => ({ bytes: 0, chunks: [] as Array }), + (acc, chunk) => { + if (acc.bytes + chunk.byteLength > MAX_PROBE_BODY_BYTES) { + return Effect.fail({ _tag: "not-a-t3-server" } as const); + } + acc.bytes += chunk.byteLength; + acc.chunks.push(chunk); + return Effect.succeed(acc); + }, + ), + Effect.map(({ bytes, chunks }) => { + const merged = new Uint8Array(bytes); + let offset = 0; + for (const chunk of chunks) { + merged.set(chunk, offset); + offset += chunk.byteLength; + } + return new TextDecoder().decode(merged); + }), + Effect.timeout(PAIR_PROBE_TIMEOUT), + ); + +export const probeEnvironmentDescriptor = ( baseUrl: string, ): Effect.Effect => Effect.gen(function* () { @@ -222,27 +248,27 @@ const probeEnvironmentDescriptor = ( // backend that is gone — a stale mapping, not a live occupant. Treating // it as unreachable lets `t3 pair --tailscale` repair its own mapping // after the server's port changed. Drain the body so the pooled - // connection is reusable for the re-configured mapping. + // connection is reusable for the re-configured mapping; the drain itself + // is time-bounded and best-effort. if (response.status === 502 || response.status === 503 || response.status === 504) { - yield* Effect.ignore(response.text); + yield* Effect.ignore(readBoundedProbeBody(response)); return { _tag: "unreachable" } as const; } // Anything else that answered HTTP but not with a valid descriptor is - // some other service. Refuse to buffer + decode a stranger's body blind: - // the descriptor is a few hundred bytes of JSON, so non-JSON content or - // anything over the cap is classified without a Schema decode. + // some other service. Refuse to decode a stranger's body blind: the + // descriptor is a few hundred bytes of JSON, so non-JSON content is + // classified without reading the body at all, and JSON bodies are read + // through the bounded stream above — never buffered-then-checked. const contentType = response.headers["content-type"] ?? ""; - if (!contentType.includes("json")) { + if (!contentType.toLowerCase().includes("json")) { return { _tag: "not-a-t3-server" } as const; } const descriptor = yield* HttpClientResponse.filterStatusOk(response).pipe( - Effect.flatMap((ok) => ok.text), + Effect.flatMap(readBoundedProbeBody), Effect.flatMap((body) => - body.length > MAX_PROBE_BODY_BYTES - ? Effect.fail({ _tag: "not-a-t3-server" } as const) - : Schema.decodeUnknownEffect(Schema.fromJsonString(ExecutionEnvironmentDescriptor))( - body, - ).pipe(Effect.mapError(() => ({ _tag: "not-a-t3-server" }) as const)), + Schema.decodeUnknownEffect(Schema.fromJsonString(ExecutionEnvironmentDescriptor))( + body, + ).pipe(Effect.mapError(() => ({ _tag: "not-a-t3-server" }) as const)), ), Effect.mapError(() => ({ _tag: "not-a-t3-server" }) as const), ); From 14e56e15f10a7b3ab1e755dac9c7ecac188d329f Mon Sep 17 00:00:00 2001 From: Adamulek123 Date: Sat, 19 Sep 2026 00:02:31 +0200 Subject: [PATCH 4/9] fix(server): keep pair probe helper private for knip --- apps/server/src/cli/pair.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/cli/pair.ts b/apps/server/src/cli/pair.ts index eacfb9c004ee..c98fb6a0d1ad 100644 --- a/apps/server/src/cli/pair.ts +++ b/apps/server/src/cli/pair.ts @@ -233,7 +233,7 @@ const readBoundedProbeBody = (ok: HttpClientResponse.HttpClientResponse) => Effect.timeout(PAIR_PROBE_TIMEOUT), ); -export const probeEnvironmentDescriptor = ( +const probeEnvironmentDescriptor = ( baseUrl: string, ): Effect.Effect => Effect.gen(function* () { From 0c77bd0034ba0ea4dbeb49dd596d018e190771b2 Mon Sep 17 00:00:00 2001 From: Adamulek123 Date: Sat, 19 Sep 2026 00:14:36 +0200 Subject: [PATCH 5/9] fix(server): use Effect timer for pair slow-drip test and strict JSON media-type check --- apps/server/src/cli/pair.test.ts | 22 +++++++++++++--------- apps/server/src/cli/pair.ts | 3 ++- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/apps/server/src/cli/pair.test.ts b/apps/server/src/cli/pair.test.ts index 44f7ba3c6e47..59f650f74d14 100644 --- a/apps/server/src/cli/pair.test.ts +++ b/apps/server/src/cli/pair.test.ts @@ -9,7 +9,9 @@ import * as NetService from "@t3tools/shared/Net"; import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; import { assert, describe, expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; +import * as Fiber from "effect/Fiber"; import * as Layer from "effect/Layer"; +import * as Schedule from "effect/Schedule"; import * as TestConsole from "effect/testing/TestConsole"; import { Command } from "effect/unstable/cli"; @@ -431,15 +433,17 @@ describe("t3 pair", () => { // must give up via the body timeout rather than hang on the open // stream. response.write(`{"environmentId":`); - const timer = setInterval(() => { - if (response.destroyed) { - clearInterval(timer); - return; - } - response.write(" "); - }, 200); - timer.unref(); - response.on("close", () => clearInterval(timer)); + const drip = Effect.runFork( + Effect.repeat( + Effect.sync(() => { + if (!response.destroyed) { + response.write(" "); + } + }), + Schedule.spaced("200 millis"), + ), + ); + response.on("close", () => Effect.runFork(Fiber.interrupt(drip))); return; } response.writeHead(404); diff --git a/apps/server/src/cli/pair.ts b/apps/server/src/cli/pair.ts index c98fb6a0d1ad..e85726af71d3 100644 --- a/apps/server/src/cli/pair.ts +++ b/apps/server/src/cli/pair.ts @@ -260,7 +260,8 @@ const probeEnvironmentDescriptor = ( // classified without reading the body at all, and JSON bodies are read // through the bounded stream above — never buffered-then-checked. const contentType = response.headers["content-type"] ?? ""; - if (!contentType.toLowerCase().includes("json")) { + const mediaType = contentType.split(";", 1)[0]?.trim().toLowerCase(); + if (mediaType !== "application/json" && !mediaType?.endsWith("+json")) { return { _tag: "not-a-t3-server" } as const; } const descriptor = yield* HttpClientResponse.filterStatusOk(response).pipe( From 12a357a0f63159cfe77de32ecd76b6522cf81b43 Mon Sep 17 00:00:00 2001 From: Adamulek123 Date: Sat, 19 Sep 2026 00:42:11 +0200 Subject: [PATCH 6/9] fix(server): fork pair drip test in-effect and hoist probe schema compile --- apps/server/src/cli/pair.test.ts | 91 ++++++++++++++++++-------------- apps/server/src/cli/pair.ts | 11 ++-- 2 files changed, 60 insertions(+), 42 deletions(-) diff --git a/apps/server/src/cli/pair.test.ts b/apps/server/src/cli/pair.test.ts index 59f650f74d14..b621c8e9c298 100644 --- a/apps/server/src/cli/pair.test.ts +++ b/apps/server/src/cli/pair.test.ts @@ -9,6 +9,7 @@ import * as NetService from "@t3tools/shared/Net"; import { HostProcessEnvironment } from "@t3tools/shared/hostProcess"; import { assert, describe, expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; +import * as Deferred from "effect/Deferred"; import * as Fiber from "effect/Fiber"; import * as Layer from "effect/Layer"; import * as Schedule from "effect/Schedule"; @@ -423,8 +424,13 @@ describe("t3 pair", () => { ).pipe(Effect.provide(NodeServices.layer)), ); - it.live("times out a slow-drip stranger body instead of hanging discovery", () => - Effect.acquireUseRelease( + it.live("times out a slow-drip stranger body instead of hanging discovery", () => { + // Handoff from the raw Node request handler below: it holds the response + // open so the test fiber can drip into it. Completed synchronously from + // the callback via `doneUnsafe`, so no manual Effect runtime is created + // in the test (see `t3code(no-manual-effect-runtime-in-tests)`). + const dripTarget = Deferred.makeUnsafe(); + return Effect.acquireUseRelease( Effect.callback((resume) => { const server = NodeHttp.createServer((request, response) => { if (request.url === "/.well-known/t3/environment") { @@ -433,17 +439,7 @@ describe("t3 pair", () => { // must give up via the body timeout rather than hang on the open // stream. response.write(`{"environmentId":`); - const drip = Effect.runFork( - Effect.repeat( - Effect.sync(() => { - if (!response.destroyed) { - response.write(" "); - } - }), - Schedule.spaced("200 millis"), - ), - ); - response.on("close", () => Effect.runFork(Fiber.interrupt(drip))); + Deferred.doneUnsafe(dripTarget, Effect.succeed(response)); return; } response.writeHead(404); @@ -452,35 +448,52 @@ describe("t3 pair", () => { server.listen(0, "127.0.0.1", () => resume(Effect.succeed(server))); }), (server) => - Effect.gen(function* () { - const address = server.address(); - if (address === null || typeof address === "string") { - return Effect.die(new Error("Expected a TCP address")); - } - const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-pair-drip-test-")); - const statePath = NodePath.join(baseDir, "userdata", "server-runtime.json"); - yield* persistServerRuntimeState({ - path: statePath, - state: yield* makePersistedServerRuntimeState({ - config: { host: "127.0.0.1", devUrl: undefined }, - port: address.port, - }), - }); - - const error = yield* provideCliTestLayers( - runCli(["pair", "--base-dir", baseDir]).pipe(Effect.flip), - ); - - const rendered = String( - typeof error === "object" && error !== null && "cause" in error ? error.cause : error, - ); - assert.include(rendered, "No running T3 Code server found."); - }), + Effect.scoped( + Effect.gen(function* () { + const address = server.address(); + if (address === null || typeof address === "string") { + return Effect.die(new Error("Expected a TCP address")); + } + const baseDir = NodeFS.mkdtempSync( + NodePath.join(NodeOS.tmpdir(), "t3-pair-drip-test-"), + ); + const statePath = NodePath.join(baseDir, "userdata", "server-runtime.json"); + yield* persistServerRuntimeState({ + path: statePath, + state: yield* makePersistedServerRuntimeState({ + config: { host: "127.0.0.1", devUrl: undefined }, + port: address.port, + }), + }); + + const cliFiber = yield* provideCliTestLayers( + runCli(["pair", "--base-dir", baseDir]).pipe(Effect.flip), + ).pipe(Effect.forkChild); + // The probe request holds the response open; the drip loop is a + // scoped fork, so it is interrupted when the test settles. + const dripResponse = yield* Deferred.await(dripTarget); + yield* Effect.repeat( + Effect.sync(() => { + if (!dripResponse.destroyed) { + dripResponse.write(" "); + } + }), + Schedule.spaced("200 millis"), + ).pipe(Effect.forkScoped); + + const error = yield* Fiber.join(cliFiber); + + const rendered = String( + typeof error === "object" && error !== null && "cause" in error ? error.cause : error, + ); + assert.include(rendered, "No running T3 Code server found."); + }), + ), (server) => Effect.sync(() => { server.closeAllConnections(); server.close(); }), - ).pipe(Effect.provide(NodeServices.layer)), - ); + ).pipe(Effect.provide(NodeServices.layer)); + }); }); diff --git a/apps/server/src/cli/pair.ts b/apps/server/src/cli/pair.ts index e85726af71d3..fd18a99739b0 100644 --- a/apps/server/src/cli/pair.ts +++ b/apps/server/src/cli/pair.ts @@ -151,6 +151,11 @@ export class DevServerNotProxiableError extends Schema.TaggedError - Schema.decodeUnknownEffect(Schema.fromJsonString(ExecutionEnvironmentDescriptor))( - body, - ).pipe(Effect.mapError(() => ({ _tag: "not-a-t3-server" }) as const)), + decodeProbeDescriptor(body).pipe( + Effect.mapError(() => ({ _tag: "not-a-t3-server" }) as const), + ), ), Effect.mapError(() => ({ _tag: "not-a-t3-server" }) as const), ); From 978b1337c41579518eb096854e6eacd9763fc365 Mon Sep 17 00:00:00 2001 From: Adamulek123 Date: Sat, 19 Sep 2026 10:39:15 +0200 Subject: [PATCH 7/9] fix(server): drain non-JSON probe bodies for connection reuse --- apps/server/src/cli/pair.test.ts | 46 ++++++++++++++++++++++++++++++++ apps/server/src/cli/pair.ts | 6 +++-- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/apps/server/src/cli/pair.test.ts b/apps/server/src/cli/pair.test.ts index b621c8e9c298..8704a3de8eb3 100644 --- a/apps/server/src/cli/pair.test.ts +++ b/apps/server/src/cli/pair.test.ts @@ -385,6 +385,52 @@ describe("t3 pair", () => { ).pipe(Effect.provide(NodeServices.layer)), ); + it.effect("does not pair when a valid descriptor arrives as non-JSON", () => + Effect.acquireUseRelease( + Effect.callback((resume) => { + const server = NodeHttp.createServer((request, response) => { + if (request.url === "/.well-known/t3/environment") { + // Schema-valid JSON served as HTML: the probe must classify it + // as a stranger (and drain the body for connection reuse) + // instead of decoding and pairing with it. + response.writeHead(200, { "content-type": "text/html; charset=utf-8" }); + response.end(JSON.stringify(testDescriptor)); + return; + } + response.writeHead(404); + response.end(); + }); + server.listen(0, "127.0.0.1", () => resume(Effect.succeed(server))); + }), + (server) => + Effect.gen(function* () { + const address = server.address(); + if (address === null || typeof address === "string") { + return Effect.die(new Error("Expected a TCP address")); + } + const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-pair-ctype-test-")); + const statePath = NodePath.join(baseDir, "userdata", "server-runtime.json"); + yield* persistServerRuntimeState({ + path: statePath, + state: yield* makePersistedServerRuntimeState({ + config: { host: "127.0.0.1", devUrl: undefined }, + port: address.port, + }), + }); + + const error = yield* provideCliTestLayers( + runCli(["pair", "--base-dir", baseDir]).pipe(Effect.flip), + ); + + const rendered = String( + typeof error === "object" && error !== null && "cause" in error ? error.cause : error, + ); + assert.include(rendered, "No running T3 Code server found."); + }), + (server) => Effect.sync(() => server.close()), + ).pipe(Effect.provide(NodeServices.layer)), + ); + it.effect("pairs when the descriptor arrives with an uppercase JSON content type", () => Effect.acquireUseRelease( Effect.callback((resume) => { diff --git a/apps/server/src/cli/pair.ts b/apps/server/src/cli/pair.ts index fd18a99739b0..d75556c12853 100644 --- a/apps/server/src/cli/pair.ts +++ b/apps/server/src/cli/pair.ts @@ -262,11 +262,13 @@ const probeEnvironmentDescriptor = ( // Anything else that answered HTTP but not with a valid descriptor is // some other service. Refuse to decode a stranger's body blind: the // descriptor is a few hundred bytes of JSON, so non-JSON content is - // classified without reading the body at all, and JSON bodies are read - // through the bounded stream above — never buffered-then-checked. + // classified without decoding, but the body is still drained boundedly + // so the pooled connection is reusable — a stranger that holds its body + // open must not pin pool slots across repeated probes. const contentType = response.headers["content-type"] ?? ""; const mediaType = contentType.split(";", 1)[0]?.trim().toLowerCase(); if (mediaType !== "application/json" && !mediaType?.endsWith("+json")) { + yield* Effect.ignore(readBoundedProbeBody(response)); return { _tag: "not-a-t3-server" } as const; } const descriptor = yield* HttpClientResponse.filterStatusOk(response).pipe( From 797c70b816cb458e6f5fe8b5493d3f40a408aaa1 Mon Sep 17 00:00:00 2001 From: Adamulek123 Date: Sat, 19 Sep 2026 11:07:26 +0200 Subject: [PATCH 8/9] fix(server): drain non-2xx probe bodies for connection reuse --- apps/server/src/cli/pair.test.ts | 48 ++++++++++++++++++++++++++++++++ apps/server/src/cli/pair.ts | 10 +++++-- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/apps/server/src/cli/pair.test.ts b/apps/server/src/cli/pair.test.ts index 8704a3de8eb3..e18d239c1c44 100644 --- a/apps/server/src/cli/pair.test.ts +++ b/apps/server/src/cli/pair.test.ts @@ -431,6 +431,54 @@ describe("t3 pair", () => { ).pipe(Effect.provide(NodeServices.layer)), ); + it.effect("does not pair when a valid descriptor arrives with an error status", () => + Effect.acquireUseRelease( + Effect.callback((resume) => { + const server = NodeHttp.createServer((request, response) => { + if (request.url === "/.well-known/t3/environment") { + // A 500 carrying a schema-valid descriptor body: the probe must + // classify it as a stranger (draining the body for connection + // reuse) instead of decoding and pairing with it. + response.writeHead(500, { "content-type": "application/json" }); + response.end(JSON.stringify(testDescriptor)); + return; + } + response.writeHead(404); + response.end(); + }); + server.listen(0, "127.0.0.1", () => resume(Effect.succeed(server))); + }), + (server) => + Effect.gen(function* () { + const address = server.address(); + if (address === null || typeof address === "string") { + return Effect.die(new Error("Expected a TCP address")); + } + const baseDir = NodeFS.mkdtempSync( + NodePath.join(NodeOS.tmpdir(), "t3-pair-status-test-"), + ); + const statePath = NodePath.join(baseDir, "userdata", "server-runtime.json"); + yield* persistServerRuntimeState({ + path: statePath, + state: yield* makePersistedServerRuntimeState({ + config: { host: "127.0.0.1", devUrl: undefined }, + port: address.port, + }), + }); + + const error = yield* provideCliTestLayers( + runCli(["pair", "--base-dir", baseDir]).pipe(Effect.flip), + ); + + const rendered = String( + typeof error === "object" && error !== null && "cause" in error ? error.cause : error, + ); + assert.include(rendered, "No running T3 Code server found."); + }), + (server) => Effect.sync(() => server.close()), + ).pipe(Effect.provide(NodeServices.layer)), + ); + it.effect("pairs when the descriptor arrives with an uppercase JSON content type", () => Effect.acquireUseRelease( Effect.callback((resume) => { diff --git a/apps/server/src/cli/pair.ts b/apps/server/src/cli/pair.ts index d75556c12853..6119575d9752 100644 --- a/apps/server/src/cli/pair.ts +++ b/apps/server/src/cli/pair.ts @@ -271,8 +271,14 @@ const probeEnvironmentDescriptor = ( yield* Effect.ignore(readBoundedProbeBody(response)); return { _tag: "not-a-t3-server" } as const; } - const descriptor = yield* HttpClientResponse.filterStatusOk(response).pipe( - Effect.flatMap(readBoundedProbeBody), + // A non-2xx answer is still a stranger, but its body must be drained + // boundedly for the same reason: an unconsumed stream pins the pooled + // connection across repeated probes. + if (response.status < 200 || response.status >= 300) { + yield* Effect.ignore(readBoundedProbeBody(response)); + return { _tag: "not-a-t3-server" } as const; + } + const descriptor = yield* readBoundedProbeBody(response).pipe( Effect.flatMap((body) => decodeProbeDescriptor(body).pipe( Effect.mapError(() => ({ _tag: "not-a-t3-server" }) as const), From feec3eb3652298a10272ab729775c87e2c73c9da Mon Sep 17 00:00:00 2001 From: Adamulek123 Date: Thu, 1 Oct 2026 15:26:15 +0200 Subject: [PATCH 9/9] test(server): share pair probe fixtures and yield defects --- apps/server/src/cli/pair.test.ts | 292 ++++++++++--------------------- 1 file changed, 90 insertions(+), 202 deletions(-) diff --git a/apps/server/src/cli/pair.test.ts b/apps/server/src/cli/pair.test.ts index e18d239c1c44..b9248cbcf3f7 100644 --- a/apps/server/src/cli/pair.test.ts +++ b/apps/server/src/cli/pair.test.ts @@ -11,6 +11,7 @@ import { assert, describe, expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as Deferred from "effect/Deferred"; import * as Fiber from "effect/Fiber"; +import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Schedule from "effect/Schedule"; import * as TestConsole from "effect/testing/TestConsole"; @@ -122,6 +123,60 @@ const testDescriptor = { capabilities: { repositoryIdentity: true }, }; +const withProbeServer = ( + { status, contentType, body }: { status: number; contentType: string; body: string }, + run: (baseDir: string, origin: string) => Effect.Effect, +) => + Effect.scoped( + Effect.acquireUseRelease( + Effect.callback((resume) => { + const server = NodeHttp.createServer((request, response) => { + if (request.url === "/.well-known/t3/environment") { + response.writeHead(status, { "content-type": contentType }); + response.end(body); + return; + } + response.writeHead(404); + response.end(); + }); + server.listen(0, "127.0.0.1", () => resume(Effect.succeed(server))); + }), + (server) => + Effect.gen(function* () { + const address = server.address(); + if (address === null || typeof address === "string") { + return yield* Effect.die(new Error("Expected a TCP address")); + } + const fs = yield* FileSystem.FileSystem; + const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-pair-probe-test-" }); + yield* persistServerRuntimeState({ + path: NodePath.join(baseDir, "userdata", "server-runtime.json"), + state: yield* makePersistedServerRuntimeState({ + config: { host: "127.0.0.1", devUrl: undefined }, + port: address.port, + }), + }); + return yield* run(baseDir, `http://127.0.0.1:${String(address.port)}`); + }), + (server) => + Effect.sync(() => { + server.closeAllConnections(); + server.close(); + }), + ), + ); + +const assertPairRejected = (baseDir: string) => + Effect.gen(function* () { + const error = yield* provideCliTestLayers( + runCli(["pair", "--base-dir", baseDir]).pipe(Effect.flip), + ); + const rendered = String( + typeof error === "object" && error !== null && "cause" in error ? error.cause : error, + ); + assert.include(rendered, "No running T3 Code server found."); + }); + const withDescriptorServer = (run: (origin: string) => Effect.Effect) => Effect.acquireUseRelease( Effect.callback((resume) => { @@ -293,228 +348,61 @@ describe("t3 pair", () => { ); it.effect("does not decode an oversized stranger body as a server descriptor", () => - Effect.acquireUseRelease( - Effect.callback((resume) => { - const server = NodeHttp.createServer((request, response) => { - if (request.url === "/.well-known/t3/environment") { - response.writeHead(200, { "content-type": "application/json" }); - // A schema-valid descriptor padded far beyond any real one: - // discovery must reject it on size without a Schema decode - // (without the cap this decodes fine and pairs), not pair with it. - response.end(JSON.stringify({ ...testDescriptor, padding: "x".repeat(128 * 1024) })); - return; - } - response.writeHead(404); - response.end(); - }); - server.listen(0, "127.0.0.1", () => resume(Effect.succeed(server))); - }), - (server) => - Effect.gen(function* () { - const address = server.address(); - if (address === null || typeof address === "string") { - return Effect.die(new Error("Expected a TCP address")); - } - const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-pair-big-test-")); - const statePath = NodePath.join(baseDir, "userdata", "server-runtime.json"); - yield* persistServerRuntimeState({ - path: statePath, - state: yield* makePersistedServerRuntimeState({ - config: { host: "127.0.0.1", devUrl: undefined }, - port: address.port, - }), - }); - - const error = yield* provideCliTestLayers( - runCli(["pair", "--base-dir", baseDir]).pipe(Effect.flip), - ); - - const rendered = String( - typeof error === "object" && error !== null && "cause" in error ? error.cause : error, - ); - assert.include(rendered, "No running T3 Code server found."); - }), - (server) => Effect.sync(() => server.close()), + withProbeServer( + { + status: 200, + contentType: "application/json", + // Schema-valid JSON that would pair without the byte cap. + body: JSON.stringify({ ...testDescriptor, padding: "x".repeat(128 * 1024) }), + }, + assertPairRejected, ).pipe(Effect.provide(NodeServices.layer)), ); it.effect("rejects a descriptor that exceeds the cap in UTF-8 bytes only", () => - Effect.acquireUseRelease( - Effect.callback((resume) => { - const server = NodeHttp.createServer((request, response) => { - if (request.url === "/.well-known/t3/environment") { - response.writeHead(200, { "content-type": "application/json" }); - // "é" is two bytes in UTF-8 but one UTF-16 code unit: this body - // is ~80 KiB on the wire yet under 64 KiB in string length, so a - // string-length check would accept and pair with it. The probe - // must enforce the cap in bytes, before decoding. - response.end(JSON.stringify({ ...testDescriptor, label: "é".repeat(40 * 1024) })); - return; - } - response.writeHead(404); - response.end(); - }); - server.listen(0, "127.0.0.1", () => resume(Effect.succeed(server))); - }), - (server) => - Effect.gen(function* () { - const address = server.address(); - if (address === null || typeof address === "string") { - return Effect.die(new Error("Expected a TCP address")); - } - const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-pair-utf8-test-")); - const statePath = NodePath.join(baseDir, "userdata", "server-runtime.json"); - yield* persistServerRuntimeState({ - path: statePath, - state: yield* makePersistedServerRuntimeState({ - config: { host: "127.0.0.1", devUrl: undefined }, - port: address.port, - }), - }); - - const error = yield* provideCliTestLayers( - runCli(["pair", "--base-dir", baseDir]).pipe(Effect.flip), - ); - - const rendered = String( - typeof error === "object" && error !== null && "cause" in error ? error.cause : error, - ); - assert.include(rendered, "No running T3 Code server found."); - }), - (server) => Effect.sync(() => server.close()), + withProbeServer( + { + status: 200, + contentType: "application/json", + // Two UTF-8 bytes per character: below 64 KiB in string length, + // above it on the wire. A string-length cap would accept this. + body: JSON.stringify({ ...testDescriptor, label: "é".repeat(40 * 1024) }), + }, + assertPairRejected, ).pipe(Effect.provide(NodeServices.layer)), ); it.effect("does not pair when a valid descriptor arrives as non-JSON", () => - Effect.acquireUseRelease( - Effect.callback((resume) => { - const server = NodeHttp.createServer((request, response) => { - if (request.url === "/.well-known/t3/environment") { - // Schema-valid JSON served as HTML: the probe must classify it - // as a stranger (and drain the body for connection reuse) - // instead of decoding and pairing with it. - response.writeHead(200, { "content-type": "text/html; charset=utf-8" }); - response.end(JSON.stringify(testDescriptor)); - return; - } - response.writeHead(404); - response.end(); - }); - server.listen(0, "127.0.0.1", () => resume(Effect.succeed(server))); - }), - (server) => - Effect.gen(function* () { - const address = server.address(); - if (address === null || typeof address === "string") { - return Effect.die(new Error("Expected a TCP address")); - } - const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-pair-ctype-test-")); - const statePath = NodePath.join(baseDir, "userdata", "server-runtime.json"); - yield* persistServerRuntimeState({ - path: statePath, - state: yield* makePersistedServerRuntimeState({ - config: { host: "127.0.0.1", devUrl: undefined }, - port: address.port, - }), - }); - - const error = yield* provideCliTestLayers( - runCli(["pair", "--base-dir", baseDir]).pipe(Effect.flip), - ); - - const rendered = String( - typeof error === "object" && error !== null && "cause" in error ? error.cause : error, - ); - assert.include(rendered, "No running T3 Code server found."); - }), - (server) => Effect.sync(() => server.close()), + withProbeServer( + { + status: 200, + contentType: "text/html; charset=utf-8", + body: JSON.stringify(testDescriptor), + }, + assertPairRejected, ).pipe(Effect.provide(NodeServices.layer)), ); it.effect("does not pair when a valid descriptor arrives with an error status", () => - Effect.acquireUseRelease( - Effect.callback((resume) => { - const server = NodeHttp.createServer((request, response) => { - if (request.url === "/.well-known/t3/environment") { - // A 500 carrying a schema-valid descriptor body: the probe must - // classify it as a stranger (draining the body for connection - // reuse) instead of decoding and pairing with it. - response.writeHead(500, { "content-type": "application/json" }); - response.end(JSON.stringify(testDescriptor)); - return; - } - response.writeHead(404); - response.end(); - }); - server.listen(0, "127.0.0.1", () => resume(Effect.succeed(server))); - }), - (server) => - Effect.gen(function* () { - const address = server.address(); - if (address === null || typeof address === "string") { - return Effect.die(new Error("Expected a TCP address")); - } - const baseDir = NodeFS.mkdtempSync( - NodePath.join(NodeOS.tmpdir(), "t3-pair-status-test-"), - ); - const statePath = NodePath.join(baseDir, "userdata", "server-runtime.json"); - yield* persistServerRuntimeState({ - path: statePath, - state: yield* makePersistedServerRuntimeState({ - config: { host: "127.0.0.1", devUrl: undefined }, - port: address.port, - }), - }); - - const error = yield* provideCliTestLayers( - runCli(["pair", "--base-dir", baseDir]).pipe(Effect.flip), - ); - - const rendered = String( - typeof error === "object" && error !== null && "cause" in error ? error.cause : error, - ); - assert.include(rendered, "No running T3 Code server found."); - }), - (server) => Effect.sync(() => server.close()), + withProbeServer( + { status: 500, contentType: "application/json", body: JSON.stringify(testDescriptor) }, + assertPairRejected, ).pipe(Effect.provide(NodeServices.layer)), ); it.effect("pairs when the descriptor arrives with an uppercase JSON content type", () => - Effect.acquireUseRelease( - Effect.callback((resume) => { - const server = NodeHttp.createServer((request, response) => { - if (request.url === "/.well-known/t3/environment") { - response.writeHead(200, { "content-type": "Application/JSON; charset=utf-8" }); - response.end(JSON.stringify(testDescriptor)); - return; - } - response.writeHead(404); - response.end(); - }); - server.listen(0, "127.0.0.1", () => resume(Effect.succeed(server))); - }), - (server) => + withProbeServer( + { + status: 200, + contentType: "Application/JSON; charset=utf-8", + body: JSON.stringify(testDescriptor), + }, + (baseDir) => Effect.gen(function* () { - const address = server.address(); - if (address === null || typeof address === "string") { - return Effect.die(new Error("Expected a TCP address")); - } - const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-pair-ctype-test-")); - const statePath = NodePath.join(baseDir, "userdata", "server-runtime.json"); - yield* persistServerRuntimeState({ - path: statePath, - state: yield* makePersistedServerRuntimeState({ - config: { host: "127.0.0.1", devUrl: undefined }, - port: address.port, - }), - }); - const output = yield* captureStdout(runCli(["pair", "--base-dir", baseDir])); - assert.include(output, "Pairing with pair-test ("); assert.include(output, "/pair#token="); }), - (server) => Effect.sync(() => server.close()), ).pipe(Effect.provide(NodeServices.layer)), ); @@ -546,7 +434,7 @@ describe("t3 pair", () => { Effect.gen(function* () { const address = server.address(); if (address === null || typeof address === "string") { - return Effect.die(new Error("Expected a TCP address")); + return yield* Effect.die(new Error("Expected a TCP address")); } const baseDir = NodeFS.mkdtempSync( NodePath.join(NodeOS.tmpdir(), "t3-pair-drip-test-"),