0http-bun 1.1.3 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,783 @@
1
+ /**
2
+ * Advanced Body Parser Middleware for 0http-bun
3
+ * Supports JSON, text, URL-encoded, and multipart form data parsing
4
+ *
5
+ * Security Features:
6
+ * - Protected against prototype pollution attacks
7
+ * - ReDoS (Regular Expression Denial of Service) protection
8
+ * - Memory exhaustion prevention with strict size limits
9
+ * - Excessive nesting protection for JSON
10
+ * - Parameter count limits for form data
11
+ * - Input validation and sanitization
12
+ * - Error message sanitization to prevent information leakage
13
+ */
14
+
15
+ /**
16
+ * Parses size limit strings with suffixes (e.g. '500b', '1kb', '2mb')
17
+ * @param {number|string} limit - Size limit
18
+ * @returns {number} Size in bytes
19
+ */
20
+ function parseLimit(limit) {
21
+ if (typeof limit === 'number') {
22
+ // Enforce maximum limit to prevent memory exhaustion
23
+ return Math.min(Math.max(0, limit), 1024 * 1024 * 1024) // Max 1GB
24
+ }
25
+
26
+ if (typeof limit === 'string') {
27
+ // Prevent ReDoS by limiting string length and using a more restrictive regex
28
+ if (limit.length > 20) {
29
+ throw new Error(`Invalid limit format: ${limit}`)
30
+ }
31
+
32
+ // More restrictive regex to prevent ReDoS attacks
33
+ const match = limit.match(/^(\d{1,10}(?:\.\d{1,3})?)\s*(b|kb|mb|gb)$/i)
34
+ if (!match) {
35
+ throw new Error(`Invalid limit format: ${limit}`)
36
+ }
37
+
38
+ const value = parseFloat(match[1])
39
+ if (isNaN(value) || value < 0) {
40
+ throw new Error(`Invalid limit value: ${limit}`)
41
+ }
42
+
43
+ const unit = match[2].toLowerCase()
44
+
45
+ let bytes
46
+ switch (unit) {
47
+ case 'b':
48
+ bytes = value
49
+ break
50
+ case 'kb':
51
+ bytes = value * 1024
52
+ break
53
+ case 'mb':
54
+ bytes = value * 1024 * 1024
55
+ break
56
+ case 'gb':
57
+ bytes = value * 1024 * 1024 * 1024
58
+ break
59
+ default:
60
+ bytes = value
61
+ }
62
+
63
+ // Enforce maximum limit to prevent memory exhaustion
64
+ return Math.min(bytes, 1024 * 1024 * 1024) // Max 1GB
65
+ }
66
+
67
+ return limit || 1024 * 1024 // Default 1MB
68
+ }
69
+
70
+ /**
71
+ * Helper function to check if content type matches any of the JSON types
72
+ * @param {string} contentType - Content type from request
73
+ * @param {string[]} jsonTypes - Array of JSON content types
74
+ * @returns {boolean} Whether content type matches any JSON type
75
+ */
76
+ function isJsonType(contentType, jsonTypes) {
77
+ if (!contentType) return false
78
+ const lowerContentType = contentType.toLowerCase()
79
+ return jsonTypes.some((type) => lowerContentType.includes(type.toLowerCase()))
80
+ }
81
+
82
+ /**
83
+ * Helper function to check if request method typically has a body
84
+ * @param {Request} req - Request object
85
+ * @returns {boolean} Whether the request method has a body
86
+ */
87
+ function hasBody(req) {
88
+ return ['POST', 'PUT', 'PATCH'].includes(req.method.toUpperCase())
89
+ }
90
+
91
+ /**
92
+ * Helper function to check if content type should be parsed
93
+ * @param {Request} req - Request object
94
+ * @param {string} type - Expected content type
95
+ * @returns {boolean} Whether content should be parsed
96
+ */
97
+ function shouldParse(req, type) {
98
+ const contentType = req.headers.get('content-type')
99
+ return contentType && contentType.toLowerCase().includes(type.toLowerCase())
100
+ }
101
+
102
+ /**
103
+ * Helper function to parse nested keys in URL-encoded data
104
+ * Protected against prototype pollution attacks
105
+ * @param {Object} obj - Target object
106
+ * @param {string} key - Key with potential nesting
107
+ * @param {string} value - Value to set
108
+ * @param {number} depth - Current nesting depth to prevent excessive recursion
109
+ */
110
+ function parseNestedKey(obj, key, value, depth = 0) {
111
+ // Prevent excessive nesting to avoid stack overflow
112
+ if (depth > 20) {
113
+ throw new Error('Maximum nesting depth exceeded')
114
+ }
115
+
116
+ // Protect against prototype pollution
117
+ const prototypePollutionKeys = [
118
+ '__proto__',
119
+ 'constructor',
120
+ 'prototype',
121
+ 'hasOwnProperty',
122
+ 'isPrototypeOf',
123
+ 'propertyIsEnumerable',
124
+ 'valueOf',
125
+ 'toString',
126
+ ]
127
+
128
+ if (prototypePollutionKeys.includes(key)) {
129
+ return // Silently ignore dangerous keys
130
+ }
131
+
132
+ const match = key.match(/^([^[]+)\[([^\]]*)\](.*)$/)
133
+ if (!match) {
134
+ obj[key] = value
135
+ return
136
+ }
137
+
138
+ const [, baseKey, indexKey, remaining] = match
139
+
140
+ // Protect against prototype pollution on base key
141
+ if (prototypePollutionKeys.includes(baseKey)) {
142
+ return
143
+ }
144
+
145
+ if (!obj[baseKey]) {
146
+ obj[baseKey] = indexKey === '' ? [] : {}
147
+ }
148
+
149
+ // Ensure obj[baseKey] is a safe object/array
150
+ if (typeof obj[baseKey] !== 'object' || obj[baseKey] === null) {
151
+ obj[baseKey] = indexKey === '' ? [] : {}
152
+ }
153
+
154
+ if (remaining) {
155
+ const nextKey = indexKey + remaining
156
+ parseNestedKey(obj[baseKey], nextKey, value, depth + 1)
157
+ } else {
158
+ if (indexKey === '') {
159
+ if (Array.isArray(obj[baseKey])) {
160
+ obj[baseKey].push(value)
161
+ }
162
+ } else {
163
+ // Protect against prototype pollution on index key
164
+ if (!prototypePollutionKeys.includes(indexKey)) {
165
+ obj[baseKey][indexKey] = value
166
+ }
167
+ }
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Creates a JSON body parser middleware
173
+ * @param {Object} options - Body parser configuration
174
+ * @param {number|string} options.limit - Maximum body size in bytes (default: 1MB)
175
+ * @param {Function} options.reviver - JSON.parse reviver function
176
+ * @param {boolean} options.strict - Only parse arrays and objects (default: true)
177
+ * @param {string} options.type - Content-Type to parse (default: application/json)
178
+ * @param {boolean} options.deferNext - If true, don't call next() and let caller handle it
179
+ * @returns {Function} Middleware function
180
+ */
181
+ function createJSONParser(options = {}) {
182
+ const {
183
+ limit = '1mb',
184
+ reviver,
185
+ strict = true,
186
+ type = 'application/json',
187
+ deferNext = false,
188
+ } = options
189
+
190
+ const parsedLimit = parseLimit(limit)
191
+
192
+ return async function jsonParserMiddleware(req, next) {
193
+ if (!hasBody(req) || !shouldParse(req, type)) {
194
+ return deferNext ? null : next()
195
+ }
196
+
197
+ try {
198
+ const contentLength = req.headers.get('content-length')
199
+ if (contentLength) {
200
+ const length = parseInt(contentLength)
201
+ if (isNaN(length) || length < 0) {
202
+ return new Response('Invalid content-length header', {status: 400})
203
+ }
204
+ if (length > parsedLimit) {
205
+ return new Response('Request body size exceeded', {status: 413})
206
+ }
207
+ }
208
+
209
+ // Check if the request has a null body (no body was provided)
210
+ if (req.body === null) {
211
+ Object.defineProperty(req, 'body', {
212
+ value: undefined,
213
+ writable: true,
214
+ enumerable: true,
215
+ configurable: true,
216
+ })
217
+ return deferNext ? null : next()
218
+ }
219
+
220
+ const text = await req.text()
221
+ // Store raw body text for verification
222
+ req._rawBodyText = text
223
+
224
+ // Validate text length to prevent memory exhaustion
225
+ const textLength = new TextEncoder().encode(text).length
226
+ if (textLength > parsedLimit) {
227
+ return new Response('Request body size exceeded', {status: 413})
228
+ }
229
+
230
+ // Additional protection against excessively deep nesting
231
+ if (text.length > 0) {
232
+ // Count nesting levels to prevent stack overflow during parsing
233
+ let nestingLevel = 0
234
+ let maxNesting = 0
235
+ for (let i = 0; i < text.length; i++) {
236
+ if (text[i] === '{' || text[i] === '[') {
237
+ nestingLevel++
238
+ maxNesting = Math.max(maxNesting, nestingLevel)
239
+ } else if (text[i] === '}' || text[i] === ']') {
240
+ nestingLevel--
241
+ }
242
+ }
243
+ if (maxNesting > 100) {
244
+ return new Response('JSON nesting too deep', {status: 400})
245
+ }
246
+ }
247
+
248
+ // Handle empty string body (becomes empty object)
249
+ if (text === '' || text.trim() === '') {
250
+ Object.defineProperty(req, 'body', {
251
+ value: {},
252
+ writable: true,
253
+ enumerable: true,
254
+ configurable: true,
255
+ })
256
+ return deferNext ? null : next()
257
+ }
258
+
259
+ let body
260
+ try {
261
+ body = JSON.parse(text, reviver)
262
+ } catch (parseError) {
263
+ throw new Error(`Invalid JSON: ${parseError.message}`)
264
+ }
265
+
266
+ if (strict && typeof body !== 'object') {
267
+ throw new Error('JSON body must be an object or array')
268
+ }
269
+
270
+ Object.defineProperty(req, 'body', {
271
+ value: body,
272
+ writable: true,
273
+ enumerable: true,
274
+ configurable: true,
275
+ })
276
+
277
+ return deferNext ? null : next()
278
+ } catch (error) {
279
+ throw error
280
+ }
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Creates a text body parser middleware
286
+ * @param {Object} options - Body parser configuration
287
+ * @param {number|string} options.limit - Maximum body size in bytes
288
+ * @param {string} options.type - Content-Type to parse (default: text/*)
289
+ * @param {boolean} options.deferNext - If true, don't call next() and let caller handle it
290
+ * @returns {Function} Middleware function
291
+ */
292
+ function createTextParser(options = {}) {
293
+ const {limit = '1mb', type = 'text/', deferNext = false} = options
294
+
295
+ const parsedLimit = parseLimit(limit)
296
+
297
+ return async function textParserMiddleware(req, next) {
298
+ if (!hasBody(req) || !shouldParse(req, type)) {
299
+ return deferNext ? null : next()
300
+ }
301
+
302
+ try {
303
+ const contentLength = req.headers.get('content-length')
304
+ if (contentLength) {
305
+ const length = parseInt(contentLength)
306
+ if (isNaN(length) || length < 0) {
307
+ return new Response('Invalid content-length header', {status: 400})
308
+ }
309
+ if (length > parsedLimit) {
310
+ return new Response('Request body size exceeded', {status: 413})
311
+ }
312
+ }
313
+
314
+ const text = await req.text()
315
+ // Store raw body text for verification
316
+ req._rawBodyText = text
317
+
318
+ const textLength = new TextEncoder().encode(text).length
319
+ if (textLength > parsedLimit) {
320
+ return new Response('Request body size exceeded', {status: 413})
321
+ }
322
+
323
+ Object.defineProperty(req, 'body', {
324
+ value: text,
325
+ writable: true,
326
+ enumerable: true,
327
+ configurable: true,
328
+ })
329
+
330
+ return deferNext ? null : next()
331
+ } catch (error) {
332
+ throw error
333
+ }
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Creates a URL-encoded form parser middleware
339
+ * @param {Object} options - Body parser configuration
340
+ * @param {number|string} options.limit - Maximum body size in bytes
341
+ * @param {boolean} options.extended - Use extended query string parsing
342
+ * @param {boolean} options.parseNestedObjects - Parse nested object notation
343
+ * @param {boolean} options.deferNext - If true, don't call next() and let caller handle it
344
+ * @returns {Function} Middleware function
345
+ */
346
+ function createURLEncodedParser(options = {}) {
347
+ const {
348
+ limit = '1mb',
349
+ extended = true,
350
+ parseNestedObjects = true,
351
+ deferNext = false,
352
+ } = options
353
+
354
+ const parsedLimit = parseLimit(limit)
355
+
356
+ return async function urlEncodedParserMiddleware(req, next) {
357
+ if (
358
+ !hasBody(req) ||
359
+ !shouldParse(req, 'application/x-www-form-urlencoded')
360
+ ) {
361
+ return deferNext ? null : next()
362
+ }
363
+
364
+ try {
365
+ const contentLength = req.headers.get('content-length')
366
+ if (contentLength) {
367
+ const length = parseInt(contentLength)
368
+ if (isNaN(length) || length < 0) {
369
+ return new Response('Invalid content-length header', {status: 400})
370
+ }
371
+ if (length > parsedLimit) {
372
+ return new Response('Request body size exceeded', {status: 413})
373
+ }
374
+ }
375
+
376
+ const text = await req.text()
377
+ // Store raw body text for verification
378
+ req._rawBodyText = text
379
+
380
+ const textLength = new TextEncoder().encode(text).length
381
+ if (textLength > parsedLimit) {
382
+ return new Response('Request body size exceeded', {status: 413})
383
+ }
384
+
385
+ const body = {}
386
+ const params = new URLSearchParams(text)
387
+
388
+ // Prevent DoS through excessive parameters
389
+ let paramCount = 0
390
+ const maxParams = 1000 // Reasonable limit for URL-encoded parameters
391
+
392
+ for (const [key, value] of params.entries()) {
393
+ paramCount++
394
+ if (paramCount > maxParams) {
395
+ return new Response('Too many parameters', {status: 400})
396
+ }
397
+
398
+ // Validate key and value lengths to prevent memory exhaustion
399
+ if (key.length > 1000 || value.length > 10000) {
400
+ return new Response('Parameter too long', {status: 400})
401
+ }
402
+
403
+ if (parseNestedObjects) {
404
+ try {
405
+ parseNestedKey(body, key, value)
406
+ } catch (parseError) {
407
+ return new Response(
408
+ `Invalid parameter structure: ${parseError.message}`,
409
+ {status: 400},
410
+ )
411
+ }
412
+ } else {
413
+ // Protect against prototype pollution even when parseNestedObjects is false
414
+ const prototypePollutionKeys = [
415
+ '__proto__',
416
+ 'constructor',
417
+ 'prototype',
418
+ 'hasOwnProperty',
419
+ 'isPrototypeOf',
420
+ 'propertyIsEnumerable',
421
+ 'valueOf',
422
+ 'toString',
423
+ ]
424
+
425
+ if (!prototypePollutionKeys.includes(key)) {
426
+ if (body[key] !== undefined) {
427
+ if (Array.isArray(body[key])) {
428
+ body[key].push(value)
429
+ } else {
430
+ body[key] = [body[key], value]
431
+ }
432
+ } else {
433
+ body[key] = value
434
+ }
435
+ }
436
+ }
437
+ }
438
+
439
+ Object.defineProperty(req, 'body', {
440
+ value: body,
441
+ writable: true,
442
+ enumerable: true,
443
+ configurable: true,
444
+ })
445
+
446
+ return deferNext ? null : next()
447
+ } catch (error) {
448
+ throw error
449
+ }
450
+ }
451
+ }
452
+
453
+ /**
454
+ * Creates a multipart/form-data parser middleware
455
+ * @param {Object} options - Body parser configuration
456
+ * @param {number|string} options.limit - Maximum body size in bytes
457
+ * @param {boolean} options.deferNext - If true, don't call next() and let caller handle it
458
+ * @returns {Function} Middleware function
459
+ */
460
+ function createMultipartParser(options = {}) {
461
+ const {limit = '10mb', deferNext = false} = options
462
+
463
+ const parsedLimit = parseLimit(limit)
464
+
465
+ return async function multipartParserMiddleware(req, next) {
466
+ const contentType = req.headers.get('content-type')
467
+ if (!hasBody(req) || !contentType?.startsWith('multipart/form-data')) {
468
+ return deferNext ? null : next()
469
+ }
470
+
471
+ try {
472
+ const contentLength = req.headers.get('content-length')
473
+ if (contentLength) {
474
+ const length = parseInt(contentLength)
475
+ if (isNaN(length) || length < 0) {
476
+ return new Response('Invalid content-length header', {status: 400})
477
+ }
478
+ if (length > parsedLimit) {
479
+ return new Response('Request body size exceeded', {status: 413})
480
+ }
481
+ }
482
+
483
+ const formData = await req.formData()
484
+
485
+ // Calculate actual size of form data and validate
486
+ let totalSize = 0
487
+ let fieldCount = 0
488
+ const maxFields = 100 // Reasonable limit for form fields
489
+
490
+ for (const [key, value] of formData.entries()) {
491
+ fieldCount++
492
+ if (fieldCount > maxFields) {
493
+ return new Response('Too many form fields', {status: 400})
494
+ }
495
+
496
+ // Validate field name length
497
+ if (key.length > 1000) {
498
+ return new Response('Field name too long', {status: 400})
499
+ }
500
+
501
+ if (value instanceof File) {
502
+ totalSize += value.size
503
+ // Validate file name length for security
504
+ if (value.name && value.name.length > 255) {
505
+ return new Response('Filename too long', {status: 400})
506
+ }
507
+ // Validate file size individually
508
+ if (value.size > parsedLimit) {
509
+ return new Response('File too large', {status: 413})
510
+ }
511
+ } else {
512
+ const valueSize = new TextEncoder().encode(value).length
513
+ totalSize += valueSize
514
+ // Validate field value length
515
+ if (valueSize > 100000) {
516
+ // 100KB per field
517
+ return new Response('Field value too long', {status: 400})
518
+ }
519
+ }
520
+ totalSize += new TextEncoder().encode(key).length
521
+
522
+ // Check total size periodically to prevent memory exhaustion
523
+ if (totalSize > parsedLimit) {
524
+ return new Response('Request body size exceeded', {status: 413})
525
+ }
526
+ }
527
+
528
+ const body = {}
529
+ const files = {}
530
+
531
+ for (const [key, value] of formData.entries()) {
532
+ if (value instanceof File) {
533
+ const mimetype = value.type?.split(';')[0] || value.type
534
+ const fileData = new Uint8Array(await value.arrayBuffer())
535
+ files[key] = {
536
+ filename: value.name,
537
+ name: value.name,
538
+ size: value.size,
539
+ type: value.type,
540
+ mimetype: mimetype,
541
+ data: fileData,
542
+ }
543
+ } else {
544
+ if (body[key] !== undefined) {
545
+ if (Array.isArray(body[key])) {
546
+ body[key].push(value)
547
+ } else {
548
+ body[key] = [body[key], value]
549
+ }
550
+ } else {
551
+ body[key] = value
552
+ }
553
+ }
554
+ }
555
+
556
+ Object.defineProperty(req, 'body', {
557
+ value: body,
558
+ writable: true,
559
+ enumerable: true,
560
+ configurable: true,
561
+ })
562
+
563
+ Object.defineProperty(req, 'files', {
564
+ value: files,
565
+ writable: true,
566
+ enumerable: true,
567
+ configurable: true,
568
+ })
569
+
570
+ return deferNext ? null : next()
571
+ } catch (error) {
572
+ throw error
573
+ }
574
+ }
575
+ }
576
+
577
+ /**
578
+ * Combines multiple body parsers based on content type
579
+ * @param {Object} options - Configuration for each parser type
580
+ * @param {Object} options.json - JSON parser options
581
+ * @param {Object} options.text - Text parser options
582
+ * @param {Object} options.urlencoded - URL-encoded parser options
583
+ * @param {Object} options.multipart - Multipart parser options
584
+ * @param {string[]} options.jsonTypes - Custom JSON content types
585
+ * @param {Function} options.jsonParser - Custom JSON parser function
586
+ * @param {Function} options.onError - Custom error handler
587
+ * @param {Function} options.verify - Body verification function
588
+ * @param {boolean} options.parseNestedObjects - Parse nested object notation (for compatibility)
589
+ * @param {string|number} options.jsonLimit - JSON size limit (backward compatibility)
590
+ * @param {string|number} options.textLimit - Text size limit (backward compatibility)
591
+ * @param {string|number} options.urlencodedLimit - URL-encoded size limit (backward compatibility)
592
+ * @param {string|number} options.multipartLimit - Multipart size limit (backward compatibility)
593
+ * @returns {Function} Middleware function
594
+ */
595
+ function createBodyParser(options = {}) {
596
+ const {
597
+ json = {},
598
+ text = {},
599
+ urlencoded = {},
600
+ multipart = {},
601
+ jsonTypes = ['application/json'],
602
+ jsonParser,
603
+ onError,
604
+ verify,
605
+ parseNestedObjects = true,
606
+ // Backward compatibility for direct limit options
607
+ jsonLimit,
608
+ textLimit,
609
+ urlencodedLimit,
610
+ multipartLimit,
611
+ } = options
612
+
613
+ // Map configuration keys to actual limits for the parsers
614
+ const jsonOptions = {
615
+ ...json,
616
+ limit: jsonLimit || json.jsonLimit || json.limit || '1mb',
617
+ }
618
+ const textOptions = {
619
+ ...text,
620
+ limit: textLimit || text.textLimit || text.limit || '1mb',
621
+ }
622
+ const urlencodedOptions = {
623
+ ...urlencoded,
624
+ limit:
625
+ urlencodedLimit ||
626
+ urlencoded.urlencodedLimit ||
627
+ urlencoded.limit ||
628
+ '1mb',
629
+ parseNestedObjects:
630
+ urlencoded.parseNestedObjects !== undefined
631
+ ? urlencoded.parseNestedObjects
632
+ : parseNestedObjects,
633
+ }
634
+ const multipartOptions = {
635
+ ...multipart,
636
+ limit:
637
+ multipartLimit || multipart.multipartLimit || multipart.limit || '10mb',
638
+ }
639
+
640
+ // Create parsers with custom types consideration
641
+ const jsonParserMiddleware = createJSONParser({
642
+ ...jsonOptions,
643
+ type: 'application/', // Broad match for JSON types
644
+ deferNext: !!verify, // Defer next if verification is enabled
645
+ })
646
+ const textParserMiddleware = createTextParser({
647
+ ...textOptions,
648
+ deferNext: !!verify,
649
+ })
650
+ const urlEncodedParserMiddleware = createURLEncodedParser({
651
+ ...urlencodedOptions,
652
+ deferNext: !!verify,
653
+ })
654
+ const multipartParserMiddleware = createMultipartParser({
655
+ ...multipartOptions,
656
+ deferNext: !!verify,
657
+ })
658
+
659
+ return async function bodyParserMiddleware(req, next) {
660
+ const contentType = req.headers.get('content-type')
661
+
662
+ // For GET requests or requests without body, set body to undefined
663
+ if (!hasBody(req)) {
664
+ Object.defineProperty(req, 'body', {
665
+ value: undefined,
666
+ writable: true,
667
+ enumerable: true,
668
+ configurable: true,
669
+ })
670
+ return next()
671
+ }
672
+
673
+ // If no content type, set body to undefined
674
+ if (!contentType) {
675
+ Object.defineProperty(req, 'body', {
676
+ value: undefined,
677
+ writable: true,
678
+ enumerable: true,
679
+ configurable: true,
680
+ })
681
+ return next()
682
+ }
683
+
684
+ try {
685
+ let result
686
+
687
+ // Custom JSON parser handling for custom JSON types (case-insensitive)
688
+ if (jsonParser && isJsonType(contentType, jsonTypes)) {
689
+ const text = await req.text()
690
+ const body = jsonParser(text)
691
+ Object.defineProperty(req, 'body', {
692
+ value: body,
693
+ writable: true,
694
+ enumerable: true,
695
+ configurable: true,
696
+ })
697
+
698
+ // No result set, will be handled after verification
699
+ } else {
700
+ // Check if content type matches any JSON types first (including custom ones)
701
+ if (isJsonType(contentType, jsonTypes)) {
702
+ result = await jsonParserMiddleware(req, next)
703
+ } else {
704
+ // Route to appropriate parser based on content type (case-insensitive)
705
+ const lowerContentType = contentType.toLowerCase()
706
+ if (lowerContentType.includes('application/json')) {
707
+ result = await jsonParserMiddleware(req, next)
708
+ } else if (
709
+ lowerContentType.includes('application/x-www-form-urlencoded')
710
+ ) {
711
+ result = await urlEncodedParserMiddleware(req, next)
712
+ } else if (lowerContentType.includes('multipart/form-data')) {
713
+ result = await multipartParserMiddleware(req, next)
714
+ } else if (lowerContentType.includes('text/')) {
715
+ result = await textParserMiddleware(req, next)
716
+ } else {
717
+ // For unsupported content types, set body to undefined
718
+ Object.defineProperty(req, 'body', {
719
+ value: undefined,
720
+ writable: true,
721
+ enumerable: true,
722
+ configurable: true,
723
+ })
724
+ result = verify ? null : next() // Defer if verification enabled
725
+ }
726
+ }
727
+ }
728
+
729
+ // If a parser returned an error response, return it immediately
730
+ if (result && result instanceof Response) {
731
+ return result
732
+ }
733
+
734
+ // Apply verification after parsing if provided
735
+ if (verify && req.body !== undefined) {
736
+ try {
737
+ // For verification, we need to pass the raw body text
738
+ // Get the original text/data that was parsed
739
+ let rawBody = ''
740
+ if (req._rawBodyText) {
741
+ rawBody = req._rawBodyText
742
+ }
743
+ verify(req, rawBody)
744
+ } catch (verifyError) {
745
+ // Sanitize error message to prevent information leakage
746
+ const sanitizedMessage = verifyError.message
747
+ ? verifyError.message.substring(0, 100)
748
+ : 'Verification failed'
749
+ return new Response(`Verification failed: ${sanitizedMessage}`, {
750
+ status: 400,
751
+ })
752
+ }
753
+ }
754
+
755
+ // If result is null (deferred) or verification passed, call next
756
+ return result || next()
757
+ } catch (error) {
758
+ if (onError) {
759
+ return onError(error, req, next)
760
+ }
761
+ // Sanitize error message to prevent information leakage
762
+ const sanitizedMessage = error.message
763
+ ? error.message.substring(0, 100)
764
+ : 'Body parsing failed'
765
+ return new Response(sanitizedMessage, {status: 400})
766
+ }
767
+ }
768
+ }
769
+
770
+ // CommonJS exports
771
+ module.exports = {
772
+ createBodyParser,
773
+ createJSONParser,
774
+ createTextParser,
775
+ createURLEncodedParser,
776
+ createMultipartParser,
777
+ hasBody,
778
+ shouldParse,
779
+ parseLimit,
780
+ }
781
+
782
+ // Default export is the main body parser
783
+ module.exports.default = createBodyParser