fix(feishu): catch thrown SDK errors for withdrawn reply targets

The Feishu Lark SDK can throw exceptions (SDK errors with .code or
AxiosErrors with .response.data.code) for withdrawn/deleted reply
targets, in addition to returning error codes in the response object.

Wrap reply calls in sendMessageFeishu and sendCardFeishu with
try-catch to handle thrown withdrawn/not-found errors (230011,
231003) and fall back to client.im.message.create, matching the
existing response-level fallback behavior.

Also extract sendFallbackDirect helper to deduplicate the
direct-send fallback block across both functions.

Closes #33496
This commit is contained in:
Munem Hashmi
2026-03-04 02:33:51 +05:00
parent 666073ee46
commit ad0901aec1
2 changed files with 156 additions and 48 deletions

View File

@@ -102,4 +102,78 @@ describe("Feishu reply fallback for withdrawn/deleted targets", () => {
expect(createMock).not.toHaveBeenCalled();
});
it("falls back to create when reply throws a withdrawn SDK error", async () => {
const sdkError = Object.assign(new Error("request failed"), { code: 230011 });
replyMock.mockRejectedValue(sdkError);
createMock.mockResolvedValue({
code: 0,
data: { message_id: "om_thrown_fallback" },
});
const result = await sendMessageFeishu({
cfg: {} as never,
to: "user:ou_target",
text: "hello",
replyToMessageId: "om_parent",
});
expect(replyMock).toHaveBeenCalledTimes(1);
expect(createMock).toHaveBeenCalledTimes(1);
expect(result.messageId).toBe("om_thrown_fallback");
});
it("falls back to create when card reply throws a not-found AxiosError", async () => {
const axiosError = Object.assign(new Error("Request failed"), {
response: { status: 200, data: { code: 231003, msg: "The message is not found" } },
});
replyMock.mockRejectedValue(axiosError);
createMock.mockResolvedValue({
code: 0,
data: { message_id: "om_axios_fallback" },
});
const result = await sendCardFeishu({
cfg: {} as never,
to: "user:ou_target",
card: { schema: "2.0" },
replyToMessageId: "om_parent",
});
expect(replyMock).toHaveBeenCalledTimes(1);
expect(createMock).toHaveBeenCalledTimes(1);
expect(result.messageId).toBe("om_axios_fallback");
});
it("re-throws non-withdrawn thrown errors for text messages", async () => {
const sdkError = Object.assign(new Error("rate limited"), { code: 99991400 });
replyMock.mockRejectedValue(sdkError);
await expect(
sendMessageFeishu({
cfg: {} as never,
to: "user:ou_target",
text: "hello",
replyToMessageId: "om_parent",
}),
).rejects.toThrow("rate limited");
expect(createMock).not.toHaveBeenCalled();
});
it("re-throws non-withdrawn thrown errors for card messages", async () => {
const sdkError = Object.assign(new Error("permission denied"), { code: 99991401 });
replyMock.mockRejectedValue(sdkError);
await expect(
sendCardFeishu({
cfg: {} as never,
to: "user:ou_target",
card: { schema: "2.0" },
replyToMessageId: "om_parent",
}),
).rejects.toThrow("permission denied");
expect(createMock).not.toHaveBeenCalled();
});
});