Skip to content

Commit b870831

Browse files
committed
fix(feishu): avoid axios interceptor internals
1 parent 6ec579a commit b870831

2 files changed

Lines changed: 90 additions & 35 deletions

File tree

extensions/feishu/src/client.test.ts

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ type CreateFeishuWSClient = typeof import("./client.js").createFeishuWSClient;
77
type ClearClientCache = typeof import("./client.js").clearClientCache;
88
type SetFeishuClientRuntimeForTest = typeof import("./client.js").setFeishuClientRuntimeForTest;
99

10+
const requestInterceptorState = vi.hoisted(() => {
11+
let registered: ((req: unknown) => unknown) | undefined;
12+
return {
13+
get registered() {
14+
return registered;
15+
},
16+
use: vi.fn((fn: (req: unknown) => unknown) => {
17+
registered = fn;
18+
}),
19+
};
20+
});
1021
const clientCtorMock = vi.hoisted(() =>
1122
vi.fn(function clientCtor() {
1223
return { connected: true };
@@ -22,16 +33,31 @@ const proxyAgentCtorMock = vi.hoisted(() =>
2233
return { proxied: true };
2334
}),
2435
);
25-
const mockBaseHttpInstance = vi.hoisted(() => ({
26-
request: vi.fn().mockResolvedValue({}),
27-
get: vi.fn().mockResolvedValue({}),
28-
post: vi.fn().mockResolvedValue({}),
29-
put: vi.fn().mockResolvedValue({}),
30-
patch: vi.fn().mockResolvedValue({}),
31-
delete: vi.fn().mockResolvedValue({}),
32-
head: vi.fn().mockResolvedValue({}),
33-
options: vi.fn().mockResolvedValue({}),
34-
}));
36+
const mockBaseHttpInstance = vi.hoisted(() => {
37+
const requestInterceptors = { use: requestInterceptorState.use };
38+
Object.defineProperty(requestInterceptors, "handlers", {
39+
configurable: true,
40+
get() {
41+
throw new Error("Do not read axios private interceptor handlers");
42+
},
43+
set() {
44+
throw new Error("Do not write axios private interceptor handlers");
45+
},
46+
});
47+
return {
48+
request: vi.fn().mockResolvedValue({}),
49+
get: vi.fn().mockResolvedValue({}),
50+
post: vi.fn().mockResolvedValue({}),
51+
put: vi.fn().mockResolvedValue({}),
52+
patch: vi.fn().mockResolvedValue({}),
53+
delete: vi.fn().mockResolvedValue({}),
54+
head: vi.fn().mockResolvedValue({}),
55+
options: vi.fn().mockResolvedValue({}),
56+
interceptors: {
57+
request: requestInterceptors,
58+
},
59+
};
60+
});
3561
const proxyEnvKeys = ["https_proxy", "HTTPS_PROXY", "http_proxy", "HTTP_PROXY"] as const;
3662
type ProxyEnvKey = (typeof proxyEnvKeys)[number];
3763
const registerFeishuDocToolsMock = vi.hoisted(() => vi.fn());
@@ -51,6 +77,7 @@ let setFeishuClientRuntimeForTest: SetFeishuClientRuntimeForTest;
5177
let FEISHU_HTTP_TIMEOUT_MS: number;
5278
let FEISHU_HTTP_TIMEOUT_MAX_MS: number;
5379
let FEISHU_HTTP_TIMEOUT_ENV_VAR: string;
80+
let FEISHU_USER_AGENT: string;
5481

5582
let priorProxyEnv: Partial<Record<ProxyEnvKey, string | undefined>> = {};
5683
let priorFeishuTimeoutEnv: string | undefined;
@@ -178,6 +205,7 @@ beforeAll(async () => {
178205
FEISHU_HTTP_TIMEOUT_MS,
179206
FEISHU_HTTP_TIMEOUT_MAX_MS,
180207
FEISHU_HTTP_TIMEOUT_ENV_VAR,
208+
FEISHU_USER_AGENT,
181209
} = await import("./client.js"));
182210
});
183211

@@ -239,6 +267,26 @@ afterAll(() => {
239267
vi.resetModules();
240268
});
241269

270+
describe("Feishu default User-Agent interceptor", () => {
271+
it("registers through the public interceptor API and overrides the SDK User-Agent", () => {
272+
expect(requestInterceptorState.registered).toBeTypeOf("function");
273+
274+
const req = { headers: { "User-Agent": "oapi-node-sdk/1.0.0" } };
275+
expect(requestInterceptorState.registered?.(req)).toBe(req);
276+
277+
expect(req.headers["User-Agent"]).toBe(FEISHU_USER_AGENT);
278+
});
279+
280+
it("sets the User-Agent on AxiosHeaders-like request headers", () => {
281+
const headers = { set: vi.fn() };
282+
const req = { headers };
283+
284+
expect(requestInterceptorState.registered?.(req)).toBe(req);
285+
286+
expect(headers.set).toHaveBeenCalledWith("User-Agent", FEISHU_USER_AGENT);
287+
});
288+
});
289+
242290
describe("createFeishuClient HTTP timeout", () => {
243291
const readLastClientHttpInstance = (): HttpInstanceLike =>
244292
requireHttpInstance(readCallOptions(clientCtorMock).httpInstance);

extensions/feishu/src/client.ts

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -54,33 +54,40 @@ const defaultFeishuClientSdk: FeishuClientSdk = {
5454

5555
let feishuClientSdk: FeishuClientSdk = defaultFeishuClientSdk;
5656

57-
// Override the SDK's default User-Agent interceptor.
58-
// The Lark SDK registers an axios request interceptor that sets
59-
// 'oapi-node-sdk/1.0.0'. Axios request interceptors execute in LIFO order
60-
// (last-registered runs first), so simply appending ours doesn't work — the
61-
// SDK's interceptor would run last and overwrite our UA. We must clear
62-
// handlers[] first, then register our own as the sole interceptor.
63-
//
64-
// Risk is low: the SDK only registers one interceptor (UA) at init time, and
65-
// we clear it at module load before any other code can register handlers.
66-
// If a future SDK version adds more interceptors, the upgrade will need
67-
// compatibility verification regardless.
68-
{
69-
const inst = Lark.defaultHttpInstance as {
70-
interceptors?: {
71-
request: { handlers: unknown[]; use: (fn: (req: unknown) => unknown) => void };
72-
};
57+
type RequestInterceptorApi = {
58+
use: (fn: (req: unknown) => unknown) => unknown;
59+
};
60+
61+
type FeishuDefaultHttpInstanceWithInterceptors = {
62+
interceptors?: {
63+
request?: RequestInterceptorApi;
7364
};
74-
if (inst.interceptors?.request) {
75-
inst.interceptors.request.handlers = [];
76-
inst.interceptors.request.use((req: unknown) => {
77-
const r = req as { headers?: Record<string, string> };
78-
if (r.headers) {
79-
r.headers["User-Agent"] = getFeishuUserAgent();
80-
}
81-
return req;
82-
});
65+
};
66+
67+
function setRequestUserAgent(req: unknown) {
68+
const request = req as { headers?: unknown };
69+
const headers = request.headers;
70+
if (!headers) {
71+
request.headers = { "User-Agent": getFeishuUserAgent() };
72+
return req;
8373
}
74+
75+
const maybeAxiosHeaders = headers as { set?: unknown };
76+
if (typeof maybeAxiosHeaders.set === "function") {
77+
maybeAxiosHeaders.set("User-Agent", getFeishuUserAgent());
78+
return req;
79+
}
80+
81+
(headers as Record<string, string>)["User-Agent"] = getFeishuUserAgent();
82+
return req;
83+
}
84+
85+
// Override the SDK's default User-Agent through the public interceptor API.
86+
// The SDK fallback interceptor only fills User-Agent when it is absent, so this
87+
// interceptor can preserve the rest of the SDK's request interceptor stack.
88+
{
89+
const inst = Lark.defaultHttpInstance as FeishuDefaultHttpInstanceWithInterceptors;
90+
inst.interceptors?.request?.use(setRequestUserAgent);
8491
}
8592

8693
export { FEISHU_HTTP_TIMEOUT_ENV_VAR, FEISHU_HTTP_TIMEOUT_MAX_MS, FEISHU_HTTP_TIMEOUT_MS };

0 commit comments

Comments
 (0)