Skip to content

Commit 0a23739

Browse files
colin719liruiclaudeTakhoffman
authored
fix(feishu): pass proxy agent to WSClient for proxy environments (#26397)
* fix(feishu): pass proxy agent to WSClient for environments behind HTTPS proxy The Lark SDK WSClient uses the `ws` library which does not automatically respect https_proxy/HTTP_PROXY environment variables. This causes WebSocket connection failures in proxy environments (e.g. WSL2 with a local proxy). Detect proxy env vars and pass an HttpsProxyAgent to WSClient via the existing `agent` constructor option. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix(feishu): add generic type parameter to HttpsProxyAgent return type Fix TS2314: `HttpsProxyAgent<Uri>` requires a type argument. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix(feishu): wire ws proxy dependency and coverage * chore(lockfile): resolve axios peer lock entry after rebase --------- Co-authored-by: lirui <[email protected]> Co-authored-by: Claude Opus 4.6 <[email protected]> Co-authored-by: Tak Hoffman <[email protected]>
1 parent 4dc55ea commit 0a23739

File tree

5 files changed

+121
-0
lines changed

5 files changed

+121
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ Docs: https://docs.openclaw.ai
234234
- Security/Microsoft Teams: isolate group allowlist and command authorization from DM pairing-store entries to prevent cross-context authorization bleed. (#26111) Thanks @bmendonca3.
235235
- Security/SSRF guard: classify IPv6 multicast literals (`ff00::/8`) as blocked/private-internal targets in shared SSRF IP checks, preventing multicast literals from bypassing URL-host preflight and DNS answer validation. This ships in the next npm release (`2026.2.26`). Thanks @zpbrent for reporting.
236236
- Tests/Low-memory stability: disable Vitest `vmForks` by default on low-memory local hosts (`<64 GiB`), keep low-profile extension lane parallelism at 4 workers, and align cron isolated-agent tests with `setSessionRuntimeModel` usage to avoid deterministic suite failures. (#26324) Thanks @ngutman.
237+
- Feishu/WebSocket proxy: pass a proxy agent to Feishu WS clients from standard proxy environment variables and include plugin-local runtime dependency wiring so websocket mode works in proxy-constrained installs. (#26397) Thanks @colin719.
237238

238239
## 2026.2.24
239240

extensions/feishu/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"dependencies": {
77
"@larksuiteoapi/node-sdk": "^1.59.0",
88
"@sinclair/typebox": "0.34.48",
9+
"https-proxy-agent": "^7.0.6",
910
"zod": "^4.3.6"
1011
},
1112
"openclaw": {
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2+
import type { FeishuConfig, ResolvedFeishuAccount } from "./types.js";
3+
4+
const wsClientCtorMock = vi.hoisted(() =>
5+
vi.fn(function wsClientCtor() {
6+
return { connected: true };
7+
}),
8+
);
9+
const httpsProxyAgentCtorMock = vi.hoisted(() =>
10+
vi.fn(function httpsProxyAgentCtor(proxyUrl: string) {
11+
return { proxyUrl };
12+
}),
13+
);
14+
15+
vi.mock("@larksuiteoapi/node-sdk", () => ({
16+
AppType: { SelfBuild: "self" },
17+
Domain: { Feishu: "https://open.feishu.cn", Lark: "https://open.larksuite.com" },
18+
LoggerLevel: { info: "info" },
19+
Client: vi.fn(),
20+
WSClient: wsClientCtorMock,
21+
EventDispatcher: vi.fn(),
22+
}));
23+
24+
vi.mock("https-proxy-agent", () => ({
25+
HttpsProxyAgent: httpsProxyAgentCtorMock,
26+
}));
27+
28+
import { createFeishuWSClient } from "./client.js";
29+
30+
const proxyEnvKeys = ["https_proxy", "HTTPS_PROXY", "http_proxy", "HTTP_PROXY"] as const;
31+
type ProxyEnvKey = (typeof proxyEnvKeys)[number];
32+
33+
let priorProxyEnv: Partial<Record<ProxyEnvKey, string | undefined>> = {};
34+
35+
const baseAccount: ResolvedFeishuAccount = {
36+
accountId: "main",
37+
enabled: true,
38+
configured: true,
39+
appId: "app_123",
40+
appSecret: "secret_123",
41+
domain: "feishu",
42+
config: {} as FeishuConfig,
43+
};
44+
45+
function firstWsClientOptions(): { agent?: unknown } {
46+
const calls = wsClientCtorMock.mock.calls as unknown as Array<[options: { agent?: unknown }]>;
47+
return calls[0]?.[0] ?? {};
48+
}
49+
50+
beforeEach(() => {
51+
priorProxyEnv = {};
52+
for (const key of proxyEnvKeys) {
53+
priorProxyEnv[key] = process.env[key];
54+
delete process.env[key];
55+
}
56+
vi.clearAllMocks();
57+
});
58+
59+
afterEach(() => {
60+
for (const key of proxyEnvKeys) {
61+
const value = priorProxyEnv[key];
62+
if (value === undefined) {
63+
delete process.env[key];
64+
} else {
65+
process.env[key] = value;
66+
}
67+
}
68+
});
69+
70+
describe("createFeishuWSClient proxy handling", () => {
71+
it("does not set a ws proxy agent when proxy env is absent", () => {
72+
createFeishuWSClient(baseAccount);
73+
74+
expect(httpsProxyAgentCtorMock).not.toHaveBeenCalled();
75+
const options = firstWsClientOptions();
76+
expect(options?.agent).toBeUndefined();
77+
});
78+
79+
it("uses proxy env precedence: https_proxy first, then HTTPS_PROXY, then http_proxy/HTTP_PROXY", () => {
80+
process.env.https_proxy = "http://lower-https:8001";
81+
process.env.HTTPS_PROXY = "http://upper-https:8002";
82+
process.env.http_proxy = "http://lower-http:8003";
83+
process.env.HTTP_PROXY = "http://upper-http:8004";
84+
85+
createFeishuWSClient(baseAccount);
86+
87+
expect(httpsProxyAgentCtorMock).toHaveBeenCalledTimes(1);
88+
expect(httpsProxyAgentCtorMock).toHaveBeenCalledWith("http://lower-https:8001");
89+
const options = firstWsClientOptions();
90+
expect(options.agent).toEqual({ proxyUrl: "http://lower-https:8001" });
91+
});
92+
93+
it("passes HTTP_PROXY to ws client when https vars are unset", () => {
94+
process.env.HTTP_PROXY = "http://upper-http:8999";
95+
96+
createFeishuWSClient(baseAccount);
97+
98+
expect(httpsProxyAgentCtorMock).toHaveBeenCalledTimes(1);
99+
expect(httpsProxyAgentCtorMock).toHaveBeenCalledWith("http://upper-http:8999");
100+
const options = firstWsClientOptions();
101+
expect(options.agent).toEqual({ proxyUrl: "http://upper-http:8999" });
102+
});
103+
});

extensions/feishu/src/client.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
import * as Lark from "@larksuiteoapi/node-sdk";
2+
import { HttpsProxyAgent } from "https-proxy-agent";
23
import type { FeishuDomain, ResolvedFeishuAccount } from "./types.js";
34

5+
function getWsProxyAgent(): HttpsProxyAgent<string> | undefined {
6+
const proxyUrl =
7+
process.env.https_proxy ||
8+
process.env.HTTPS_PROXY ||
9+
process.env.http_proxy ||
10+
process.env.HTTP_PROXY;
11+
if (!proxyUrl) return undefined;
12+
return new HttpsProxyAgent(proxyUrl);
13+
}
14+
415
// Multi-account client cache
516
const clientCache = new Map<
617
string,
@@ -81,11 +92,13 @@ export function createFeishuWSClient(account: ResolvedFeishuAccount): Lark.WSCli
8192
throw new Error(`Feishu credentials not configured for account "${accountId}"`);
8293
}
8394

95+
const agent = getWsProxyAgent();
8496
return new Lark.WSClient({
8597
appId,
8698
appSecret,
8799
domain: resolveDomain(domain),
88100
loggerLevel: Lark.LoggerLevel.info,
101+
...(agent ? { agent } : {}),
89102
});
90103
}
91104

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)