





















@@ -639,6 +639,12 @@ describe("sanitizeFileNameForUpload", () => {
639639});
640640641641describe("downloadMessageResourceFeishu", () => {
642+function httpStatusError(status: number): Error & { response: { status: number } } {
643+return Object.assign(new Error(`Request failed with status code ${status}`), {
644+response: { status },
645+});
646+}
647+642648beforeEach(() => {
643649vi.clearAllMocks();
644650mockResolvedFeishuAccount();
@@ -717,6 +723,97 @@ describe("downloadMessageResourceFeishu", () => {
717723});
718724});
719725726+it("retries file resources as media after HTTP 502", async () => {
727+const originalError = httpStatusError(502);
728+messageResourceGetMock.mockRejectedValueOnce(originalError).mockResolvedValueOnce({
729+data: Buffer.from("fake-ios-video-data"),
730+headers: {
731+"content-type": "video/mp4",
732+"content-disposition": `attachment; filename="ios-video.mp4"`,
733+},
734+});
735+736+const result = await downloadMessageResourceFeishu({
737+cfg: emptyConfig,
738+messageId: "om_ios_video_msg",
739+fileKey: "file_key_ios_video",
740+type: "file",
741+});
742+743+expect(messageResourceGetMock).toHaveBeenNthCalledWith(
744+1,
745+expect.objectContaining({
746+path: { message_id: "om_ios_video_msg", file_key: "file_key_ios_video" },
747+params: { type: "file" },
748+}),
749+);
750+expect(messageResourceGetMock).toHaveBeenNthCalledWith(
751+2,
752+expect.objectContaining({
753+path: { message_id: "om_ios_video_msg", file_key: "file_key_ios_video" },
754+params: { type: "media" },
755+}),
756+);
757+expect(result).toMatchObject({
758+buffer: Buffer.from("fake-ios-video-data"),
759+contentType: "video/mp4",
760+fileName: "ios-video.mp4",
761+});
762+});
763+764+it("rethrows the original HTTP 502 when the media retry fails", async () => {
765+const originalError = httpStatusError(502);
766+messageResourceGetMock
767+.mockRejectedValueOnce(originalError)
768+.mockRejectedValueOnce(new Error("media retry failed"));
769+770+await expect(
771+downloadMessageResourceFeishu({
772+cfg: emptyConfig,
773+messageId: "om_ios_video_msg",
774+fileKey: "file_key_ios_video",
775+type: "file",
776+}),
777+).rejects.toBe(originalError);
778+779+expect(messageResourceGetMock).toHaveBeenNthCalledWith(
780+1,
781+expect.objectContaining({ params: { type: "file" } }),
782+);
783+expect(messageResourceGetMock).toHaveBeenNthCalledWith(
784+2,
785+expect.objectContaining({ params: { type: "media" } }),
786+);
787+});
788+789+it("does not retry non-fallback download failures", async () => {
790+for (const scenario of [
791+{ messageId: "om_image_msg", fileKey: "img_key_502", type: "image" as const, status: 502 },
792+{ messageId: "om_file_msg", fileKey: "file_key_500", type: "file" as const, status: 500 },
793+]) {
794+const originalError = httpStatusError(scenario.status);
795+messageResourceGetMock.mockClear();
796+messageResourceGetMock.mockRejectedValueOnce(originalError);
797+798+await expect(
799+downloadMessageResourceFeishu({
800+cfg: emptyConfig,
801+messageId: scenario.messageId,
802+fileKey: scenario.fileKey,
803+type: scenario.type,
804+}),
805+).rejects.toBe(originalError);
806+807+expect(messageResourceGetMock).toHaveBeenCalledTimes(1);
808+expect(messageResourceGetMock).toHaveBeenCalledWith(
809+expect.objectContaining({
810+path: { message_id: scenario.messageId, file_key: scenario.fileKey },
811+params: { type: scenario.type },
812+}),
813+);
814+}
815+});
816+720817it("recovers CJK filenames from plain Content-Disposition headers decoded as Latin-1", async () => {
721818const fileName = "武汉15座山登山信息汇总.csv";
722819const latin1HeaderFileName = Buffer.from(fileName, "utf8").toString("latin1");
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。