Skip to content

Commit

Permalink
feat!: don't populate req.body, return the parsed body instead
Browse files Browse the repository at this point in the history
  • Loading branch information
Lordfirespeed committed Sep 22, 2024
1 parent ddf091b commit 65e64c8
Show file tree
Hide file tree
Showing 16 changed files with 122 additions and 170 deletions.
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,11 @@ Custom function for `parsec`.

```js
// curl -d "this text must be uppercased" localhost
await custom(
req,
(d) => d.toUpperCase(),
(err) => {}
await makeCustom(
req,
(d) => d.toUpperCase(),
(err) => {
}
)
res.end(req.body) // "THIS TEXT MUST BE UPPERCASED"
```
Expand Down
13 changes: 7 additions & 6 deletions src/content-types/custom.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { getRead } from '@/get-read'
import type { HasBody, MaybeParsed, NextFunction, Request, Response } from '@/types'
import type { MaybeParsed, Request, Response } from '@/types'
import { alreadyParsed } from '@/utils/already-parsed-symbol'
import { hasNoBody } from '@/utils/has-no-body'

export const custom = <T = unknown>(fn: (body: string) => T) => {
export const makeCustom = <T = unknown>(fn: (body: string) => T) => {
const read = getRead(fn)
return async (req: Request & HasBody<T> & MaybeParsed, res: Response, next: NextFunction) => {
if (req[alreadyParsed] === true) return next()
req.body = await read(req, res)
next()
return async (req: Request & MaybeParsed, res: Response): Promise<T | undefined> => {
if (req[alreadyParsed] === true) return undefined
if (hasNoBody(req.method)) return undefined
return await read(req, res)
}
}
34 changes: 16 additions & 18 deletions src/content-types/json.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import { ContentType } from '@otterhttp/content-type'

import { type ReadOptions, getRead } from '@/get-read'
import type { HasBody, MaybeParsed, NextFunction, Request, Response } from '@/types'
import type { MaybeParsed, Request, Response } from '@/types'
import { alreadyParsed } from '@/utils/already-parsed-symbol'
import { compose } from '@/utils/compose-functions'
import { ClientCharsetError } from '@/utils/errors'
import { hasNoBody } from '@/utils/has-no-body'
import { typeChecker } from '@/utils/type-checker'

export type JsonBodyParsingOptions<
Body = unknown,
Req extends Request & HasBody<Body> = Request & HasBody<Body>,
Res extends Response<Req> = Response<Req>,
> = Omit<ReadOptions, 'defaultCharset'> & {
export type JsonBodyParsingOptions<Req extends Request = Request, Res extends Response<Req> = Response<Req>> = Omit<
ReadOptions,
'defaultCharset'
> & {
/**
* JSON reviver to pass into JSON.parse.
*
Expand Down Expand Up @@ -65,11 +64,11 @@ function ensureCharsetIsUtf8(_req: unknown, _res: unknown, charset: string | und
})
}

export function json<
Body = unknown,
Req extends Request & HasBody<Body> = Request & HasBody<Body>,
Res extends Response<Req> = Response<Req>,
>(options?: JsonBodyParsingOptions<Body, Req, Res>) {
type ParsedJson = never | string | number | boolean | null | { [property: string]: ParsedJson } | ParsedJson[]

export function makeJson<Req extends Request = Request, Res extends Response<Req> = Response<Req>>(
options?: JsonBodyParsingOptions<Req, Res>,
) {
const optionsCopy: ReadOptions = Object.assign({ defaultCharset: 'utf-8' }, options)
optionsCopy.limit ??= '100kb'
optionsCopy.inflate ??= true
Expand All @@ -79,7 +78,7 @@ export function json<
const strict = options?.strict ?? true
const matcher = options?.matcher ?? typeChecker(ContentType.parse('application/*+json'))

function parse(body: string) {
function parse(body: string): ParsedJson {
if (body.length === 0) return {}

if (strict) {
Expand All @@ -93,11 +92,10 @@ export function json<
}

const read = getRead(parse, optionsCopy)
return async (req: Req & MaybeParsed, res: Res, next: NextFunction) => {
if (req[alreadyParsed] === true) return next()
if (hasNoBody(req.method)) return next()
if (!matcher(req, res)) return next()
req.body = await read(req, res)
next()
return async (req: Req & MaybeParsed, res: Res): Promise<ParsedJson | undefined> => {
if (req[alreadyParsed] === true) return undefined
if (hasNoBody(req.method)) return undefined
if (!matcher(req, res)) return undefined
return await read(req, res)
}
}
22 changes: 10 additions & 12 deletions src/content-types/multipart-form-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { ClientError, HttpError } from '@otterhttp/errors'

import { type RawReadOptions, type ReadOptions, getRawRead } from '@/get-read'
import { type ParsedMultipartFormData, parseMultipartFormData } from '@/parsers/multipart-form-data'
import type { HasBody, MaybeParsed, NextFunction, Request, Response } from '@/types'
import type { MaybeParsed, Request, Response } from '@/types'
import { alreadyParsed } from '@/utils/already-parsed-symbol'
import { hasNoBody } from '@/utils/has-no-body'
import { typeChecker } from '@/utils/type-checker'

export type MultipartFormDataBodyParsingOptions<
Req extends Request & HasBody<ParsedMultipartFormData> = Request & HasBody<ParsedMultipartFormData>,
Req extends Request = Request,
Res extends Response<Req> = Response<Req>,
> = RawReadOptions & {
/**
Expand All @@ -22,10 +22,9 @@ export type MultipartFormDataBodyParsingOptions<
matcher?: (req: Req, res: Res) => boolean
}

export function multipartFormData<
Req extends Request & HasBody<ParsedMultipartFormData> = Request & HasBody<ParsedMultipartFormData>,
Res extends Response<Req> = Response<Req>,
>(options?: MultipartFormDataBodyParsingOptions<Req, Res>) {
export function makeMultipartFormData<Req extends Request = Request, Res extends Response<Req> = Response<Req>>(
options?: MultipartFormDataBodyParsingOptions<Req, Res>,
) {
const optionsCopy: ReadOptions = Object.assign({}, options)
optionsCopy.limit ??= '10mb'
optionsCopy.inflate ??= true
Expand All @@ -40,10 +39,10 @@ export function multipartFormData<
}

const rawRead = getRawRead(options)
return async (req: Req & MaybeParsed, res: Res, next: NextFunction) => {
if (req[alreadyParsed] === true) return next()
if (hasNoBody(req.method)) return next()
if (!matcher(req, res)) return next()
return async (req: Req & MaybeParsed, res: Res): Promise<ParsedMultipartFormData | undefined> => {
if (req[alreadyParsed] === true) return undefined
if (hasNoBody(req.method)) return undefined
if (!matcher(req, res)) return undefined

if (req.headers['content-type'] == null) failBoundaryParameter()
const contentType = ContentType.parse(req.headers['content-type'])
Expand All @@ -53,7 +52,7 @@ export function multipartFormData<
const rawBody = await rawRead(req, res)

try {
req.body = parseMultipartFormData(rawBody, boundary)
return parseMultipartFormData(rawBody, boundary)
} catch (err) {
if (err instanceof HttpError) throw err
throw new ClientError('Multipart body parsing failed', {
Expand All @@ -62,7 +61,6 @@ export function multipartFormData<
cause: err instanceof Error ? err : undefined,
})
}
next()
}
}

Expand Down
22 changes: 10 additions & 12 deletions src/content-types/raw-multipart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { ClientError, HttpError } from '@otterhttp/errors'

import { type RawReadOptions, type ReadOptions, getRawRead } from '@/get-read'
import { type ParsedMultipartData, parseMultipart } from '@/parsers/multipart'
import type { HasBody, MaybeParsed, NextFunction, Request, Response } from '@/types'
import type { MaybeParsed, Request, Response } from '@/types'
import { alreadyParsed } from '@/utils/already-parsed-symbol'
import { hasNoBody } from '@/utils/has-no-body'
import { typeChecker } from '@/utils/type-checker'

export type RawMultipartBodyParsingOptions<
Req extends Request & HasBody<ParsedMultipartData> = Request & HasBody<ParsedMultipartData>,
Req extends Request = Request,
Res extends Response<Req> = Response<Req>,
> = RawReadOptions & {
/**
Expand All @@ -22,10 +22,9 @@ export type RawMultipartBodyParsingOptions<
matcher?: (req: Req, res: Res) => boolean
}

export function rawMultipart<
Req extends Request & HasBody<ParsedMultipartData> = Request & HasBody<ParsedMultipartData>,
Res extends Response<Req> = Response<Req>,
>(options?: RawMultipartBodyParsingOptions<Req, Res>) {
export function makeRawMultipart<Req extends Request = Request, Res extends Response<Req> = Response<Req>>(
options?: RawMultipartBodyParsingOptions<Req, Res>,
) {
const optionsCopy: ReadOptions = Object.assign({}, options)
optionsCopy.limit ??= '10mb'
optionsCopy.inflate ??= true
Expand All @@ -40,10 +39,10 @@ export function rawMultipart<
}

const rawRead = getRawRead(options)
return async (req: Req & MaybeParsed, res: Res, next: NextFunction) => {
if (req[alreadyParsed] === true) return next()
if (hasNoBody(req.method)) return next()
if (!matcher(req, res)) return next()
return async (req: Req & MaybeParsed, res: Res): Promise<ParsedMultipartData | undefined> => {
if (req[alreadyParsed] === true) return undefined
if (hasNoBody(req.method)) return undefined
if (!matcher(req, res)) return undefined

if (req.headers['content-type'] == null) failBoundaryParameter()
const contentType = ContentType.parse(req.headers['content-type'])
Expand All @@ -53,7 +52,7 @@ export function rawMultipart<
const rawBody = await rawRead(req, res)

try {
req.body = parseMultipart(rawBody, boundary)
return parseMultipart(rawBody, boundary)
} catch (err) {
if (err instanceof HttpError) throw err
throw new ClientError('Multipart body parsing failed', {
Expand All @@ -62,7 +61,6 @@ export function rawMultipart<
cause: err instanceof Error ? err : undefined,
})
}
next()
}
}

Expand Down
22 changes: 10 additions & 12 deletions src/content-types/raw.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { type RawReadOptions, getRawRead } from '@/get-read'
import type { HasBody, MaybeParsed, NextFunction, Request, Response } from '@/types'
import type { MaybeParsed, Request, Response } from '@/types'
import { alreadyParsed } from '@/utils/already-parsed-symbol'
import { hasNoBody } from '@/utils/has-no-body'

export type RawBodyParsingOptions<
Req extends Request & HasBody<Buffer> = Request & HasBody<Buffer>,
Req extends Request = Request,
Res extends Response<Req> = Response<Req>,
> = RawReadOptions & {
/**
Expand All @@ -17,22 +17,20 @@ export type RawBodyParsingOptions<
matcher?: (req: Req, res: Res) => boolean
}

export function raw<
Req extends Request & HasBody<Buffer> = Request & HasBody<Buffer>,
Res extends Response<Req> = Response<Req>,
>(options?: RawBodyParsingOptions<Req, Res>) {
export function makeRaw<Req extends Request = Request, Res extends Response<Req> = Response<Req>>(
options?: RawBodyParsingOptions<Req, Res>,
) {
const optionsCopy: RawReadOptions = Object.assign({}, options)
optionsCopy.limit ??= '100kb'
optionsCopy.inflate ??= true

const matcher = options?.matcher ?? (() => true)

const read = getRawRead(options)
return async (req: Req & MaybeParsed, res: Res, next: NextFunction) => {
if (req[alreadyParsed] === true) return next()
if (hasNoBody(req.method)) return next()
if (!matcher(req, res)) return next()
req.body = await read(req, res)
next()
return async (req: Req & MaybeParsed, res: Res): Promise<Buffer | undefined> => {
if (req[alreadyParsed] === true) return undefined
if (hasNoBody(req.method)) return undefined
if (!matcher(req, res)) return undefined
return await read(req, res)
}
}
22 changes: 10 additions & 12 deletions src/content-types/text.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { ContentType } from '@otterhttp/content-type'

import { type ReadOptions, getRead } from '@/get-read'
import type { HasBody, MaybeParsed, NextFunction, Request, Response } from '@/types'
import type { MaybeParsed, Request, Response } from '@/types'
import { alreadyParsed } from '@/utils/already-parsed-symbol'
import { hasNoBody } from '@/utils/has-no-body'
import { typeChecker } from '@/utils/type-checker'

export type TextBodyParsingOptions<
Req extends Request & HasBody<string> = Request & HasBody<string>,
Req extends Request = Request,
Res extends Response<Req> = Response<Req>,
> = ReadOptions & {
/**
Expand All @@ -20,22 +20,20 @@ export type TextBodyParsingOptions<
matcher?: (req: Req, res: Res) => boolean
}

export function text<
Req extends Request & HasBody<string> = Request & HasBody<string>,
Res extends Response<Req> = Response<Req>,
>(options?: TextBodyParsingOptions<Req, Res>) {
export function makeText<Req extends Request = Request, Res extends Response<Req> = Response<Req>>(
options?: TextBodyParsingOptions<Req, Res>,
) {
const optionsCopy: ReadOptions = Object.assign({}, options)
optionsCopy.limit ??= '100kb'
optionsCopy.inflate ??= true

const matcher = options?.matcher ?? typeChecker(ContentType.parse('text/*'))

const read = getRead((x) => x.toString(), optionsCopy)
return async (req: Req & MaybeParsed, res: Res, next: NextFunction) => {
if (req[alreadyParsed] === true) return next()
if (hasNoBody(req.method)) return next()
if (!matcher(req, res)) return next()
req.body = await read(req, res)
next()
return async (req: Req & MaybeParsed, res: Res): Promise<string | undefined> => {
if (req[alreadyParsed] === true) return undefined
if (hasNoBody(req.method)) return undefined
if (!matcher(req, res)) return undefined
return await read(req, res)
}
}
22 changes: 10 additions & 12 deletions src/content-types/urlencoded.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import { type ParsedUrlQuery, parse } from 'node:querystring'
import { ContentType } from '@otterhttp/content-type'

import { type ReadOptions, getRead } from '@/get-read'
import type { HasBody, MaybeParsed, NextFunction, Request, Response } from '@/types'
import type { MaybeParsed, Request, Response } from '@/types'
import { alreadyParsed } from '@/utils/already-parsed-symbol'
import { compose } from '@/utils/compose-functions'
import { ClientCharsetError } from '@/utils/errors'
import { hasNoBody } from '@/utils/has-no-body'
import { typeChecker } from '@/utils/type-checker'

export type UrlencodedBodyParsingOptions<
Req extends Request & HasBody<ParsedUrlQuery> = Request & HasBody<ParsedUrlQuery>,
Req extends Request = Request,
Res extends Response<Req> = Response<Req>,
> = Omit<ReadOptions, 'defaultCharset'> & {
/**
Expand All @@ -36,10 +36,9 @@ function ensureCharsetIsUtf8(_req: unknown, _res: unknown, charset: string | und
})
}

export function urlencoded<
Req extends Request & HasBody<ParsedUrlQuery> = Request & HasBody<ParsedUrlQuery>,
Res extends Response<Req> = Response<Req>,
>(options?: UrlencodedBodyParsingOptions<Req, Res>) {
export function makeUrlencoded<Req extends Request = Request, Res extends Response<Req> = Response<Req>>(
options?: UrlencodedBodyParsingOptions<Req, Res>,
) {
const optionsCopy: ReadOptions = Object.assign({ defaultCharset: 'utf-8' }, options)
optionsCopy.limit ??= '100kb'
optionsCopy.inflate ??= true
Expand All @@ -48,12 +47,11 @@ export function urlencoded<
const matcher = options?.matcher ?? typeChecker(ContentType.parse('application/*+x-www-form-urlencoded'))

const read = getRead<ParsedUrlQuery>((x) => parse(x), optionsCopy)
return async (req: Req & MaybeParsed, res: Res, next: NextFunction) => {
if (req[alreadyParsed] === true) return next()
if (hasNoBody(req.method)) return next()
if (!matcher(req, res)) return next()
req.body = await read(req, res)
next()
return async (req: Req & MaybeParsed, res: Res): Promise<ParsedUrlQuery | undefined> => {
if (req[alreadyParsed] === true) return undefined
if (hasNoBody(req.method)) return undefined
if (!matcher(req, res)) return undefined
return await read(req, res)
}
}

Expand Down
Loading

0 comments on commit 65e64c8

Please sign in to comment.