diff --git a/client/src/lib/__tests__/proxyFetch.test.ts b/client/src/lib/__tests__/proxyFetch.test.ts index 5ac239eb9..886c85de7 100644 --- a/client/src/lib/__tests__/proxyFetch.test.ts +++ b/client/src/lib/__tests__/proxyFetch.test.ts @@ -241,4 +241,91 @@ describe("createProxyFetch", () => { const callBody = JSON.parse(mockFetch.mock.calls[0][1].body); expect(callBody.url).toBe("https://example.com/from-request"); }); + + it("preserves Request method, headers, and body when input is a Request", async () => { + mockFetch.mockResolvedValue({ + ok: true, + json: () => + Promise.resolve({ + ok: true, + status: 200, + statusText: "OK", + headers: {}, + body: "", + }), + }); + + const fetchFn = createProxyFetch(configWithProxy); + await fetchFn( + new Request("https://example.com/token", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "X-Test": "1", + }, + body: "grant_type=authorization_code&code=abc", + }), + ); + + const callBody = JSON.parse(mockFetch.mock.calls[0][1].body); + expect(callBody).toEqual({ + url: "https://example.com/token", + init: { + method: "POST", + headers: { + "content-type": "application/x-www-form-urlencoded", + "x-test": "1", + }, + body: "grant_type=authorization_code&code=abc", + }, + }); + }); + + it("lets an explicit init override method, headers, and body from a Request input", async () => { + mockFetch.mockResolvedValue({ + ok: true, + json: () => + Promise.resolve({ + ok: true, + status: 200, + statusText: "OK", + headers: {}, + body: "", + }), + }); + + const fetchFn = createProxyFetch(configWithProxy); + await fetchFn( + new Request("https://example.com/token", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "X-Test": "1", + }, + body: "grant_type=authorization_code&code=abc", + }), + { + method: "PUT", + headers: { + "X-Test": "override", + "X-Extra": "2", + }, + body: "override-body", + }, + ); + + const callBody = JSON.parse(mockFetch.mock.calls[0][1].body); + expect(callBody).toEqual({ + url: "https://example.com/token", + init: { + method: "PUT", + headers: { + "content-type": "application/x-www-form-urlencoded", + "x-test": "override", + "x-extra": "2", + }, + body: "override-body", + }, + }); + }); }); diff --git a/client/src/lib/proxyFetch.ts b/client/src/lib/proxyFetch.ts index a0ff5977f..c47b08e0b 100644 --- a/client/src/lib/proxyFetch.ts +++ b/client/src/lib/proxyFetch.ts @@ -92,26 +92,48 @@ export function createProxyFetch(config: InspectorConfig): typeof fetch { input: RequestInfo | URL, init?: RequestInit, ): Promise => { + const requestInput = input instanceof Request ? input : undefined; const url = typeof input === "string" ? input - : input instanceof Request - ? input.url + : requestInput + ? requestInput.url : input.toString(); // Serialize body for JSON transport. URLSearchParams and similar don't // JSON-serialize (they become {}), so we must convert to string first. let serializedBody: string | undefined; - if (init?.body != null) { - if (typeof init.body === "string") { - serializedBody = init.body; - } else if (init.body instanceof URLSearchParams) { - serializedBody = init.body.toString(); + const requestBody = + requestInput && + !requestInput.bodyUsed && + !["GET", "HEAD"].includes(requestInput.method) + ? await requestInput.clone().text() + : undefined; + const effectiveBody = init?.body ?? requestBody; + if (effectiveBody != null) { + if (typeof effectiveBody === "string") { + serializedBody = effectiveBody; + } else if (effectiveBody instanceof URLSearchParams) { + serializedBody = effectiveBody.toString(); } else { - serializedBody = String(init.body); + serializedBody = String(effectiveBody); } } + const forwardedHeaders = new Headers(requestInput?.headers); + if (init?.headers) { + new Headers(init.headers).forEach((value, key) => { + forwardedHeaders.set(key, value); + }); + } + const serializedHeadersObject = Object.fromEntries( + forwardedHeaders.entries(), + ); + const serializedHeaders = + Object.keys(serializedHeadersObject).length > 0 + ? serializedHeadersObject + : undefined; + const proxyResponse = await fetch(`${proxyAddress}/fetch`, { method: "POST", headers: { @@ -121,10 +143,8 @@ export function createProxyFetch(config: InspectorConfig): typeof fetch { body: JSON.stringify({ url, init: { - method: init?.method, - headers: init?.headers - ? Object.fromEntries(new Headers(init.headers)) - : undefined, + method: init?.method ?? requestInput?.method, + headers: serializedHeaders, body: serializedBody, }, }),