From 47fe3cc8fc0c42d15e5fe4990ac2a4f30fdc682a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 26 Aug 2026 20:17:04 -0700 Subject: [PATCH] fix: preserve data URL attachment filename extensions (#130625) Carry the normalized MIME into the existing attachment filename inference. Cover PNG/CSV metadata across actions and the real action-to-plugin dispatch boundary. Explicit filenames and MIME overrides remain unchanged. Closes #130618. --- .../message-action-params.attachments.test.ts | 37 +++++++++++++++---- src/infra/outbound/message-action-params.ts | 2 +- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/infra/outbound/message-action-params.attachments.test.ts b/src/infra/outbound/message-action-params.attachments.test.ts index 2d539f0ab82c..264926efe193 100644 --- a/src/infra/outbound/message-action-params.attachments.test.ts +++ b/src/infra/outbound/message-action-params.attachments.test.ts @@ -29,6 +29,7 @@ const onePixelPng = Buffer.from( const onePixelPngBase64 = onePixelPng.toString("base64"); const wrappedOnePixelPngBase64 = onePixelPngBase64.match(/.{1,24}/g)?.join("\r\n") ?? ""; const parameterizedPngDataUrl = `data:image/png;charset=utf-8;name=../../ignored.svg;base64,${wrappedOnePixelPngBase64}`; +const csvBase64 = Buffer.from("name,value\nexample,1\n").toString("base64"); function firstMockArg( mock: { mock: { calls: readonly unknown[][] } }, @@ -106,10 +107,29 @@ describe("runMessageAction media behavior", () => { await resetMessageActionMediaMocks(); }); - it.each(["send", "sendAttachment", "reply", "upload-file", "setGroupIcon"] as const)( - "normalizes parameterized, line-wrapped image data URLs for %s", - async (action) => { - const args: Record = { buffer: parameterizedPngDataUrl }; + it.each( + (["send", "sendAttachment", "reply", "upload-file", "setGroupIcon"] as const).flatMap( + (action) => [ + { + action, + buffer: parameterizedPngDataUrl, + base64: onePixelPngBase64, + contentType: "image/png", + filename: "attachment.png", + }, + { + action, + buffer: `data:text/csv;base64,${csvBase64}`, + base64: csvBase64, + contentType: "text/csv", + filename: "attachment.csv", + }, + ], + ), + )( + "normalizes $contentType data URLs and infers filenames for $action", + async ({ action, buffer, base64, contentType, filename }) => { + const args: Record = { buffer }; await hydrateAttachmentParamsForAction({ cfg: {}, @@ -120,14 +140,13 @@ describe("runMessageAction media behavior", () => { mediaPolicy: { mode: "host" }, }); - expect(args.contentType).toBe("image/png"); + expect(args.contentType).toBe(contentType); + expect(args.filename).toBe(filename); if (action === "send") { expect(args.media).toBe("buffer://message-send/attachment"); - expect(args.filename).toBe("attachment.png"); } else { - expect(canonicalizeBase64(String(args.buffer))).toBe(onePixelPngBase64); + expect(canonicalizeBase64(String(args.buffer))).toBe(base64); } - expect(args.filename).not.toBe("../../ignored.svg"); }, ); @@ -329,6 +348,7 @@ describe("runMessageAction media behavior", () => { const payload = requireActionPayload(result); expect(payload.contentType).toBe("image/png"); + expect(payload.filename).toBe("attachment.png"); expect(canonicalizeBase64(String(payload.buffer))).toBe(onePixelPngBase64); }, ); @@ -617,6 +637,7 @@ describe("runMessageAction media behavior", () => { const handlerParams = firstMockArg(handleActionMock, "handleAction"); expect(handlerParams.contentType).toBe("image/png"); + expect(handlerParams.filename).toBe("attachment.png"); expect(canonicalizeBase64(String(handlerParams.buffer))).toBe(onePixelPngBase64); }); diff --git a/src/infra/outbound/message-action-params.ts b/src/infra/outbound/message-action-params.ts index 920ea77d6b1d..e19135cf293e 100644 --- a/src/infra/outbound/message-action-params.ts +++ b/src/infra/outbound/message-action-params.ts @@ -538,7 +538,7 @@ async function hydrateAttachmentPayload(params: { } else if (!filename) { params.args.filename = inferAttachmentFilename({ mediaHint: mediaSource, - contentType: contentTypeParam ?? undefined, + contentType: normalized.contentType, }); } }