diff --git a/extensions/discord/src/client.proxy.test.ts b/extensions/discord/src/client.proxy.test.ts index 3d5e583547d..bf6dbf2a977 100644 --- a/extensions/discord/src/client.proxy.test.ts +++ b/extensions/discord/src/client.proxy.test.ts @@ -1,6 +1,9 @@ +import http from "node:http"; +import { fetch as undiciFetch } from "undici"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../../src/config/config.js"; import { createDiscordRestClient } from "./client.js"; +import { createDiscordRequestClient } from "./proxy-request-client.js"; const makeProxyFetchMock = vi.hoisted(() => vi.fn()); @@ -118,4 +121,56 @@ describe("createDiscordRestClient proxy support", () => { expect(makeProxyFetchMock).toHaveBeenCalledWith("http://[::1]:8080"); expect(requestClient.options?.fetch).toEqual(expect.any(Function)); }); + + it("serializes multipart media with undici-compatible FormData for proxy fetches", async () => { + const received = await new Promise<{ + contentType: string | undefined; + body: string; + }>((resolve, reject) => { + const server = http.createServer((req, res) => { + const chunks: Buffer[] = []; + req.on("data", (chunk: Buffer) => chunks.push(chunk)); + req.on("error", reject); + req.on("end", () => { + resolve({ + contentType: req.headers["content-type"], + body: Buffer.concat(chunks).toString("utf8"), + }); + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify({ id: "message-id", channel_id: "channel-id" })); + server.close(); + }); + }); + server.on("error", reject); + server.listen(0, "127.0.0.1", () => { + const address = server.address(); + if (!address || typeof address === "string") { + reject(new Error("failed to bind test server")); + server.close(); + return; + } + const rest = createDiscordRequestClient("test-token", { + baseUrl: `http://127.0.0.1:${address.port}`, + fetch: undiciFetch as typeof fetch, + queueRequests: false, + }); + void rest + .post("/channels/123/messages", { + body: { + content: "with image", + files: [{ data: Buffer.from("png-data"), name: "image.png" }], + }, + }) + .catch((err: unknown) => { + reject(err); + server.close(); + }); + }); + }); + + expect(received.contentType).toMatch(/^multipart\/form-data; boundary=/); + expect(received.body).toContain('name="files[0]"; filename="image.png"'); + expect(received.body).toContain('name="payload_json"'); + expect(received.body).toContain('"attachments":[{"id":0,"filename":"image.png"}]'); + }); }); diff --git a/extensions/discord/src/proxy-request-client.ts b/extensions/discord/src/proxy-request-client.ts index bd68846dd3f..a78bc25aae2 100644 --- a/extensions/discord/src/proxy-request-client.ts +++ b/extensions/discord/src/proxy-request-client.ts @@ -7,6 +7,7 @@ import { type RequestClientOptions, } from "@buape/carbon"; import { isRecord } from "openclaw/plugin-sdk/text-runtime"; +import { FormData as UndiciFormData } from "undici"; export type ProxyRequestClientOptions = RequestClientOptions & { fetch?: typeof fetch; @@ -281,7 +282,7 @@ class ProxyRequestClientCompat { typeof payload === "string" ? { content: payload, attachments: [] } : { ...payload, attachments: [] }; - const formData = new FormData(); + const formData = new UndiciFormData(); const files = getMultipartFiles(payload); for (const [index, file] of files.entries()) {