From 0a67033fe302e8d36600d0d366ba43903bbd0a94 Mon Sep 17 00:00:00 2001 From: SidQin-cyber Date: Fri, 27 Feb 2026 23:55:11 +0800 Subject: [PATCH] fix(discord): keep attachment metadata when media fetch is blocked Preserve inbound attachment/sticker metadata in Discord message context when media download fails (for example due to SSRF blocking), so agents still see file references instead of silent drops. Closes #28816 --- src/discord/monitor/message-utils.test.ts | 79 +++++++++++++++++++++++ src/discord/monitor/message-utils.ts | 27 ++++++++ 2 files changed, 106 insertions(+) diff --git a/src/discord/monitor/message-utils.test.ts b/src/discord/monitor/message-utils.test.ts index 300780b0d54..5b66d61a5c3 100644 --- a/src/discord/monitor/message-utils.test.ts +++ b/src/discord/monitor/message-utils.test.ts @@ -139,6 +139,34 @@ describe("resolveForwardedMediaList", () => { ); }); + it("keeps forwarded attachment metadata when download fails", async () => { + const attachment = { + id: "att-fallback", + url: "https://cdn.discordapp.com/attachments/1/fallback.png", + filename: "fallback.png", + content_type: "image/png", + }; + fetchRemoteMedia.mockRejectedValueOnce(new Error("blocked by ssrf guard")); + + const result = await resolveForwardedMediaList( + asMessage({ + rawData: { + message_snapshots: [{ message: { attachments: [attachment] } }], + }, + }), + 512, + ); + + expect(saveMediaBuffer).not.toHaveBeenCalled(); + expect(result).toEqual([ + { + path: attachment.url, + contentType: "image/png", + placeholder: "", + }, + ]); + }); + it("downloads forwarded stickers", async () => { const sticker = { id: "sticker-1", @@ -279,6 +307,57 @@ describe("resolveMediaList", () => { expect.objectContaining({ fetchImpl: proxyFetch }), ); }); + + it("keeps attachment metadata when download fails", async () => { + const attachment = { + id: "att-main-fallback", + url: "https://cdn.discordapp.com/attachments/1/main-fallback.png", + filename: "main-fallback.png", + content_type: "image/png", + }; + fetchRemoteMedia.mockRejectedValueOnce(new Error("blocked by ssrf guard")); + + const result = await resolveMediaList( + asMessage({ + attachments: [attachment], + }), + 512, + ); + + expect(saveMediaBuffer).not.toHaveBeenCalled(); + expect(result).toEqual([ + { + path: attachment.url, + contentType: "image/png", + placeholder: "", + }, + ]); + }); + + it("keeps sticker metadata when sticker download fails", async () => { + const sticker = { + id: "sticker-fallback", + name: "fallback", + format_type: StickerFormatType.PNG, + }; + fetchRemoteMedia.mockRejectedValueOnce(new Error("blocked by ssrf guard")); + + const result = await resolveMediaList( + asMessage({ + stickers: [sticker], + }), + 512, + ); + + expect(saveMediaBuffer).not.toHaveBeenCalled(); + expect(result).toEqual([ + { + path: "https://media.discordapp.net/stickers/sticker-fallback.png", + contentType: "image/png", + placeholder: "", + }, + ]); + }); }); describe("Discord media SSRF policy", () => { diff --git a/src/discord/monitor/message-utils.ts b/src/discord/monitor/message-utils.ts index bf446686e59..52b30c8c87c 100644 --- a/src/discord/monitor/message-utils.ts +++ b/src/discord/monitor/message-utils.ts @@ -250,6 +250,12 @@ async function appendResolvedMediaFromAttachments(params: { } catch (err) { const id = attachment.id ?? attachment.url; logVerbose(`${params.errorPrefix} ${id}: ${String(err)}`); + // Preserve attachment context even when remote fetch is blocked/fails. + params.out.push({ + path: attachment.url, + contentType: attachment.content_type, + placeholder: inferPlaceholder(attachment), + }); } } } @@ -306,6 +312,19 @@ function formatStickerError(err: unknown): string { } } +function inferStickerContentType(sticker: APIStickerItem): string | undefined { + switch (sticker.format_type) { + case StickerFormatType.GIF: + return "image/gif"; + case StickerFormatType.APNG: + case StickerFormatType.Lottie: + case StickerFormatType.PNG: + return "image/png"; + default: + return undefined; + } +} + async function appendResolvedMediaFromStickers(params: { stickers?: APIStickerItem[] | null; maxBytes: number; @@ -348,6 +367,14 @@ async function appendResolvedMediaFromStickers(params: { } if (lastError) { logVerbose(`${params.errorPrefix} ${sticker.id}: ${formatStickerError(lastError)}`); + const fallback = candidates[0]; + if (fallback) { + params.out.push({ + path: fallback.url, + contentType: inferStickerContentType(sticker), + placeholder: "", + }); + } } } }