mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
fix(control-ui): classify websocket security errors
This commit is contained in:
@@ -324,6 +324,46 @@ describe("GatewayBrowserClient", () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("reports generic WebSocket construction failures without retrying", async () => {
|
||||
vi.useFakeTimers();
|
||||
const onClose = vi.fn();
|
||||
class ThrowingWebSocket {
|
||||
static OPEN = 1;
|
||||
|
||||
constructor(_url: string) {
|
||||
throw new TypeError("constructor failed");
|
||||
}
|
||||
}
|
||||
vi.stubGlobal("WebSocket", ThrowingWebSocket);
|
||||
|
||||
const client = new GatewayBrowserClient({
|
||||
url: "ws://gateway.example:18789",
|
||||
token: "shared-auth-token",
|
||||
onClose,
|
||||
});
|
||||
|
||||
expect(() => client.start()).not.toThrow();
|
||||
expect(onClose).toHaveBeenCalledWith({
|
||||
code: 1006,
|
||||
reason: "websocket error",
|
||||
error: expect.objectContaining({
|
||||
code: "BROWSER_WEBSOCKET_CONSTRUCTOR_ERROR",
|
||||
message: expect.stringContaining("Could not create the Gateway WebSocket"),
|
||||
details: expect.objectContaining({
|
||||
code: "BROWSER_WEBSOCKET_CONSTRUCTOR_ERROR",
|
||||
browserErrorName: "TypeError",
|
||||
browserMessage: "constructor failed",
|
||||
}),
|
||||
}),
|
||||
});
|
||||
expect(wsInstances).toHaveLength(0);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(30_000);
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("reports request timing for attributed RPC latency", async () => {
|
||||
const onRequestTiming = vi.fn();
|
||||
const client = new GatewayBrowserClient({
|
||||
|
||||
@@ -115,6 +115,36 @@ describe("resolveLoginFailureFeedback", () => {
|
||||
expect(feedback?.steps.join(" ")).toContain("gateway.controlUi.allowInsecureAuth");
|
||||
});
|
||||
|
||||
it("explains browser WebSocket security failures as insecure context", () => {
|
||||
const feedback = resolveLoginFailureFeedback({
|
||||
connected: false,
|
||||
lastError:
|
||||
"Browser refused the Gateway WebSocket for security reasons. Use wss:// when the Control UI is served over HTTPS/Tailscale Serve, or open the loopback dashboard at http://127.0.0.1:18789.",
|
||||
lastErrorCode: "BROWSER_WEBSOCKET_SECURITY_ERROR",
|
||||
hasToken: true,
|
||||
hasPassword: false,
|
||||
});
|
||||
|
||||
expect(feedback?.kind).toBe("insecure-context");
|
||||
expect(feedback?.rawError).toContain("Use wss://");
|
||||
expect(feedback?.rawError).toContain("http://127.0.0.1:18789");
|
||||
expect(feedback?.steps.join(" ")).toContain("Tailscale Serve");
|
||||
expect(feedback?.steps.join(" ")).toContain("gateway.controlUi.allowInsecureAuth");
|
||||
});
|
||||
|
||||
it("keeps generic browser WebSocket constructor failures on the network path", () => {
|
||||
const feedback = resolveLoginFailureFeedback({
|
||||
connected: false,
|
||||
lastError: "Could not create the Gateway WebSocket: constructor failed",
|
||||
lastErrorCode: "BROWSER_WEBSOCKET_CONSTRUCTOR_ERROR",
|
||||
hasToken: false,
|
||||
hasPassword: false,
|
||||
});
|
||||
|
||||
expect(feedback?.kind).toBe("network");
|
||||
expect(feedback?.steps.join(" ")).toContain("WebSocket URL");
|
||||
});
|
||||
|
||||
it("explains browser origin rejections", () => {
|
||||
const feedback = resolveLoginFailureFeedback({
|
||||
connected: false,
|
||||
|
||||
@@ -25,7 +25,10 @@ const AUTH_FAILURE_CODES = new Set<string>([
|
||||
ConnectErrorDetailCodes.AUTH_TAILSCALE_IDENTITY_MISMATCH,
|
||||
]);
|
||||
|
||||
const BROWSER_WEBSOCKET_SECURITY_ERROR_CODE = "BROWSER_WEBSOCKET_SECURITY_ERROR";
|
||||
|
||||
const INSECURE_CONTEXT_CODES = new Set<string>([
|
||||
BROWSER_WEBSOCKET_SECURITY_ERROR_CODE,
|
||||
ConnectErrorDetailCodes.CONTROL_UI_DEVICE_IDENTITY_REQUIRED,
|
||||
ConnectErrorDetailCodes.DEVICE_IDENTITY_REQUIRED,
|
||||
]);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ConnectErrorDetailCodes } from "../../../../src/gateway/protocol/connec
|
||||
import {
|
||||
resolveAuthHintKind,
|
||||
resolvePairingHint,
|
||||
shouldShowInsecureContextHint,
|
||||
shouldShowPairingHint,
|
||||
} from "./overview-hints.ts";
|
||||
|
||||
@@ -107,3 +108,25 @@ describe("resolveAuthHintKind", () => {
|
||||
).toBe("failed");
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldShowInsecureContextHint", () => {
|
||||
it("returns true for browser WebSocket security errors", () => {
|
||||
expect(
|
||||
shouldShowInsecureContextHint(
|
||||
false,
|
||||
"Browser refused the Gateway WebSocket for security reasons.",
|
||||
"BROWSER_WEBSOCKET_SECURITY_ERROR",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not treat generic WebSocket constructor errors as insecure context", () => {
|
||||
expect(
|
||||
shouldShowInsecureContextHint(
|
||||
false,
|
||||
"Could not create the Gateway WebSocket: constructor failed",
|
||||
"BROWSER_WEBSOCKET_CONSTRUCTOR_ERROR",
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user