From 56e472169161d286ac37bc91722c6d36a382d553 Mon Sep 17 00:00:00 2001 From: Markus Blomqvist Date: Wed, 27 Mar 2024 00:59:32 +0200 Subject: [PATCH] Improve RPC route response serialization This change serializes Blob data for API routes and creates a JSON object from form responses, although these are rare cases. --- .../src/pages/api/v1/rpc/[operationId].ts | 8 +++++ .../src/app-router/rpc-route.ts | 20 ++++++------ .../src/pages-router/rpc-api-route.ts | 32 +++++++++++++++++-- .../next-rest-framework/src/shared/utils.ts | 14 ++++++++ 4 files changed, 60 insertions(+), 14 deletions(-) diff --git a/apps/example/src/pages/api/v1/rpc/[operationId].ts b/apps/example/src/pages/api/v1/rpc/[operationId].ts index 43078ae..006df1d 100644 --- a/apps/example/src/pages/api/v1/rpc/[operationId].ts +++ b/apps/example/src/pages/api/v1/rpc/[operationId].ts @@ -8,6 +8,14 @@ import { } from '@/actions'; import { rpcApiRoute } from 'next-rest-framework'; +// Body parser must be disabled when parsing multipart/form-data requests with pages router. +// A recommended way is to create a separate RPC API route for multipart/form-data requests. +// export const config = { +// api: { +// bodyParser: false +// } +// }; + const handler = rpcApiRoute({ getTodos, getTodoById, diff --git a/packages/next-rest-framework/src/app-router/rpc-route.ts b/packages/next-rest-framework/src/app-router/rpc-route.ts index 92ab62d..1bdf871 100644 --- a/packages/next-rest-framework/src/app-router/rpc-route.ts +++ b/packages/next-rest-framework/src/app-router/rpc-route.ts @@ -7,7 +7,8 @@ import { import { validateSchema, logNextRestFrameworkError, - type RpcOperationDefinition + type RpcOperationDefinition, + parseRpcOperationResponseJson } from '../shared'; import { type FormDataContentType, @@ -194,21 +195,18 @@ export const rpcRoute = < ); } - const parseRes = (res: unknown): BodyInit => { - if ( - res instanceof ReadableStream || - res instanceof ArrayBuffer || - res instanceof Blob || - res instanceof FormData || - res instanceof URLSearchParams - ) { + const parseRes = async (res: unknown): Promise => { + if (res instanceof ReadableStream || res instanceof Blob) { return res; } - return JSON.stringify(res); + const parsed = await parseRpcOperationResponseJson(res); + return JSON.stringify(parsed); }; - return new NextResponse(parseRes(res), { + const json = await parseRes(res); + + return new NextResponse(json, { status: 200, headers: { 'Content-Type': 'application/json' diff --git a/packages/next-rest-framework/src/pages-router/rpc-api-route.ts b/packages/next-rest-framework/src/pages-router/rpc-api-route.ts index f64389f..b9e3bf0 100644 --- a/packages/next-rest-framework/src/pages-router/rpc-api-route.ts +++ b/packages/next-rest-framework/src/pages-router/rpc-api-route.ts @@ -7,7 +7,8 @@ import { validateSchema, logNextRestFrameworkError, type RpcOperationDefinition, - logPagesEdgeRuntimeErrorForRoute + logPagesEdgeRuntimeErrorForRoute, + parseRpcOperationResponseJson } from '../shared'; import { type NextApiRequest, type NextApiResponse } from 'next/types'; import { @@ -117,7 +118,7 @@ export const rpcApiRoute = < // Parse multipart/form-data into a FormData object. try { req.body = await parseMultiPartFormData(req); - } catch (e) { + } catch { res.status(400).json({ message: `${DEFAULT_ERRORS.invalidRequestBody} Failed to parse form data.` }); @@ -166,7 +167,32 @@ export const rpcApiRoute = < return; } - res.status(200).json(_res); + if (_res instanceof Blob) { + const reader = _res.stream().getReader(); + res.setHeader('Content-Type', 'application/octet-stream'); + + res.setHeader( + 'Content-Disposition', + `attachment; filename="${_res.name}"` + ); + + const pump = async () => { + await reader.read().then(async ({ done, value }) => { + if (done) { + res.end(); + return; + } + + res.write(value); + await pump(); + }); + }; + + await pump(); + } + + const json = await parseRpcOperationResponseJson(_res); + res.status(200).json(json); } catch (error) { logNextRestFrameworkError(error); res.status(400).json({ message: DEFAULT_ERRORS.unexpectedError }); diff --git a/packages/next-rest-framework/src/shared/utils.ts b/packages/next-rest-framework/src/shared/utils.ts index b0fa235..ba24dbc 100644 --- a/packages/next-rest-framework/src/shared/utils.ts +++ b/packages/next-rest-framework/src/shared/utils.ts @@ -5,3 +5,17 @@ export const isValidMethod = (x: unknown): x is ValidMethod => export const capitalizeFirstLetter = (str: string) => str[0]?.toUpperCase() + str.slice(1); + +export const parseRpcOperationResponseJson = async (res: unknown) => { + if (res instanceof FormData || res instanceof URLSearchParams) { + const body: Record = {}; + + for (const [key, value] of res.entries()) { + body[key] = value; + } + + return body; + } + + return res; +};