diff --git a/src/utils/proxy.ts b/src/utils/proxy.ts index 1a5ca10b..0a3884b4 100644 --- a/src/utils/proxy.ts +++ b/src/utils/proxy.ts @@ -1,15 +1,18 @@ import type { H3Event } from "../event"; import type { H3EventContext, RequestHeaders } from "../types"; import { getMethod, getRequestHeaders } from "./request"; -import { readRawBody } from "./body"; import { splitCookiesString } from "./cookie"; import { sanitizeStatusMessage, sanitizeStatusCode } from "./sanitize"; +import { readRawBody } from "./body"; + +export type Duplex = "half" | "full"; export interface ProxyOptions { headers?: RequestHeaders | HeadersInit; - fetchOptions?: RequestInit; + fetchOptions?: RequestInit & { duplex?: Duplex }; fetch?: typeof fetch; sendStream?: boolean; + streamRequest?: boolean; cookieDomainRewrite?: string | Record; cookiePathRewrite?: string | Record; onResponse?: (event: H3Event, response: Response) => void; @@ -35,8 +38,14 @@ export async function proxyRequest( // Body let body; + let duplex: Duplex | undefined; if (PayloadMethods.has(method)) { - body = await readRawBody(event, false).catch(() => undefined); + if (opts.streamRequest) { + body = event.body; + duplex = "half"; + } else { + body = await readRawBody(event, false).catch(() => undefined); + } } // Headers @@ -54,6 +63,7 @@ export async function proxyRequest( headers, method, body, + duplex, ...opts.fetchOptions, }, }); diff --git a/test/proxy.test.ts b/test/proxy.test.ts index 78a51200..0c67e97b 100644 --- a/test/proxy.test.ts +++ b/test/proxy.test.ts @@ -146,6 +146,60 @@ describe("", () => { expect(resBody.headers["x-req-header"]).toEqual("works"); expect(resBody.bytes).toEqual(dummyFile.length); }); + + it("can proxy stream request", async () => { + app.use( + "/debug", + eventHandler(async (event) => { + return { + body: await readRawBody(event), + headers: getHeaders(event), + }; + }) + ); + + app.use( + "/", + eventHandler((event) => { + return proxyRequest(event, url + "/debug", { fetch }); + }) + ); + + const isNode16 = process.version.startsWith("v16."); + const body = isNode16 + ? "This is a streamed request." + : new ReadableStream({ + start(controller) { + controller.enqueue("This "); + controller.enqueue("is "); + controller.enqueue("a "); + controller.enqueue("streamed "); + controller.enqueue("request."); + controller.close(); + }, + }).pipeThrough(new TextEncoderStream()); + + const res = await fetch(url + "/", { + method: "POST", + // @ts-ignore + duplex: "half", + body, + headers: { + "content-type": "application/octet-stream", + "x-custom": "hello", + "content-length": "27", + }, + }); + const resBody = await res.json(); + + expect(resBody.headers["content-type"]).toEqual( + "application/octet-stream" + ); + expect(resBody.headers["x-custom"]).toEqual("hello"); + expect(resBody.body).toMatchInlineSnapshot( + '"This is a streamed request."' + ); + }); }); describe("multipleCookies", () => {