Skip to content

Commit b29f8fd

Browse files
steipeteruanrrn
andcommitted
fix(ui): preserve structured connect failures
Co-authored-by: RayRuan <[email protected]>
1 parent bf04d04 commit b29f8fd

2 files changed

Lines changed: 40 additions & 2 deletions

File tree

ui/src/api/gateway.node.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,6 +1057,41 @@ describe("GatewayBrowserClient", () => {
10571057
}
10581058
});
10591059

1060+
it("preserves structured connect errors for pending requests", async () => {
1061+
useNodeFakeTimers();
1062+
const client = new GatewayBrowserClient({
1063+
url: "ws://127.0.0.1:18789",
1064+
token: "shared-auth-token",
1065+
});
1066+
1067+
try {
1068+
const { ws, connectFrame } = await startConnect(client);
1069+
const pendingRequest = client.request("cron.list", { quiet: true });
1070+
1071+
ws.emitMessage({
1072+
type: "res",
1073+
id: connectFrame.id,
1074+
ok: false,
1075+
error: {
1076+
code: "INVALID_REQUEST",
1077+
message: "unauthorized",
1078+
details: { code: "PAIRING_REQUIRED" },
1079+
},
1080+
});
1081+
await expectSocketClosed(ws);
1082+
ws.emitClose(4008, "connect failed");
1083+
1084+
await expect(pendingRequest).rejects.toMatchObject({
1085+
name: "GatewayRequestError",
1086+
gatewayCode: "INVALID_REQUEST",
1087+
details: { code: "PAIRING_REQUIRED" },
1088+
});
1089+
} finally {
1090+
client.stop();
1091+
vi.useRealTimers();
1092+
}
1093+
});
1094+
10601095
it("treats IPv6 loopback as trusted for bounded device-token retry", async () => {
10611096
useNodeFakeTimers();
10621097
const { client } = await expectRetriedDeviceTokenConnect({

ui/src/api/gateway.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -581,12 +581,15 @@ export class GatewayBrowserClient {
581581
errorCode: connectError?.code ?? "SOCKET_CLOSED",
582582
});
583583
this.ws = null;
584+
const closeError = connectError
585+
? new GatewayRequestError(connectError)
586+
: new Error(`gateway closed (${ev.code}): ${reason}`);
584587
if (this.pendingStartupReconnectDelayMs !== null) {
585-
this.flushPending(new Error(`gateway closed (${ev.code}): ${reason}`));
588+
this.flushPending(closeError);
586589
this.scheduleReconnect();
587590
return;
588591
}
589-
this.flushPending(new Error(`gateway closed (${ev.code}): ${reason}`));
592+
this.flushPending(closeError);
590593
const connectErrorCode = resolveGatewayErrorDetailCode(connectError);
591594
// willRetry drives both the reconnect scheduling below and the app
592595
// layer's "still reconnecting vs gave up" rendering; keep them in sync.

0 commit comments

Comments
 (0)