Skip to content

Commit ce7f899

Browse files
committed
fix(discord): bound voice upload error bodies
1 parent 4c3b15b commit ce7f899

2 files changed

Lines changed: 80 additions & 1 deletion

File tree

extensions/discord/src/voice-message.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,28 @@ vi.mock("openclaw/plugin-sdk/ssrf-runtime", async () => {
5252
let ensureOggOpus: typeof import("./voice-message.js").ensureOggOpus;
5353
let sendDiscordVoiceMessage: typeof import("./voice-message.js").sendDiscordVoiceMessage;
5454

55+
function cancelTrackedResponse(
56+
text: string,
57+
init: ResponseInit,
58+
): {
59+
response: Response;
60+
wasCanceled: () => boolean;
61+
} {
62+
let canceled = false;
63+
const stream = new ReadableStream<Uint8Array>({
64+
start(controller) {
65+
controller.enqueue(new TextEncoder().encode(text));
66+
},
67+
cancel() {
68+
canceled = true;
69+
},
70+
});
71+
return {
72+
response: new Response(stream, init),
73+
wasCanceled: () => canceled,
74+
};
75+
}
76+
5577
describe("ensureOggOpus", () => {
5678
beforeAll(async () => {
5779
({ ensureOggOpus, sendDiscordVoiceMessage } = await import("./voice-message.js"));
@@ -374,4 +396,57 @@ describe("sendDiscordVoiceMessage", () => {
374396
message: "cdn unavailable",
375397
});
376398
});
399+
400+
it("bounds voice upload error bodies without using response.text()", async () => {
401+
const rest = createRest();
402+
const tracked = cancelTrackedResponse(`${"cdn unavailable ".repeat(1024)}tail`, {
403+
status: 503,
404+
headers: { "content-type": "text/plain" },
405+
});
406+
const textSpy = vi.spyOn(tracked.response, "text").mockRejectedValue(new Error("unbounded"));
407+
vi.spyOn(globalThis, "fetch").mockImplementation(async (input, init) => {
408+
const url = input instanceof Request ? input.url : String(input);
409+
const method = input instanceof Request ? input.method : (init?.method ?? "GET");
410+
if (method === "POST" && url.endsWith("/channels/channel-1/attachments")) {
411+
return new Response(
412+
JSON.stringify({
413+
attachments: [
414+
{
415+
id: 0,
416+
upload_url: "https://cdn.test/upload",
417+
upload_filename: "uploaded.ogg",
418+
},
419+
],
420+
}),
421+
{ status: 200 },
422+
);
423+
}
424+
if (method === "PUT" && url === "https://cdn.test/upload") {
425+
return tracked.response;
426+
}
427+
throw new Error(`unexpected fetch ${method} ${url}`);
428+
});
429+
430+
let error: unknown;
431+
try {
432+
await sendDiscordVoiceMessage(
433+
rest,
434+
"channel-1",
435+
Buffer.from("ogg"),
436+
metadata,
437+
undefined,
438+
async (fn) => await fn(),
439+
false,
440+
"bot-token",
441+
);
442+
} catch (caught) {
443+
error = caught;
444+
}
445+
expect(error).toBeInstanceOf(Error);
446+
expect((error as Error).name).toBe("DiscordError");
447+
expect((error as Error).message).toContain("cdn unavailable");
448+
expect(JSON.stringify((error as { rawBody?: unknown }).rawBody)).not.toContain("tail");
449+
expect(tracked.wasCanceled()).toBe(true);
450+
expect(textSpy).not.toHaveBeenCalled();
451+
});
377452
});

extensions/discord/src/voice-message.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
import { MEDIA_FFMPEG_MAX_AUDIO_DURATION_SECS } from "openclaw/plugin-sdk/media-runtime";
2323
import { unlinkIfExists } from "openclaw/plugin-sdk/media-runtime";
2424
import { parseStrictFiniteNumber } from "openclaw/plugin-sdk/number-runtime";
25+
import { readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
2526
import type { RetryRunner } from "openclaw/plugin-sdk/retry-runtime";
2627
import { writeExternalFileWithinRoot } from "openclaw/plugin-sdk/security-runtime";
2728
import { fetchWithSsrFGuard, type SsrFPolicy } from "openclaw/plugin-sdk/ssrf-runtime";
@@ -34,6 +35,7 @@ const DISCORD_VOICE_MESSAGE_FLAG = 1 << 13;
3435
const SUPPRESS_NOTIFICATIONS_FLAG = 1 << 12;
3536
const WAVEFORM_SAMPLES = 256;
3637
const DISCORD_OPUS_SAMPLE_RATE_HZ = 48_000;
38+
const DISCORD_VOICE_ERROR_BODY_LIMIT_BYTES = 8 * 1024;
3739
const DISCORD_VOICE_UPLOAD_SSRF_POLICY: SsrFPolicy = {
3840
allowRfc2544BenchmarkRange: true,
3941
allowIpv6UniqueLocalRange: true,
@@ -299,7 +301,9 @@ async function createVoiceRequestError(
299301
response: Response,
300302
fallbackMessage: string,
301303
): Promise<Error> {
302-
const raw = await response.text().catch(() => "");
304+
const raw = await readResponseTextLimited(response, DISCORD_VOICE_ERROR_BODY_LIMIT_BYTES).catch(
305+
() => "",
306+
);
303307
const parsed = coerceDiscordErrorBody(raw);
304308
if (response.status === 429) {
305309
throw createRateLimitError(response, {

0 commit comments

Comments
 (0)