Skip to content

Commit e8ac6aa

Browse files
refactor(server): CloudLink fails with its own errors; the connect routes map them to HTTP
CloudLink now constructs domain errors (invalid relay config or origin, account mismatch, rejected or replayed proofs, an unavailable tunnel, and an internal error naming the failed operation with its cause), and relay responses become a RelayRequestError that says whether the relay refused the request. The connect routes map these to the same HTTP errors and messages as before. Startup, recovery and shutdown no longer receive HTTP-shaped errors, and retries key off the domain errors. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
1 parent 5573d02 commit e8ac6aa

8 files changed

Lines changed: 577 additions & 337 deletions

File tree

‎apps/server/src/cloud/CloudLink.lifecycle.test.ts‎

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ import {
4545
managedTunnelStartupAction,
4646
retryManagedTunnelRegistration,
4747
} from "./managedTunnelStartup.ts";
48-
import { shouldRetryCloudLink } from "./relayResponse.ts";
4948
import * as ManagedEndpointRuntime from "./ManagedEndpointRuntime.ts";
5049

5150
const unusedSecretStoreOperation = () => Effect.die("unused secret-store operation");
@@ -109,10 +108,7 @@ describe("reconcileDesiredCloudLink", () => {
109108
const link = yield* CloudLink.CloudLink;
110109
const error = yield* Effect.flip(link.reconcileDesiredLink("http://127.0.0.1:3774"));
111110

112-
expect(error).toMatchObject({
113-
_tag: "EnvironmentHttpUnauthorizedError",
114-
message: "Run `t3 connect link` to authorize this environment.",
115-
});
111+
expect(error._tag).toBe("CloudLinkAuthorizationMissingError");
116112
}).pipe(
117113
Effect.provide(CloudLink.layer),
118114
Effect.provideService(HttpServer.HttpServer, idleHttpServer),
@@ -689,7 +685,7 @@ describe("releaseManagedTunnelOnShutdown", () => {
689685
link
690686
.registerManagedTunnelRecovery(localOrigin)
691687
.pipe(Effect.tapError(() => Deferred.succeed(firstFailure, undefined))),
692-
shouldRetryCloudLink,
688+
CloudLink.shouldRetryCloudLink,
693689
link
694690
.startManagedTunnelIfOriginConfirmed(localOrigin, {
695691
requireConfirmedOrigin: false,
@@ -1051,10 +1047,10 @@ describe("releaseManagedTunnelOnShutdown", () => {
10511047
});
10521048

10531049
it.effect.each([
1054-
{ status: 401, errorTag: "EnvironmentHttpUnauthorizedError" },
1055-
{ status: 403, errorTag: "EnvironmentHttpForbiddenError" },
1056-
{ status: 409, errorTag: "EnvironmentHttpBadRequestError" },
1057-
])("preserves a permanent $status relay recovery failure", ({ status, errorTag }) => {
1050+
{ status: 401, rejection: "unauthorized" },
1051+
{ status: 403, rejection: "forbidden" },
1052+
{ status: 409, rejection: "rejected" },
1053+
])("preserves a permanent $status relay recovery failure", ({ status, rejection }) => {
10581054
const { store } = makeMemorySecretStore([
10591055
[CLOUD_ENDPOINT_RUNTIME_CONFIG, "old-config"],
10601056
[RELAY_URL_SECRET, "https://relay.example.test"],
@@ -1068,7 +1064,8 @@ describe("releaseManagedTunnelOnShutdown", () => {
10681064
const link = yield* CloudLink.CloudLink;
10691065
const error = yield* Effect.flip(link.recoverManagedTunnel("http://127.0.0.1:3773"));
10701066

1071-
expect(error._tag).toBe(errorTag);
1067+
expect(error).toMatchObject({ _tag: "RelayRequestError", rejection });
1068+
expect(CloudLink.shouldRetryCloudLink(error)).toBe(false);
10721069
expect(requests).toHaveLength(1);
10731070
expect(applyConfigCalls).toEqual([]);
10741071
}).pipe(

‎apps/server/src/cloud/CloudLink.test.ts‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,10 @@ it.effect("puts the relay back when the local save fails", () =>
137137
const error = yield* preferences
138138
.update({ publishAgentActivity: true, holdWebhooksWhileOffline: true })
139139
.pipe(Effect.flip);
140-
assert.equal(error._tag, "EnvironmentHttpInternalServerError");
140+
assert.deepInclude(error, {
141+
_tag: "CloudLinkInternalError",
142+
operation: "persist-preferences",
143+
});
141144
assert.deepEqual(relayCalls, [true, false]);
142145
assert.equal(
143146
new TextDecoder().decode(stored.get(HOLD_WEBHOOKS_WHILE_OFFLINE_SECRET)),
@@ -153,7 +156,10 @@ it.effect("leaves the relay untouched when the activity setting can't be saved",
153156
const error = yield* preferences
154157
.update({ publishAgentActivity: true, holdWebhooksWhileOffline: true })
155158
.pipe(Effect.flip);
156-
assert.equal(error._tag, "EnvironmentHttpInternalServerError");
159+
assert.deepInclude(error, {
160+
_tag: "CloudLinkInternalError",
161+
operation: "persist-preferences",
162+
});
157163
assert.deepEqual(relayCalls, []);
158164
assert.equal(
159165
new TextDecoder().decode(stored.get(HOLD_WEBHOOKS_WHILE_OFFLINE_SECRET)),
@@ -184,7 +190,7 @@ it.effect("changes nothing when the current activity setting can't be read", ()
184190
const error = yield* preferences
185191
.update({ publishAgentActivity: true, holdWebhooksWhileOffline: true })
186192
.pipe(Effect.flip);
187-
assert.equal(error._tag, "EnvironmentHttpInternalServerError");
193+
assert.deepInclude(error, { _tag: "CloudLinkInternalError", operation: "read-preferences" });
188194
assert.deepEqual(relayCalls, []);
189195
assert.equal(new TextDecoder().decode(stored.get(PUBLISH_AGENT_ACTIVITY_SECRET)), "false");
190196
}),
@@ -231,7 +237,7 @@ it.effect("changes nothing when the current hold setting can't be read", () =>
231237
const error = yield* preferences
232238
.update({ publishAgentActivity: true, holdWebhooksWhileOffline: false })
233239
.pipe(Effect.flip);
234-
assert.equal(error._tag, "EnvironmentHttpInternalServerError");
240+
assert.deepInclude(error, { _tag: "CloudLinkInternalError", operation: "read-preferences" });
235241
assert.deepEqual(relayCalls, []);
236242
assert.equal(new TextDecoder().decode(stored.get(PUBLISH_AGENT_ACTIVITY_SECRET)), "false");
237243
}),

0 commit comments

Comments
 (0)