
























@@ -13,43 +13,64 @@ const ORIGINAL_PROXY_ENV = Object.fromEntries(
1313PROXY_ENV_KEYS.map((key) => [key, process.env[key]]),
1414) as Record<(typeof PROXY_ENV_KEYS)[number], string | undefined>;
151516-const { ProxyAgent, EnvHttpProxyAgent, undiciFetch, proxyAgentSpy, envAgentSpy, getLastAgent } =
17-vi.hoisted(() => {
18-const undiciFetch = vi.fn();
19-const proxyAgentSpy = vi.fn();
20-const envAgentSpy = vi.fn();
21-class ProxyAgent {
22-static lastCreated: ProxyAgent | undefined;
23-proxyUrl: string;
24-constructor(proxyUrl: string) {
25-this.proxyUrl = proxyUrl;
26-ProxyAgent.lastCreated = this;
27-proxyAgentSpy(proxyUrl);
28-}
16+const {
17+ ProxyAgent,
18+ EnvHttpProxyAgent,
19+ MockUndiciFormData,
20+ undiciFetch,
21+ proxyAgentSpy,
22+ envAgentSpy,
23+ getLastAgent,
24+} = vi.hoisted(() => {
25+const undiciFetch = vi.fn();
26+const proxyAgentSpy = vi.fn();
27+const envAgentSpy = vi.fn();
28+class MockUndiciFormData {
29+readonly [Symbol.toStringTag] = "FormData";
30+readonly entriesList: [string, unknown, string | undefined][] = [];
31+32+append(key: string, value: unknown, filename?: string): void {
33+this.entriesList.push([key, value, filename]);
34+}
35+36+get(key: string): unknown {
37+return this.entriesList.find(([entryKey]) => entryKey === key)?.[1] ?? null;
2938}
30-class EnvHttpProxyAgent {
31-static lastCreated: EnvHttpProxyAgent | undefined;
32-constructor(public readonly options?: Record<string, unknown>) {
33-EnvHttpProxyAgent.lastCreated = this;
34-envAgentSpy(options);
35-}
39+}
40+class ProxyAgent {
41+static lastCreated: ProxyAgent | undefined;
42+proxyUrl: string;
43+constructor(proxyUrl: string) {
44+this.proxyUrl = proxyUrl;
45+ProxyAgent.lastCreated = this;
46+proxyAgentSpy(proxyUrl);
47+}
48+}
49+class EnvHttpProxyAgent {
50+static lastCreated: EnvHttpProxyAgent | undefined;
51+constructor(public readonly options?: Record<string, unknown>) {
52+EnvHttpProxyAgent.lastCreated = this;
53+envAgentSpy(options);
3654}
55+}
375638-return {
39- ProxyAgent,
40- EnvHttpProxyAgent,
41- undiciFetch,
42- proxyAgentSpy,
43- envAgentSpy,
44-getLastAgent: () => ProxyAgent.lastCreated,
45-};
46-});
57+return {
58+ ProxyAgent,
59+ EnvHttpProxyAgent,
60+ MockUndiciFormData,
61+ undiciFetch,
62+ proxyAgentSpy,
63+ envAgentSpy,
64+getLastAgent: () => ProxyAgent.lastCreated,
65+};
66+});
47674868const mockedModuleIds = ["undici"] as const;
49695070vi.mock("undici", () => ({
5171 ProxyAgent,
5272 EnvHttpProxyAgent,
73+FormData: MockUndiciFormData,
5374fetch: undiciFetch,
5475}));
5576@@ -112,6 +133,84 @@ describe("makeProxyFetch", () => {
112133expect(proxyAgentSpy).toHaveBeenCalledOnce();
113134expect(secondDispatcher).toBe(firstDispatcher);
114135});
136+137+it("converts global FormData bodies before dispatching through undici", async () => {
138+undiciFetch.mockResolvedValue({ ok: true });
139+140+const proxyFetch = makeProxyFetch("http://proxy.test:8080");
141+const form = new globalThis.FormData();
142+form.append("model", "whisper-1");
143+form.append("file", new Blob([new Uint8Array(4)], { type: "audio/ogg" }), "voice.ogg");
144+145+await proxyFetch("https://api.example.com/v1/audio/transcriptions", {
146+method: "POST",
147+headers: {
148+"content-length": "999",
149+"content-type": "multipart/form-data; boundary=stale",
150+},
151+body: form,
152+});
153+154+const passedInit = undiciFetch.mock.calls[0]?.[1];
155+expect(passedInit?.body).toBeInstanceOf(MockUndiciFormData);
156+const passedBody = passedInit?.body as InstanceType<typeof MockUndiciFormData>;
157+expect(passedBody.get("model")).toBe("whisper-1");
158+expect(passedBody.get("file")).toBeInstanceOf(Blob);
159+expect(passedBody.entriesList.find(([key]) => key === "file")?.[2]).toBe("voice.ogg");
160+const sentHeaders = new Headers(passedInit?.headers);
161+expect(sentHeaders.has("content-length")).toBe(false);
162+expect(sentHeaders.has("content-type")).toBe(false);
163+});
164+165+it("keeps non-FormData bodies unchanged", async () => {
166+undiciFetch.mockResolvedValue({ ok: true });
167+168+const proxyFetch = makeProxyFetch("http://proxy.test:8080");
169+const body = JSON.stringify({ hello: "world" });
170+171+await proxyFetch("https://api.example.com/json", {
172+method: "POST",
173+ body,
174+});
175+176+expect(undiciFetch.mock.calls[0]?.[1]?.body).toBe(body);
177+});
178+179+it("keeps undici FormData instances unchanged", async () => {
180+undiciFetch.mockResolvedValue({ ok: true });
181+182+const proxyFetch = makeProxyFetch("http://proxy.test:8080");
183+const form = new MockUndiciFormData();
184+form.append("key", "value");
185+186+await proxyFetch("https://api.example.com/upload", {
187+method: "POST",
188+body: form as unknown as BodyInit,
189+});
190+191+expect(undiciFetch.mock.calls[0]?.[1]?.body).toBe(form);
192+});
193+194+it("converts FormData-like bodies from another implementation", async () => {
195+undiciFetch.mockResolvedValue({ ok: true });
196+197+const proxyFetch = makeProxyFetch("http://proxy.test:8080");
198+const formLike = {
199+[Symbol.toStringTag]: "FormData",
200+*entries(): IterableIterator<[string, FormDataEntryValue]> {
201+yield ["model", "whisper-1"];
202+},
203+};
204+205+await proxyFetch("https://api.example.com/upload", {
206+method: "POST",
207+body: formLike as unknown as BodyInit,
208+});
209+210+const passedInit = undiciFetch.mock.calls[0]?.[1];
211+expect(passedInit?.body).toBeInstanceOf(MockUndiciFormData);
212+expect(passedInit?.body.get("model")).toBe("whisper-1");
213+});
115214});
116215117216describe("getProxyUrlFromFetch", () => {
@@ -168,6 +267,30 @@ describe("resolveProxyFetchFromEnv", () => {
168267);
169268});
170269270+it("converts global FormData bodies when using proxy env fetch", async () => {
271+undiciFetch.mockResolvedValue({ ok: true });
272+273+const fetchFn = resolveProxyFetchFromEnv({
274+HTTP_PROXY: "",
275+HTTPS_PROXY: "http://proxy.test:8080",
276+});
277+expect(fetchFn).toBeDefined();
278+279+const form = new globalThis.FormData();
280+form.append("file", new Blob([new Uint8Array(8)], { type: "audio/wav" }), "test.wav");
281+form.append("model", "test-model");
282+283+await fetchFn!("https://api.example.com/v1/audio/transcriptions", {
284+method: "POST",
285+body: form,
286+});
287+288+const passedInit = undiciFetch.mock.calls[0]?.[1];
289+expect(passedInit?.body).toBeInstanceOf(MockUndiciFormData);
290+expect(passedInit?.body.get("model")).toBe("test-model");
291+expect(passedInit?.body.get("file")).toBeInstanceOf(Blob);
292+});
293+171294it("returns proxy fetch when HTTP_PROXY is set", () => {
172295const fetchFn = resolveProxyFetchFromEnv({
173296HTTPS_PROXY: "",
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。