1
- import type { EventEmitter } from 'node:events '
1
+ import { Buffer } from 'node:buffer '
2
2
import type { IncomingMessage , ServerResponse as Response } from 'node:http'
3
3
4
4
type NextFunction = ( err ?: any ) => void
5
5
6
- // Extend the request object with body
6
+ /**
7
+ * Request extension with a body
8
+ */
7
9
export type ReqWithBody < T = any > = IncomingMessage & {
8
10
body ?: T
9
- } & EventEmitter
11
+ }
10
12
11
13
export const hasBody = ( method : string ) => [ 'POST' , 'PUT' , 'PATCH' , 'DELETE' ] . includes ( method )
12
14
@@ -15,45 +17,76 @@ const defaultPayloadLimit = 104857600 // 100KB
15
17
export type LimitErrorFn = ( limit : number ) => Error
16
18
17
19
export type ParserOptions = Partial < {
20
+ /**
21
+ * Limit payload size (in bytes)
22
+ * @default '100KB'
23
+ */
18
24
payloadLimit : number
25
+ /**
26
+ * Custom error function for payload limit
27
+ */
19
28
payloadLimitErrorFn : LimitErrorFn
20
29
} >
21
30
22
31
const defaultErrorFn : LimitErrorFn = ( payloadLimit ) => new Error ( `Payload too large. Limit: ${ payloadLimit } bytes` )
23
32
24
33
// Main function
25
34
export const p =
26
- < T = any > ( fn : ( body : any ) => any , payloadLimit = defaultPayloadLimit , payloadLimitErrorFn : LimitErrorFn = defaultErrorFn ) =>
35
+ < T = any > (
36
+ fn : ( body : Buffer ) => void ,
37
+ payloadLimit = defaultPayloadLimit ,
38
+ payloadLimitErrorFn : LimitErrorFn = defaultErrorFn
39
+ ) =>
27
40
async ( req : ReqWithBody < T > , _res : Response , next : ( err ?: any ) => void ) => {
28
41
try {
29
- let body = ''
42
+ const body : Buffer [ ] = [ ]
30
43
31
44
for await ( const chunk of req ) {
32
- if ( body . length > payloadLimit ) throw payloadLimitErrorFn ( payloadLimit )
33
- body += chunk
45
+ const totalSize = body . reduce ( ( total , buffer ) => total + buffer . byteLength , 0 )
46
+ if ( totalSize > payloadLimit ) throw payloadLimitErrorFn ( payloadLimit )
47
+ body . push ( chunk as Buffer )
34
48
}
35
49
36
- return fn ( body )
50
+ return fn ( Buffer . concat ( body ) )
37
51
} catch ( e ) {
38
52
next ( e )
39
53
}
40
54
}
41
55
56
+ /**
57
+ * Parse payload with a custom function
58
+ * @param fn
59
+ */
42
60
const custom =
43
- < T = any > ( fn : ( body : any ) => any ) =>
61
+ < T = any > ( fn : ( body : Buffer ) => any ) =>
44
62
async ( req : ReqWithBody , _res : Response , next : NextFunction ) => {
45
63
if ( hasBody ( req . method ! ) ) req . body = await p < T > ( fn ) ( req , _res , next )
46
64
next ( )
47
65
}
48
66
67
+ /**
68
+ * Parse JSON payload
69
+ * @param options
70
+ */
49
71
const json =
50
72
( { payloadLimit, payloadLimitErrorFn } : ParserOptions = { } ) =>
51
73
async ( req : ReqWithBody , res : Response , next : NextFunction ) => {
52
74
if ( hasBody ( req . method ! ) ) {
53
- req . body = await p ( ( x ) => ( x ? JSON . parse ( x . toString ( ) ) : { } ) , payloadLimit , payloadLimitErrorFn ) ( req , res , next )
75
+ req . body = await p (
76
+ ( x ) => {
77
+ const str = td . decode ( x )
78
+ return str ? JSON . parse ( str ) : { }
79
+ } ,
80
+ payloadLimit ,
81
+ payloadLimitErrorFn
82
+ ) ( req , res , next )
54
83
} else next ( )
55
84
}
56
85
86
+ /**
87
+ * Parse raw payload
88
+ * @param options
89
+ */
57
90
const raw =
58
91
( { payloadLimit, payloadLimitErrorFn } : ParserOptions = { } ) =>
59
92
async ( req : ReqWithBody , _res : Response , next : NextFunction ) => {
@@ -62,46 +95,54 @@ const raw =
62
95
} else next ( )
63
96
}
64
97
98
+ const td = new TextDecoder ( )
99
+ /**
100
+ * Stringify request payload
101
+ * @param param0
102
+ * @returns
103
+ */
65
104
const text =
66
105
( { payloadLimit, payloadLimitErrorFn } : ParserOptions = { } ) =>
67
106
async ( req : ReqWithBody , _res : Response , next : NextFunction ) => {
68
107
if ( hasBody ( req . method ! ) ) {
69
- req . body = await p ( ( x ) => x . toString ( ) , payloadLimit , payloadLimitErrorFn ) ( req , _res , next )
108
+ req . body = await p ( ( x ) => td . decode ( x ) , payloadLimit , payloadLimitErrorFn ) ( req , _res , next )
70
109
} else next ( )
71
110
}
72
111
112
+ /**
113
+ * Parse urlencoded payload
114
+ * @param options
115
+ */
73
116
const urlencoded =
74
117
( { payloadLimit, payloadLimitErrorFn } : ParserOptions = { } ) =>
75
118
async ( req : ReqWithBody , _res : Response , next : NextFunction ) => {
76
119
if ( hasBody ( req . method ! ) ) {
77
120
req . body = await p (
78
- ( x ) => {
79
- const urlSearchParam = new URLSearchParams ( x . toString ( ) )
80
- return Object . fromEntries ( urlSearchParam . entries ( ) )
81
- } ,
121
+ ( x ) => Object . fromEntries ( new URLSearchParams ( x . toString ( ) ) . entries ( ) ) ,
82
122
payloadLimit ,
83
123
payloadLimitErrorFn
84
124
) ( req , _res , next )
85
125
} else next ( )
86
126
}
87
127
88
128
const getBoundary = ( contentType : string ) => {
89
- // Extract the boundary from the Content-Type header
90
129
const match = / b o u n d a r y = ( .+ ) ; ? / . exec ( contentType )
91
130
return match ? `--${ match [ 1 ] } ` : null
92
131
}
93
132
94
133
const defaultFileSizeLimitErrorFn : LimitErrorFn = ( limit ) => new Error ( `File too large. Limit: ${ limit } bytes` )
95
134
96
- const parseMultipart = ( body : string , boundary : string , { fileCountLimit, fileSizeLimit, fileSizeLimitErrorFn = defaultFileSizeLimitErrorFn } : MultipartOptions ) => {
97
- // Split the body into an array of parts
135
+ const parseMultipart = (
136
+ body : string ,
137
+ boundary : string ,
138
+ { fileCountLimit, fileSizeLimit, fileSizeLimitErrorFn = defaultFileSizeLimitErrorFn } : MultipartOptions
139
+ ) => {
98
140
const parts = body . split ( new RegExp ( `${ boundary } (--)?` ) ) . filter ( ( part ) => ! ! part && / c o n t e n t - d i s p o s i t i o n / i. test ( part ) )
99
141
const parsedBody : Record < string , ( File | string ) [ ] > = { }
100
142
101
143
if ( fileCountLimit && parts . length > fileCountLimit ) throw new Error ( `Too many files. Limit: ${ fileCountLimit } ` )
102
144
103
- // Parse each part into a form data object
104
- // biome-ignore lint/complexity/noForEach: <explanation>
145
+ // biome-ignore lint/complexity/noForEach: for...of fails
105
146
parts . forEach ( ( part ) => {
106
147
const [ headers , ...lines ] = part . split ( '\r\n' ) . filter ( ( part ) => ! ! part )
107
148
const data = lines . join ( '\r\n' ) . trim ( )
@@ -120,28 +161,45 @@ const parseMultipart = (body: string, boundary: string, { fileCountLimit, fileSi
120
161
parsedBody [ name ] = parsedBody [ name ] ? [ ...parsedBody [ name ] , file ] : [ file ]
121
162
return
122
163
}
123
- // This is a regular field
124
164
parsedBody [ name ] = parsedBody [ name ] ? [ ...parsedBody [ name ] , data ] : [ data ]
125
165
return
126
166
} )
127
167
128
168
return parsedBody
129
169
}
130
170
type MultipartOptions = Partial < {
171
+ /**
172
+ * Limit number of files
173
+ */
131
174
fileCountLimit : number
175
+ /**
176
+ * Limit file size (in bytes)
177
+ */
132
178
fileSizeLimit : number
179
+ /**
180
+ * Custom error function for file size limit
181
+ */
133
182
fileSizeLimitErrorFn : LimitErrorFn
134
183
} >
135
-
184
+ /**
185
+ * Parse multipart form data (supports files as well)
186
+ *
187
+ * Does not restrict total payload size by default
188
+ * @param options
189
+ */
136
190
const multipart =
137
- ( { payloadLimit, payloadLimitErrorFn, ...opts } : MultipartOptions & ParserOptions = { } ) =>
191
+ ( { payloadLimit = Number . POSITIVE_INFINITY , payloadLimitErrorFn, ...opts } : MultipartOptions & ParserOptions = { } ) =>
138
192
async ( req : ReqWithBody , res : Response , next : NextFunction ) => {
139
193
if ( hasBody ( req . method ! ) ) {
140
- req . body = await p ( ( x ) => {
141
- const boundary = getBoundary ( req . headers [ 'content-type' ] ! )
142
- if ( boundary ) return parseMultipart ( x , boundary , opts )
143
- return { }
144
- } , payloadLimit , payloadLimitErrorFn ) ( req , res , next )
194
+ req . body = await p (
195
+ ( x ) => {
196
+ const boundary = getBoundary ( req . headers [ 'content-type' ] ! )
197
+ if ( boundary ) return parseMultipart ( td . decode ( x ) , boundary , opts )
198
+ return { }
199
+ } ,
200
+ payloadLimit ,
201
+ payloadLimitErrorFn
202
+ ) ( req , res , next )
145
203
next ( )
146
204
} else next ( )
147
205
}
0 commit comments