From d33e9d5c2ac22ec79beb93e3c4018020f5124fe0 Mon Sep 17 00:00:00 2001 From: Bodo Graumann Date: Thu, 24 Oct 2024 22:49:58 +0200 Subject: [PATCH] fix: allow proxy middleware to reuse body from memo (#679) Resolves #642 --- body.test.ts | 16 ++++++++++++++++ body.ts | 8 ++++++++ middleware/proxy.test.ts | 25 +++++++++++++++++++++++++ middleware/proxy.ts | 15 +-------------- testing.ts | 5 +++++ 5 files changed, 55 insertions(+), 14 deletions(-) diff --git a/body.test.ts b/body.test.ts index 598d804..e3a3830 100644 --- a/body.test.ts +++ b/body.test.ts @@ -467,3 +467,19 @@ Deno.test({ assert(timingSafeEqual(actual, expected)); }, }); + +Deno.test({ + name: "body - empty", + async fn() { + const body = new Body(nativeToServer( + new Request( + "http://localhost/index.html", + { + method: "GET", + }, + ), + )); + assert(!body.has); + assertEquals(await body.init(), null); + }, +}); diff --git a/body.ts b/body.ts index 376a633..5a94426 100644 --- a/body.ts +++ b/body.ts @@ -89,6 +89,14 @@ export class Body { return this.#request?.bodyUsed ?? !!this.#used; } + /** Return the body to be reused as BodyInit. */ + async init(): Promise { + if (!this.has) { + return null; + } + return await this.#memo ?? this.stream; + } + /** Reads a body to the end and resolves with the value as an * {@linkcode ArrayBuffer} */ async arrayBuffer(): Promise { diff --git a/middleware/proxy.test.ts b/middleware/proxy.test.ts index d39d379..0ed3876 100644 --- a/middleware/proxy.test.ts +++ b/middleware/proxy.test.ts @@ -266,3 +266,28 @@ Deno.test({ await mw(ctx, next); }, }); + +Deno.test({ + name: "proxy - consumed body", + async fn() { + async function fetch(request: Request): Promise { + const body = await request.text(); + assertEquals(body, "hello world"); + return new Response(body); + } + + const mw = proxy("https://oakserver.github.io/", { fetch }); + const stream = ReadableStream.from([ + new TextEncoder().encode("hello world"), + ]); + const ctx = createMockContext({ + method: "POST", + path: "/oak/FAQ", + body: stream, + }); + const next = createMockNext(); + + assertEquals(await ctx.request.body.text(), "hello world"); + await mw(ctx, next); + }, +}); diff --git a/middleware/proxy.ts b/middleware/proxy.ts index 0935257..4a62507 100644 --- a/middleware/proxy.ts +++ b/middleware/proxy.ts @@ -152,7 +152,7 @@ async function createRequest< } url.search = ctx.request.url.search; - const body = getBodyInit(ctx); + const body = await ctx.request.body?.init() ?? null; const headers = new Headers(ctx.request.headers); if (optHeaders) { if (typeof optHeaders === "function") { @@ -195,19 +195,6 @@ async function createRequest< return request; } -function getBodyInit< - R extends string, - P extends RouteParams, - S extends State, ->( - ctx: Context | RouterContext, -): BodyInit | null { - if (!ctx.request.hasBody) { - return null; - } - return ctx.request.body.stream; -} - function iterableHeaders( headers: HeadersInit, ): IterableIterator<[string, string]> { diff --git a/testing.ts b/testing.ts index 7e23b83..0e8aa41 100644 --- a/testing.ts +++ b/testing.ts @@ -16,6 +16,7 @@ import { type ErrorStatus, SecureCookieMap, } from "./deps.ts"; +import { Body } from "./body.ts"; import type { RouteParams, RouterContext } from "./router.ts"; import type { Request } from "./request.ts"; import { Response } from "./response.ts"; @@ -67,6 +68,7 @@ export interface MockContextOptions< path?: string; state?: S; headers?: [string, string][]; + body?: ReadableStream; } /** Allows external parties to modify the context state. */ @@ -90,6 +92,7 @@ export function createMockContext< state, app = createMockApp(state), headers: requestHeaders, + body = undefined, }: MockContextOptions = {}, ): RouterContext { function createMockRequest(): Request { @@ -120,6 +123,8 @@ export function createMockContext< search: undefined, searchParams: new URLSearchParams(), url: new URL(path, "http://localhost/"), + hasBody: !!body, + body: body ? new Body({ headers, getBody: () => body }) : undefined, } as any; }