perf: avoid decoding attachment buffers during send preflight (#147879)

This commit is contained in:
Peter Steinberger 2026-09-13 23:23:55 -07:00 • committed by GitHub
parent b790d46d04
commit e89c182094
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 49 additions and 27 deletions

View file

@ -794,10 +794,35 @@ describe("message action media helpers", () => {
it.each(
["dry-run", "preserve-buffer"].flatMap((mode) => [
{ mode, buffer: "SGVsbG8=", name: "raw base64" },
{ mode, buffer: "data:application/octet-stream;base64,SGVsbG8=", name: "data URL" },
{ mode, buffer: "SGVsbG8=", name: "raw base64", expectedError: undefined },
{
mode,
buffer: "data:application/octet-stream;base64,SGVsbG8=",
name: "data URL",
expectedError: undefined,
},
{ mode, buffer: "SGVsbG8", name: "unpadded base64", expectedError: undefined },
{ mode, buffer: " SGV s bG8= \n", name: "whitespace base64", expectedError: undefined },
{
mode,
buffer: "SGVsbG8h",
name: "one byte over the limit",
expectedError: "Media too large: 6 bytes (limit: 5 bytes)",
},
{
mode,
buffer: "!!!!!!!!",
name: "oversized malformed base64",
expectedError: "Media too large: 6 bytes (limit: 5 bytes)",
},
{
mode,
buffer: " \t\r\n",
name: "whitespace-only base64",
expectedError: "message.send buffer has invalid base64 data",
},
]),
)("keeps explicit MIME for $mode $name without staging", async ({ mode, buffer }) => {
)("validates $mode $name without staging", async ({ mode, buffer, expectedError }) => {
await withTempOpenClawStateDir(async (stateDir) => {
const args: Record<string, unknown> = {
buffer,
@ -805,8 +830,8 @@ describe("message action media helpers", () => {
mimeType: "text/plain",
};
await hydrateAttachmentParamsForAction({
cfg,
const hydration = hydrateAttachmentParamsForAction({
cfg: { agents: { defaults: { mediaMaxMb: 5 / (1024 * 1024) } } },
channel: "imessage",
args,
action: "send",
@ -815,12 +840,18 @@ describe("message action media helpers", () => {
mediaPolicy: { mode: "host" },
});
expect(args.media).toBe("buffer://message-send/attachment");
expect(args.mediaUrl).toBe("buffer://message-send/attachment");
expect(args.mediaUrls).toEqual(["buffer://message-send/attachment"]);
expect(args.buffer).toBe(mode === "preserve-buffer" ? buffer : undefined);
expect(args.contentType).toBe("text/plain");
expect(args.filename).toBe("preview.txt");
if (expectedError) {
await expect(hydration).rejects.toThrow(expectedError);
expect(args).toEqual({ buffer, filename: "preview.txt", mimeType: "text/plain" });
} else {
await hydration;
expect(args.media).toBe("buffer://message-send/attachment");
expect(args.mediaUrl).toBe("buffer://message-send/attachment");
expect(args.mediaUrls).toEqual(["buffer://message-send/attachment"]);
expect(args.buffer).toBe(mode === "preserve-buffer" ? buffer : undefined);
expect(args.contentType).toBe("text/plain");
expect(args.filename).toBe("preview.txt");
}
await expect(fs.readdir(path.join(stateDir, "media", "outbound"))).rejects.toThrow();
});
});

View file

@ -298,7 +298,7 @@ function resolveSendBufferMaxBytes(params: {
);
}
function decodeBoundedBase64Attachment(params: { base64: string; maxBytes: number }): Buffer {
function validateBoundedBase64Attachment(params: { base64: string; maxBytes: number }): string {
const estimatedBytes = estimateBase64DecodedBytes(params.base64);
if (estimatedBytes > params.maxBytes) {
throw new Error(`Media too large: ${estimatedBytes} bytes (limit: ${params.maxBytes} bytes)`);
@ -307,13 +307,7 @@ function decodeBoundedBase64Attachment(params: { base64: string; maxBytes: numbe
if (!canonicalBase64) {
throw new Error("message.send buffer has invalid base64 data");
}
const buffer = Buffer.from(canonicalBase64, "base64");
if (buffer.byteLength > params.maxBytes) {
throw new Error(
`Media too large: ${buffer.byteLength} bytes (limit: ${params.maxBytes} bytes)`,
);
}
return buffer;
return canonicalBase64;
}
async function hydrateSendBufferMediaParams(params: {
@ -348,11 +342,11 @@ async function hydrateSendBufferMediaParams(params: {
contentType: normalized.contentType,
});
const maxBytes = resolveSendBufferMaxBytes(params);
const canonicalBase64 = validateBoundedBase64Attachment({
base64: normalized.base64,
maxBytes,
});
if (params.dryRun || params.preserveBuffer) {
decodeBoundedBase64Attachment({
base64: normalized.base64,
maxBytes,
});
params.args.media = SEND_BUFFER_DRY_RUN_MEDIA_URL;
params.args.mediaUrl = SEND_BUFFER_DRY_RUN_MEDIA_URL;
params.args.mediaUrls = [SEND_BUFFER_DRY_RUN_MEDIA_URL];
@ -368,10 +362,7 @@ async function hydrateSendBufferMediaParams(params: {
return;
}
const staged = await resolveOutboundAttachmentFromBuffer(
decodeBoundedBase64Attachment({
base64: normalized.base64,
maxBytes,
}),
Buffer.from(canonicalBase64, "base64"),
maxBytes,
{
contentType: normalized.contentType,