@@ -44,7 +44,7 @@ Then define the API route: | |||
import * as Iceform from '@modal-sh/iceform-next'; | |||
import { greet } from '@/handlers/greet'; | |||
const handler = Iceform.action.wrapApiHandler(greet); | |||
const handler = Iceform.action.wrapApiHandler({ fn: greet }); | |||
// you can extend the route config by passing an extra argument | |||
export const config = Iceform.action.getApiConfig(); | |||
@@ -66,11 +66,11 @@ const ActionGreetPage: NextPage = () => null; | |||
export default ActionGreetPage; | |||
export const getServerSideProps = Iceform.action.getServerSideProps(greet); | |||
export const getServerSideProps = Iceform.action.getServerSideProps({ fn: greet }); | |||
``` | |||
Lastly, define the form page: | |||
```ts | |||
```tsx | |||
// [src/]pages/form.tsx | |||
import * as React from 'react'; | |||
@@ -125,4 +125,7 @@ In theory, any API route may have a corresponding action route. | |||
- [X] Content negotiation (custom request data) | |||
- [ ] Tests | |||
- [ ] Form with redirects | |||
- [ ] Form with files | |||
- [ ] Documentation | |||
- [ ] Remix support |
@@ -6,4 +6,4 @@ const ActionGreetPage: NextPage = () => null; | |||
export default ActionGreetPage; | |||
export const getServerSideProps = Iceform.action.getServerSideProps(greet); | |||
export const getServerSideProps = Iceform.action.getServerSideProps({ fn: greet }); |
@@ -1,7 +1,7 @@ | |||
import * as Iceform from '@modal-sh/iceform-next'; | |||
import { greet } from '@/handlers/greet'; | |||
const handler = Iceform.action.wrapApiHandler(greet); | |||
const handler = Iceform.action.wrapApiHandler({ fn: greet }); | |||
export const config = Iceform.action.getApiConfig(); | |||
@@ -1,5 +1,4 @@ | |||
import * as React from 'react'; | |||
import { getFormValues } from '@theoryofnekomata/formxtra'; | |||
import fetchPonyfill from 'fetch-ponyfill'; | |||
import { NextPage as DefaultNextPage } from 'next'; | |||
import { | |||
@@ -7,35 +6,41 @@ import { | |||
NextApiRequest, | |||
ENCTYPE_APPLICATION_JSON, | |||
ENCTYPE_MULTIPART_FORM_DATA, | |||
ENCTYPE_X_WWW_FORM_URLENCODED, | |||
} from './common'; | |||
import { | |||
DEFAULT_ENCTYPE_SERIALIZERS, | |||
EncTypeSerializerMap, serializeBody, | |||
SerializerOptions, | |||
} from './utils/serialization'; | |||
const FormDerivedElementComponent = 'form' as const; | |||
type FormDerivedElement = HTMLElementTagNameMap[typeof FormDerivedElementComponent]; | |||
type BaseProps = React.HTMLProps<FormDerivedElement>; | |||
type EncTypeSerializer = (data: unknown) => string; | |||
const ALLOWED_SERVER_METHODS = ['get', 'post'] as const; | |||
type EncTypeSerializerMap = Record<string, EncTypeSerializer>; | |||
type AllowedServerMethod = typeof ALLOWED_SERVER_METHODS[number]; | |||
const DEFAULT_ENCTYPE_SERIALIZERS: EncTypeSerializerMap = { | |||
[ENCTYPE_APPLICATION_JSON]: (data: unknown) => JSON.stringify(data), | |||
}; | |||
const ALLOWED_CLIENT_METHODS = [ | |||
...ALLOWED_SERVER_METHODS, | |||
'put', | |||
'patch', | |||
'delete', | |||
] as const; | |||
type GetFormValuesOptions = Parameters<typeof getFormValues>[1]; | |||
type AllowedClientMethod = typeof ALLOWED_CLIENT_METHODS[number]; | |||
export interface FormProps extends Omit<BaseProps, 'action'> { | |||
export interface FormProps extends Omit<React.HTMLProps<FormDerivedElement>, 'action' | 'method'> { | |||
action?: string; | |||
method?: AllowedServerMethod; | |||
clientAction?: string; | |||
clientHeaders?: HeadersInit; | |||
clientMethod?: BaseProps['method']; | |||
clientMethod?: AllowedClientMethod; | |||
invalidate?: (...args: unknown[]) => unknown; | |||
refresh?: (response: Response) => void; | |||
encTypeSerializers?: EncTypeSerializerMap; | |||
responseEncType?: string; | |||
serializerOptions?: GetFormValuesOptions; | |||
serializerOptions?: SerializerOptions; | |||
} | |||
export const useResponse = (res: NextApiResponse) => { | |||
@@ -62,40 +67,6 @@ export const useResponse = (res: NextApiResponse) => { | |||
]); | |||
}; | |||
interface SerializeBodyParams { | |||
form: HTMLFormElement, | |||
encType: string, | |||
serializers: EncTypeSerializerMap, | |||
options?: GetFormValuesOptions, | |||
} | |||
const serializeBody = (params: SerializeBodyParams) => { | |||
const { | |||
form, | |||
encType, | |||
serializers, | |||
options, | |||
} = params; | |||
if (encType === ENCTYPE_MULTIPART_FORM_DATA) { | |||
// type error when provided a submitter element for some reason... | |||
const FormDataUnknown = FormData as unknown as { | |||
new(formElement?: HTMLElement, submitter?: HTMLElement): BodyInit; | |||
}; | |||
return new FormDataUnknown(form, options?.submitter); | |||
} | |||
if (encType === ENCTYPE_X_WWW_FORM_URLENCODED) { | |||
return new URLSearchParams(form); | |||
} | |||
if (typeof serializers[encType] === 'function') { | |||
return serializers[encType](getFormValues(form, options)); | |||
} | |||
throw new Error(`Unsupported encType: ${encType}`); | |||
}; | |||
export const Form = React.forwardRef<FormDerivedElement, FormProps>(({ | |||
children, | |||
onSubmit, | |||
@@ -151,10 +122,6 @@ export const Form = React.forwardRef<FormDerivedElement, FormProps>(({ | |||
onSubmit?.(event); | |||
}; | |||
// TODO how to display put/patch method in form? HTML only supports get/post | |||
// > throw error when not get/post | |||
// TODO handle "dialog" method as invalid | |||
const serverMethodRaw = method.toLowerCase(); | |||
const serverMethod = serverMethodRaw === 'get' ? 'get' : 'post'; | |||
@@ -181,6 +148,7 @@ Form.defaultProps = { | |||
clientMethod: undefined, | |||
encTypeSerializers: DEFAULT_ENCTYPE_SERIALIZERS, | |||
invalidate: undefined, | |||
method: 'get' as const, | |||
refresh: undefined, | |||
responseEncType: ENCTYPE_APPLICATION_JSON, | |||
serializerOptions: undefined, | |||
@@ -5,54 +5,22 @@ import { | |||
NextApiResponse as DefaultNextApiResponse, | |||
PageConfig, | |||
} from 'next'; | |||
import * as nookies from 'nookies'; | |||
import { IncomingMessage } from 'http'; | |||
import busboy from 'busboy'; | |||
import { deserialize, serialize } from 'seroval'; | |||
import { | |||
NextApiResponse, | |||
NextApiRequest, | |||
ENCTYPE_APPLICATION_JSON, | |||
ENCTYPE_X_WWW_FORM_URLENCODED, | |||
ENCTYPE_MULTIPART_FORM_DATA, ENCTYPE_APPLICATION_OCTET_STREAM, | |||
ENCTYPE_APPLICATION_OCTET_STREAM, | |||
} from './common'; | |||
let BODY_COOKIE_KEY: string; | |||
let STATUS_CODE_COOKIE_KEY: string; | |||
let STATUS_MESSAGE_COOKIE_KEY: string; | |||
let CONTENT_TYPE_COOKIE_KEY: string; | |||
const generateContentTypeCookieKey = () => { | |||
CONTENT_TYPE_COOKIE_KEY = `ifct${Date.now()}`; | |||
}; | |||
const generateBodyCookieKey = () => { | |||
BODY_COOKIE_KEY = `ifb${Date.now()}`; | |||
}; | |||
const generateStatusCodeCookieKey = () => { | |||
STATUS_CODE_COOKIE_KEY = `ifsc${Date.now()}`; | |||
}; | |||
const generateStatusMessageCookieKey = () => { | |||
STATUS_MESSAGE_COOKIE_KEY = `ifsm${Date.now()}`; | |||
}; | |||
const getBody = (req: IncomingMessage) => new Promise<Buffer>((resolve, reject) => { | |||
let body = Buffer.from(''); | |||
req.on('data', (chunk) => { | |||
body = Buffer.concat([body, chunk]); | |||
}); | |||
req.on('error', (err) => { | |||
reject(err); | |||
}); | |||
req.on('end', () => { | |||
resolve(body); | |||
}); | |||
}); | |||
import { getBody } from './utils/body'; | |||
import { | |||
CookieManager, | |||
BODY_COOKIE_KEY, | |||
STATUS_CODE_COOKIE_KEY, | |||
STATUS_MESSAGE_COOKIE_KEY, | |||
CONTENT_TYPE_COOKIE_KEY, | |||
} from './utils/cookies'; | |||
import { deserializeBody, EncTypeDeserializerMap } from './utils/serialization'; | |||
export namespace destination { | |||
export const getServerSideProps = ( | |||
@@ -69,44 +37,33 @@ export namespace destination { | |||
req.body = body.toString('utf-8'); | |||
} | |||
const cookies = nookies.parseCookies(ctx); | |||
const res: NextApiResponse = {}; | |||
const cookieManager = new CookieManager(ctx); | |||
// TODO how to properly remove cookies without leftovers? | |||
if (STATUS_CODE_COOKIE_KEY in cookies) { | |||
ctx.res.statusCode = Number(cookies[STATUS_CODE_COOKIE_KEY] || '200'); | |||
nookies.destroyCookie(ctx, STATUS_CODE_COOKIE_KEY, { | |||
path: '/', | |||
httpOnly: true, | |||
}); | |||
if (cookieManager.hasCookie(STATUS_CODE_COOKIE_KEY)) { | |||
ctx.res.statusCode = Number(cookieManager.getCookie(STATUS_CODE_COOKIE_KEY) || '200'); | |||
cookieManager.unsetCookie(STATUS_CODE_COOKIE_KEY); | |||
} | |||
if (STATUS_MESSAGE_COOKIE_KEY in cookies) { | |||
ctx.res.statusMessage = cookies[STATUS_MESSAGE_COOKIE_KEY] || ''; | |||
nookies.destroyCookie(ctx, STATUS_MESSAGE_COOKIE_KEY, { | |||
path: '/', | |||
httpOnly: true, | |||
}); | |||
if (cookieManager.hasCookie(STATUS_MESSAGE_COOKIE_KEY)) { | |||
ctx.res.statusMessage = cookieManager.getCookie(STATUS_MESSAGE_COOKIE_KEY) || ''; | |||
cookieManager.unsetCookie(STATUS_MESSAGE_COOKIE_KEY); | |||
} | |||
if (BODY_COOKIE_KEY in cookies) { | |||
const resBody = cookies[BODY_COOKIE_KEY]; | |||
if (cookies[CONTENT_TYPE_COOKIE_KEY] === ENCTYPE_APPLICATION_JSON) { | |||
const res: NextApiResponse = {}; | |||
if (cookieManager.hasCookie(BODY_COOKIE_KEY)) { | |||
const resBody = cookieManager.getCookie(BODY_COOKIE_KEY); | |||
const contentType = cookieManager.getCookie(CONTENT_TYPE_COOKIE_KEY); | |||
if (contentType === ENCTYPE_APPLICATION_JSON) { | |||
res.body = deserialize(resBody); | |||
} else if (cookies[CONTENT_TYPE_COOKIE_KEY] === ENCTYPE_APPLICATION_OCTET_STREAM) { | |||
} else if (contentType === ENCTYPE_APPLICATION_OCTET_STREAM) { | |||
res.body = deserialize(resBody); | |||
} else { | |||
const c = console; | |||
c.warn('Could not parse response body, returning nothing'); | |||
res.body = null; | |||
} | |||
nookies.destroyCookie(ctx, BODY_COOKIE_KEY, { | |||
path: '/', | |||
httpOnly: true, | |||
}); | |||
nookies.destroyCookie(ctx, CONTENT_TYPE_COOKIE_KEY, { | |||
path: '/', | |||
httpOnly: true, | |||
}); | |||
cookieManager.unsetCookie(BODY_COOKIE_KEY); | |||
cookieManager.unsetCookie(CONTENT_TYPE_COOKIE_KEY); | |||
} | |||
let gspResult; | |||
@@ -134,68 +91,6 @@ export namespace destination { | |||
} | |||
export namespace action { | |||
const parseMultipartFormData = async ( | |||
req: IncomingMessage, | |||
) => new Promise<Record<string, unknown>>((resolve, reject) => { | |||
const body: Record<string, unknown> = {}; | |||
const bb = busboy({ | |||
headers: req.headers, | |||
}); | |||
bb.on('file', (name, file, info) => { | |||
const { | |||
filename, | |||
mimeType: mimetype, | |||
} = info; | |||
let fileData = Buffer.from(''); | |||
file.on('data', (data) => { | |||
fileData = Buffer.concat([fileData, data]); | |||
}); | |||
file.on('close', () => { | |||
body[name] = new File([fileData.buffer], filename, { | |||
type: mimetype, | |||
}); | |||
}); | |||
}); | |||
bb.on('field', (name, value) => { | |||
body[name] = value; | |||
}); | |||
bb.on('close', () => { | |||
resolve(body); | |||
}); | |||
bb.on('error', (error) => { | |||
reject(error); | |||
}); | |||
req.pipe(bb); | |||
}); | |||
const deserializeBody = async (req: IncomingMessage) => { | |||
const contentType = req.headers['content-type']; | |||
const encoding = (req.headers['content-encoding'] ?? 'utf-8') as BufferEncoding; | |||
if (contentType === ENCTYPE_APPLICATION_JSON) { | |||
const bodyRaw = await getBody(req); | |||
return JSON.parse(bodyRaw.toString(encoding)) as Record<string, unknown>; | |||
} | |||
if (contentType === ENCTYPE_X_WWW_FORM_URLENCODED) { | |||
const bodyRaw = await getBody(req); | |||
return Object.fromEntries( | |||
new URLSearchParams(bodyRaw.toString(encoding)).entries(), | |||
); | |||
} | |||
if (contentType?.startsWith(`${ENCTYPE_MULTIPART_FORM_DATA};`)) { | |||
return parseMultipartFormData(req); | |||
} | |||
const bodyRaw = await getBody(req); | |||
return bodyRaw.toString('binary'); | |||
}; | |||
export const getApiConfig = (customConfig = {} as PageConfig) => ({ | |||
api: { | |||
...(customConfig.api ?? {}), | |||
@@ -203,19 +98,34 @@ export namespace action { | |||
}, | |||
}); | |||
export const wrapApiHandler = (fn: NextApiHandler): NextApiHandler => async (req, res) => { | |||
const body = await deserializeBody(req); | |||
export interface ActionWrapperOptions { | |||
fn: NextApiHandler, | |||
deserializers?: EncTypeDeserializerMap, | |||
} | |||
export const wrapApiHandler = ( | |||
options: ActionWrapperOptions, | |||
): NextApiHandler => async (req, res) => { | |||
const body = await deserializeBody({ | |||
req, | |||
deserializers: options.deserializers, | |||
}); | |||
const reqMut = req as unknown as Record<string, unknown>; | |||
reqMut.body = body; | |||
return fn(reqMut as unknown as DefaultNextApiRequest, res); | |||
return options.fn(reqMut as unknown as DefaultNextApiRequest, res); | |||
}; | |||
export const getServerSideProps = (fn: NextApiHandler): GetServerSideProps => async (ctx) => { | |||
export const getServerSideProps = ( | |||
options: ActionWrapperOptions, | |||
): GetServerSideProps => async (ctx) => { | |||
const { referer } = ctx.req.headers; | |||
const mockReq = { | |||
...ctx.req, | |||
body: await deserializeBody(ctx.req), | |||
body: await deserializeBody({ | |||
req: ctx.req, | |||
deserializers: options.deserializers, | |||
}), | |||
} as DefaultNextApiRequest; | |||
let data: unknown = null; | |||
@@ -230,7 +140,8 @@ export namespace action { | |||
return mockRes; | |||
}, | |||
send: (raw?: unknown) => { | |||
// todo: how to transfer binary response in a more compact way? | |||
// xtodo: how to transfer binary response in a more compact way? | |||
// > we let seroval handle this for now | |||
if (typeof raw === 'undefined' || raw === null) { | |||
return; | |||
} | |||
@@ -247,37 +158,15 @@ export namespace action { | |||
}, | |||
} as DefaultNextApiResponse; | |||
await fn(mockReq, mockRes); | |||
generateStatusCodeCookieKey(); | |||
nookies.setCookie(ctx, STATUS_CODE_COOKIE_KEY, mockRes.statusCode.toString(), { | |||
maxAge: 30 * 24 * 60 * 60, | |||
path: '/', | |||
httpOnly: true, | |||
}); | |||
generateStatusMessageCookieKey(); | |||
nookies.setCookie(ctx, STATUS_MESSAGE_COOKIE_KEY, mockRes.statusMessage, { | |||
maxAge: 30 * 24 * 60 * 60, | |||
path: '/', | |||
httpOnly: true, | |||
}); | |||
await options.fn(mockReq, mockRes); | |||
const cookieManager = new CookieManager(ctx); | |||
cookieManager.setCookie(STATUS_CODE_COOKIE_KEY, mockRes.statusCode.toString()); | |||
cookieManager.setCookie(STATUS_MESSAGE_COOKIE_KEY, mockRes.statusMessage); | |||
if (data) { | |||
generateBodyCookieKey(); | |||
nookies.setCookie(ctx, BODY_COOKIE_KEY, data as string, { | |||
maxAge: 30 * 24 * 60 * 60, | |||
path: '/', | |||
httpOnly: true, | |||
}); | |||
cookieManager.setCookie(BODY_COOKIE_KEY, data as string); | |||
if (contentType) { | |||
generateContentTypeCookieKey(); | |||
nookies.setCookie(ctx, CONTENT_TYPE_COOKIE_KEY, contentType, { | |||
maxAge: 30 * 24 * 60 * 60, | |||
path: '/', | |||
httpOnly: true, | |||
}); | |||
cookieManager.setCookie(CONTENT_TYPE_COOKIE_KEY, contentType); | |||
} | |||
} | |||
@@ -0,0 +1,60 @@ | |||
import { IncomingMessage } from 'http'; | |||
import busboy from 'busboy'; | |||
export const getBody = (req: IncomingMessage) => new Promise<Buffer>((resolve, reject) => { | |||
let body = Buffer.from(''); | |||
req.on('data', (chunk) => { | |||
body = Buffer.concat([body, chunk]); | |||
}); | |||
req.on('error', (err) => { | |||
reject(err); | |||
}); | |||
req.on('end', () => { | |||
resolve(body); | |||
}); | |||
}); | |||
export const parseMultipartFormData = async ( | |||
req: IncomingMessage, | |||
) => new Promise<Record<string, unknown>>((resolve, reject) => { | |||
const body: Record<string, unknown> = {}; | |||
const bb = busboy({ | |||
headers: req.headers, | |||
}); | |||
bb.on('file', (name, file, info) => { | |||
const { | |||
filename, | |||
mimeType: mimetype, | |||
} = info; | |||
let fileData = Buffer.from(''); | |||
file.on('data', (data) => { | |||
fileData = Buffer.concat([fileData, data]); | |||
}); | |||
file.on('close', () => { | |||
body[name] = new File([fileData.buffer], filename, { | |||
type: mimetype, | |||
}); | |||
}); | |||
}); | |||
bb.on('field', (name, value) => { | |||
body[name] = value; | |||
}); | |||
bb.on('close', () => { | |||
resolve(body); | |||
}); | |||
bb.on('error', (error) => { | |||
reject(error); | |||
}); | |||
req.pipe(bb); | |||
}); |
@@ -0,0 +1,55 @@ | |||
import { IncomingMessage, ServerResponse } from 'http'; | |||
import * as nookies from 'nookies'; | |||
const COMMON_COOKIE_CONFIG = { | |||
path: '/', | |||
httpOnly: true, | |||
}; | |||
const COMMON_SET_COOKIE_CONFIG = { | |||
...COMMON_COOKIE_CONFIG, | |||
maxAge: 30 * 24 * 60 * 60, | |||
}; | |||
const cookieKeys: Record<string, string> = {}; | |||
export const BODY_COOKIE_KEY = 'b' as const; | |||
export const STATUS_CODE_COOKIE_KEY = 'sc' as const; | |||
export const STATUS_MESSAGE_COOKIE_KEY = 'sm' as const; | |||
export const CONTENT_TYPE_COOKIE_KEY = 'ct' as const; | |||
export class CookieManager { | |||
private readonly ctx: { req: IncomingMessage, res: ServerResponse<IncomingMessage> }; | |||
constructor(ctx: { req: IncomingMessage, res: ServerResponse<IncomingMessage> }) { | |||
// noop | |||
this.ctx = ctx; | |||
} | |||
private static generateCookieKey(key: string) { | |||
return `if${key}${Date.now()}`; | |||
} | |||
setCookie(key: string, value: string) { | |||
nookies.setCookie( | |||
this.ctx, | |||
cookieKeys[key] = CookieManager.generateCookieKey(key), | |||
value, | |||
COMMON_SET_COOKIE_CONFIG, | |||
); | |||
} | |||
unsetCookie(key: string) { | |||
nookies.destroyCookie(this.ctx, cookieKeys[key], COMMON_COOKIE_CONFIG); | |||
} | |||
hasCookie(key: string) { | |||
const cookies = nookies.parseCookies(this.ctx); | |||
return cookieKeys[key] in cookies; | |||
} | |||
getCookie(key: string) { | |||
const cookies = nookies.parseCookies(this.ctx); | |||
return cookies[cookieKeys[key]]; | |||
} | |||
} |
@@ -0,0 +1,89 @@ | |||
import { IncomingMessage } from 'http'; | |||
import { getFormValues } from '@theoryofnekomata/formxtra'; | |||
import { | |||
ENCTYPE_APPLICATION_JSON, | |||
ENCTYPE_MULTIPART_FORM_DATA, | |||
ENCTYPE_X_WWW_FORM_URLENCODED, | |||
} from '../common'; | |||
import { getBody, parseMultipartFormData } from './body'; | |||
export type EncTypeSerializer = (data: unknown) => string; | |||
export type EncTypeSerializerMap = Record<string, EncTypeSerializer>; | |||
export type SerializerOptions = Parameters<typeof getFormValues>[1]; | |||
export interface SerializeBodyParams { | |||
form: HTMLFormElement, | |||
encType: string, | |||
serializers?: EncTypeSerializerMap, | |||
options?: SerializerOptions, | |||
} | |||
export const DEFAULT_ENCTYPE_SERIALIZERS: EncTypeSerializerMap = { | |||
[ENCTYPE_APPLICATION_JSON]: (data: unknown) => JSON.stringify(data), | |||
}; | |||
export const serializeBody = (params: SerializeBodyParams) => { | |||
const { | |||
form, | |||
encType, | |||
serializers = DEFAULT_ENCTYPE_SERIALIZERS, | |||
options, | |||
} = params; | |||
if (encType === ENCTYPE_X_WWW_FORM_URLENCODED) { | |||
return new URLSearchParams(form); | |||
} | |||
if (encType === ENCTYPE_MULTIPART_FORM_DATA) { | |||
// type error when provided a submitter element for some reason... | |||
const FormDataUnknown = FormData as unknown as { | |||
new(formElement?: HTMLElement, submitter?: HTMLElement): BodyInit; | |||
}; | |||
return new FormDataUnknown(form, options?.submitter); | |||
} | |||
if (typeof serializers[encType] === 'function') { | |||
return serializers[encType](getFormValues(form, options)); | |||
} | |||
throw new Error(`Unsupported encType: ${encType}`); | |||
}; | |||
export type EncTypeDeserializer = (data: string) => unknown; | |||
export type EncTypeDeserializerMap = Record<string, EncTypeDeserializer>; | |||
export interface DeserializeBodyParams { | |||
req: IncomingMessage, | |||
deserializers?: EncTypeDeserializerMap, | |||
} | |||
export const DEFAULT_ENCTYPE_DESERIALIZERS: EncTypeDeserializerMap = { | |||
[ENCTYPE_APPLICATION_JSON]: (data: string) => JSON.parse(data) as Record<string, unknown>, | |||
}; | |||
export const deserializeBody = async (params: DeserializeBodyParams) => { | |||
const { req, deserializers = DEFAULT_ENCTYPE_DESERIALIZERS } = params; | |||
const contentType = req.headers['content-type'] ?? 'application/octet-stream'; | |||
if (contentType?.startsWith(`${ENCTYPE_MULTIPART_FORM_DATA};`)) { | |||
return parseMultipartFormData(req); | |||
} | |||
const encoding = (req.headers['content-encoding'] ?? 'utf-8') as BufferEncoding; | |||
const bodyRaw = await getBody(req); | |||
if (contentType === ENCTYPE_X_WWW_FORM_URLENCODED) { | |||
return Object.fromEntries( | |||
new URLSearchParams(bodyRaw.toString(encoding)).entries(), | |||
); | |||
} | |||
if (typeof deserializers[contentType] === 'function') { | |||
return deserializers[contentType](bodyRaw.toString(encoding)); | |||
} | |||
return bodyRaw.toString('binary'); | |||
}; |