authorgravatar for 91240327+shivamhwp@users.noreply.github.comshivam <91240327+shivamhwp@users.noreply.github.com> 2026-10-13 08:12:08+05:30
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-13 08:12:08+05:30
loga143fa3c56be890346b8a7915a6a870529201dda
treed503bc2686e23a1b1e8f4656549c69a659ea06d3
parentdf26ef99859ac757409f5332b185a89cc7f40267
signature Signed by PGP key B5690EEEBB952194

fix(client): T3 Connect clients reconnect once a new tunnel is ready (#16554)

Co-authored-by: PR Batch Tester <agent@local.test> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

3 files changed, 50 insertions(+), 0 deletions(-)

packages/client-runtime/src/connection/errors.test.ts+33
......@@ -1,6 +1,7 @@
11import { EnvironmentAuthInvalidError } from "@t3tools/contracts";
22import {
33 RelayAuthInvalidError,
4 RelayEnvironmentConnectNotAuthorizedError,
45 RelayEnvironmentEndpointTimedOutError,
56} from "@t3tools/contracts/relay";
67import { describe, expect, it } from "@effect/vitest";
......@@ -35,6 +36,38 @@ describe("mapManagedRelayError", () => {
3536 });
3637 });
3738
39 it("retries a tunnel that is still being set up, and blocks other refusals", () => {
40 const mapConnectRefusal = (reason: RelayEnvironmentConnectNotAuthorizedError["reason"]) => {
41 const relayError = new RelayEnvironmentConnectNotAuthorizedError({
42 code: "environment_connect_not_authorized",
43 reason,
44 traceId: "trace-connect-refused",
45 });
46 return mapManagedRelayError(
47 new ManagedRelayRequestFailedError({
48 action: "connect relay environment",
49 cause: relayError,
50 relayError,
51 }),
52 );
53 };
54
55 expect(mapConnectRefusal("managed_endpoint_allocation_not_ready")).toMatchObject({
56 _tag: "ConnectionTransientError",
57 reason: "endpoint-unavailable",
58 detail: "The environment's T3 Connect tunnel is still being set up.",
59 traceId: "trace-connect-refused",
60 });
61 expect(mapConnectRefusal("endpoint_provider_not_managed")).toMatchObject({
62 _tag: "ConnectionBlockedError",
63 reason: "permission",
64 });
65 expect(mapConnectRefusal(undefined)).toMatchObject({
66 _tag: "ConnectionBlockedError",
67 reason: "permission",
68 });
69 });
70
3871 it("presents clock skew as one possible cause for a generic DPoP error", () => {
3972 const mapped = mapManagedRelayError(
4073 new ManagedRelayRequestFailedError({
packages/client-runtime/src/connection/errors.ts+14
......@@ -46,6 +46,20 @@ function relayProtectedError(error: RelayProtectedError): ConnectionAttemptError
4646 traceId: error.traceId,
4747 });
4848 case "RelayEnvironmentConnectNotAuthorizedError":
49 // A host swapping in a new tunnel leaves its allocation unready until
50 // the tunnel is up, and nothing on the client changes when it is.
51 if (error.reason === "managed_endpoint_allocation_not_ready") {
52 return new ConnectionTransientError({
53 reason: "endpoint-unavailable",
54 detail: relayProtectedErrorMessage(error),
55 traceId: error.traceId,
56 });
57 }
58 return new ConnectionBlockedError({
59 reason: "permission",
60 detail: relayProtectedErrorMessage(error),
61 traceId: error.traceId,
62 });
4963 case "RelayEnvironmentLinkProofInvalidError":
5064 case "RelayEnvironmentLinkLimitExceededError":
5165 return new ConnectionBlockedError({
packages/client-runtime/src/relay/errorPresentation.ts+3
......@@ -43,6 +43,9 @@ export function relayProtectedErrorMessage(error: RelayProtectedError): string {
4343 if (error.reason === "environment_link_not_found") {
4444 return "Relay has no active link for this environment. The environment server may not have re-established its link yet.";
4545 }
46 if (error.reason === "managed_endpoint_allocation_not_ready") {
47 return "The environment's T3 Connect tunnel is still being set up.";
48 }
4649 return error.reason
4750 ? `Relay rejected the environment connection request (${error.reason}).`
4851 : "Relay rejected the environment connection request.";