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 @@...@@ -1,6 +1,7 @@
1import { EnvironmentAuthInvalidError } from "@t3tools/contracts";1import { EnvironmentAuthInvalidError } from "@t3tools/contracts";
2import {2import {
3 RelayAuthInvalidError,3 RelayAuthInvalidError,
4 RelayEnvironmentConnectNotAuthorizedError,
4 RelayEnvironmentEndpointTimedOutError,5 RelayEnvironmentEndpointTimedOutError,
5} from "@t3tools/contracts/relay";6} from "@t3tools/contracts/relay";
6import { describe, expect, it } from "@effect/vitest";7import { describe, expect, it } from "@effect/vitest";
...@@ -35,6 +36,38 @@ describe("mapManagedRelayError", () => {...@@ -35,6 +36,38 @@ describe("mapManagedRelayError", () => {
35 });36 });
36 });37 });
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
38 it("presents clock skew as one possible cause for a generic DPoP error", () => {71 it("presents clock skew as one possible cause for a generic DPoP error", () => {
39 const mapped = mapManagedRelayError(72 const mapped = mapManagedRelayError(
40 new ManagedRelayRequestFailedError({73 new ManagedRelayRequestFailedError({
packages/client-runtime/src/connection/errors.ts+14
...@@ -46,6 +46,20 @@ function relayProtectedError(error: RelayProtectedError): ConnectionAttemptError...@@ -46,6 +46,20 @@ function relayProtectedError(error: RelayProtectedError): ConnectionAttemptError
46 traceId: error.traceId,46 traceId: error.traceId,
47 });47 });
48 case "RelayEnvironmentConnectNotAuthorizedError":48 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 });
49 case "RelayEnvironmentLinkProofInvalidError":63 case "RelayEnvironmentLinkProofInvalidError":
50 case "RelayEnvironmentLinkLimitExceededError":64 case "RelayEnvironmentLinkLimitExceededError":
51 return new ConnectionBlockedError({65 return new ConnectionBlockedError({
packages/client-runtime/src/relay/errorPresentation.ts+3
...@@ -43,6 +43,9 @@ export function relayProtectedErrorMessage(error: RelayProtectedError): string {...@@ -43,6 +43,9 @@ export function relayProtectedErrorMessage(error: RelayProtectedError): string {
43 if (error.reason === "environment_link_not_found") {43 if (error.reason === "environment_link_not_found") {
44 return "Relay has no active link for this environment. The environment server may not have re-established its link yet.";44 return "Relay has no active link for this environment. The environment server may not have re-established its link yet.";
45 }45 }
46 if (error.reason === "managed_endpoint_allocation_not_ready") {
47 return "The environment's T3 Connect tunnel is still being set up.";
48 }
46 return error.reason49 return error.reason
47 ? `Relay rejected the environment connection request (${error.reason}).`50 ? `Relay rejected the environment connection request (${error.reason}).`
48 : "Relay rejected the environment connection request.";51 : "Relay rejected the environment connection request.";