diff --git a/src/adapter/aws-lambda/handler.ts b/src/adapter/aws-lambda/handler.ts index 4a258e9e08..caba081b3b 100644 --- a/src/adapter/aws-lambda/handler.ts +++ b/src/adapter/aws-lambda/handler.ts @@ -248,7 +248,7 @@ export const handle = ) => { - // @ts-expect-error FIXME: Fix return typing + // @ts-expect-error conditional return type is not inferable return async (event, lambdaContext?) => { const processor = getProcessor(event) diff --git a/src/middleware/combine/index.ts b/src/middleware/combine/index.ts index 1090e31e82..3b3c1d286e 100644 --- a/src/middleware/combine/index.ts +++ b/src/middleware/combine/index.ts @@ -128,7 +128,7 @@ export const every = (...middleware: (MiddlewareHandler | Condition)[]): Middlew * @example * ```ts * import { except } from 'hono/combine' - * import { bearerAuth } from 'hono/bearer-auth + * import { bearerAuth } from 'hono/bearer-auth' * * // If client is accessing public API, then skip authentication. * // Otherwise, require a valid token. diff --git a/src/request.test.ts b/src/request.test.ts index d9ae581c1b..8b894cba4e 100644 --- a/src/request.test.ts +++ b/src/request.test.ts @@ -381,6 +381,22 @@ describe('Body methods with caching', () => { expect(async () => await req.blob()).not.toThrow() }) + it('should parse form data even if formData() is called before parseBody()', async () => { + const data = new FormData() + data.append('foo', 'bar') + data.append('file', new File(['hello'], 't.txt', { type: 'text/plain' })) + const req = new HonoRequest( + new Request('http://localhost', { + method: 'POST', + body: data, + }) + ) + await req.formData() + const body = await req.parseBody() + expect(body['foo']).toBe('bar') + expect(body['file']).toBeInstanceOf(File) + }) + describe('should not break body methods after parseBody() with non-form content-type', () => { const createReq = () => new HonoRequest( @@ -540,6 +556,30 @@ describe('cloneRawRequest', () => { expect(req.raw, 'cloned request should contain the same properties').toMatchObject(clonedReq) }) + test('clones consumed multipart request with a matching boundary', async () => { + const data = new FormData() + data.append('foo', 'bar') + data.append('file', new File(['hello'], 't.txt', { type: 'text/plain' })) + const req = new HonoRequest( + new Request('http://localhost', { + method: 'POST', + body: data, + }) + ) + await req.formData() + + const clonedReq = await cloneRawRequest(req) + + const contentType = clonedReq.headers.get('Content-Type') ?? '' + const boundary = contentType.split('boundary=')[1] + const bodyText = await clonedReq.clone().text() + expect(bodyText.startsWith(`--${boundary}`)).toBe(true) + + const formData = await clonedReq.formData() + expect(formData.get('foo')).toBe('bar') + expect(formData.get('file')).toBeInstanceOf(File) + }) + test('clones GET request without body', async () => { const req = new HonoRequest( new Request('http://localhost', { diff --git a/src/request.ts b/src/request.ts index 41fde0d2b7..c3cfc32ba1 100644 --- a/src/request.ts +++ b/src/request.ts @@ -486,11 +486,19 @@ export const cloneRawRequest = async (req: HonoRequest): Promise => { }) } + const body = await req[cacheKey]() + const headers = req.header() + if (body instanceof FormData) { + // The FormData is re-serialized with a fresh multipart boundary, so the original + // Content-Type header no longer matches. Let the Request constructor generate it. + delete headers['content-type'] + } + const requestInit: RequiredRequestInit = { - body: await req[cacheKey](), + body, cache: req.raw.cache, credentials: req.raw.credentials, - headers: req.header(), + headers, integrity: req.raw.integrity, keepalive: req.raw.keepalive, method: req.method, diff --git a/src/utils/body.ts b/src/utils/body.ts index 96c801497f..62e3c11617 100644 --- a/src/utils/body.ts +++ b/src/utils/body.ts @@ -124,6 +124,12 @@ async function parseFormData( request: HonoRequest | Request, options: ParseBodyOptions ): Promise { + if (!isRawRequest(request) && request.bodyCache.formData) { + return convertFormDataToBodyData( + await (request.bodyCache.formData as FormData | Promise), + options + ) + } const headers = isRawRequest(request) ? request.headers : request.raw.headers const arrayBuffer = await (request as Request).arrayBuffer() const formDataPromise = bufferToFormData(arrayBuffer, headers.get('Content-Type') || '')