fulmine.js 5.0.0-rc.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,636 @@
1
+ /*
2
+ Copyright 2024 dimden.dev
3
+ Copyright 2026 Nigro Simone
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
16
+ */
17
+
18
+ const fs = require("fs");
19
+ const path = require("path");
20
+ const bytes = require("bytes");
21
+ const zlib = require("fast-zlib");
22
+ const typeis = require("type-is");
23
+ const querystring = require("fast-querystring");
24
+ const { AsyncResource } = require("async_hooks");
25
+ const { fastQueryParse, NullObject, asStatError, httpError, memoizeByString } = require("./utils.js");
26
+
27
+ // largest content-length we will allocate a body buffer for up front. above this the body is
28
+ // collected chunk by chunk instead, so a declared-but-unsent body cannot pin more memory than a
29
+ // real one of the same size would
30
+ const MAX_PREALLOCATED_BODY = 1024 * 1024;
31
+
32
+ // The failures express.static answers by moving on to the next handler rather than by reporting
33
+ // them, when fallthrough is on. They all mean the same thing: the request is not a file here.
34
+ //
35
+ // serve-static decides this by remembering whether send got as far as settling on a file, and
36
+ // forwards everything after that point. The list is the same thing said from the other side, since
37
+ // by the time this hands over, the file has been found and stat'ed already: what is left to fail
38
+ // is a dotfile rule or a path that will not decode.
39
+ //
40
+ // A 412 and a 416 are not on it, and that is the point of the list. Both are about a file that
41
+ // exists and about conditions the client itself set, and falling through swallowed them: a Range
42
+ // Not Satisfiable came back as a 404, which tells the client its file is gone when it is not.
43
+ const FALLTHROUGH_STATUSES = new Set([400, 403, 404]);
44
+
45
+ /**
46
+ * A path with any run of leading slashes reduced to one.
47
+ *
48
+ * This is not tidiness. A Location header beginning with "//" is a protocol-relative URL, so a
49
+ * browser given "//assets/" goes to the host called "assets" rather than to a path on this server.
50
+ * serve-static collapses them for exactly that reason, and a redirect that leaves the server is
51
+ * not a redirect the server meant to issue.
52
+ *
53
+ * @param {string} path
54
+ * @returns {string}
55
+ */
56
+ function collapseLeadingSlashes(path) {
57
+ let i = 0;
58
+ while (i < path.length && path.charCodeAt(i) === 0x2f) {
59
+ i++;
60
+ }
61
+ return i > 1 ? "/" + path.slice(i) : path;
62
+ }
63
+
64
+ /**
65
+ * The text without a leading byte order mark, which is what a decoder would have handed over.
66
+ * body-parser decodes through iconv and iconv removes it, so nothing downstream of it ever sees
67
+ * one; reading the buffer directly, as here, means removing it explicitly.
68
+ *
69
+ * @param {string} text
70
+ * @returns {string}
71
+ */
72
+ function stripBom(text) {
73
+ return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
74
+ }
75
+
76
+ /**
77
+ * The message a strict violation gets, which is the one V8 would have produced had the body been
78
+ * invalid JSON rather than merely not an object.
79
+ *
80
+ * body-parser goes to some trouble over this: it builds a string that is the body up to the
81
+ * offending character followed by placeholder characters, asks JSON.parse to fail on that, and
82
+ * then puts the real characters back into whatever V8 said. The point is that an application
83
+ * showing err.message reads the same sentence either way, naming the character and its position.
84
+ *
85
+ * @param {string} text the body as sent
86
+ * @param {string|undefined} char the first character that is neither whitespace nor { nor [
87
+ * @returns {string}
88
+ */
89
+ function strictSyntaxMessage(text, char) {
90
+ const index = char === undefined ? -1 : text.indexOf(char);
91
+ if (index === -1) {
92
+ return "Unexpected end of JSON input";
93
+ }
94
+ const partial = text.substring(0, index) + "#".repeat(text.length - index);
95
+ try {
96
+ JSON.parse(partial);
97
+ } catch (e) {
98
+ // put the real characters back where the placeholders were named
99
+ return /** @type {any} */ (e).message.replace(/#+/g, (/** @type {string} */ placeholder) =>
100
+ text.substring(index, index + placeholder.length)
101
+ );
102
+ }
103
+ return "strict violation";
104
+ }
105
+
106
+ /**
107
+ * The error a body parser hands to next(), shaped as body-parser shapes it: with a status, since
108
+ * `res.status(err.status || 500)` would otherwise answer 500 to a request that was merely too
109
+ * large, and with `type`, which applications branch on.
110
+ *
111
+ * @param {string} message
112
+ * @param {number} status
113
+ * @param {string} type body-parser's own name for the kind of failure
114
+ * @param {object} [extra] anything else body-parser puts on that particular error
115
+ * @returns {Error}
116
+ */
117
+ function bodyError(message, status, type, extra) {
118
+ const err = /** @type {any} */ (new Error(message));
119
+ // 4xx is the client's to see; a 5xx here would be the server's own problem and stays hidden
120
+ err.expose = status < 500;
121
+ err.statusCode = status;
122
+ err.status = status;
123
+ err.type = type;
124
+ return Object.assign(err, extra);
125
+ }
126
+
127
+ /**
128
+ * express.static, which is a thin front for res.sendFile: it resolves the path, refuses anything
129
+ * that climbs out of the root, applies the dotfiles and index rules, and hands the rest over.
130
+ *
131
+ * @param {string} root directory to serve from
132
+ * @param {object} [options] index, redirect, fallthrough, dotfiles, extensions, setHeaders, etag
133
+ * @returns {(req: any, res: any, next: (err?: any) => void) => any}
134
+ */
135
+ function serveStatic(root, options) {
136
+ if (!options) options = new NullObject();
137
+ if (typeof options.index === "undefined") options.index = "index.html";
138
+ if (typeof options.redirect === "undefined") options.redirect = true;
139
+ if (typeof options.fallthrough === "undefined") options.fallthrough = true;
140
+ if (typeof options.dotfiles === "undefined") options.dotfiles = "ignore";
141
+ if (options.extensions) {
142
+ if (typeof options.extensions !== "string" && !Array.isArray(options.extensions)) {
143
+ throw new Error("extensions must be a string or an array");
144
+ }
145
+ if (!Array.isArray(options.extensions)) {
146
+ options.extensions = [options.extensions];
147
+ }
148
+ options.extensions = options.extensions.map((ext) => (ext.startsWith(".") ? ext.slice(1) : ext));
149
+ }
150
+ options.root = root;
151
+ // serve-static decides this for itself and never asks the app, so a static file keeps its
152
+ // ETag under app.set("etag", false) and only { etag: false } here turns it off. res.sendFile
153
+ // takes the app's setting instead, which is why this has to be said out loud.
154
+ options.etag = options.etag !== false;
155
+ options._ownEtag = true;
156
+
157
+ return (req, res, next) => {
158
+ const iq = req.url.indexOf("?");
159
+ let url;
160
+
161
+ next = AsyncResource.bind(next);
162
+
163
+ try {
164
+ url = decodeURIComponent(iq !== -1 ? req.url.substring(0, iq) : req.url);
165
+ } catch (e) {
166
+ // 400 and not 404: send answers a path it cannot decode with a Bad Request, since
167
+ // nothing was asked for that could be missing
168
+ if (!options.fallthrough) {
169
+ res.status(400);
170
+ return next(httpError(400));
171
+ } else return next();
172
+ }
173
+ let _path = url;
174
+ const fullpath = path.resolve(path.join(options.root, url));
175
+ if (options.root && !fullpath.startsWith(path.resolve(options.root))) {
176
+ if (!options.fallthrough) {
177
+ res.status(403);
178
+ return next(httpError(403));
179
+ } else return next();
180
+ }
181
+
182
+ let stat;
183
+ try {
184
+ stat = fs.statSync(fullpath);
185
+ } catch (err) {
186
+ const ext = path.extname(fullpath);
187
+ let i = 0;
188
+ if (ext === "" && options.extensions) {
189
+ while (i < options.extensions.length) {
190
+ try {
191
+ stat = fs.statSync(fullpath + "." + options.extensions[i]);
192
+ _path = url + "." + options.extensions[i];
193
+ break;
194
+ } catch (err) {
195
+ i++;
196
+ }
197
+ }
198
+ }
199
+ if (!stat) {
200
+ if (!options.fallthrough) {
201
+ res.status(404);
202
+ // the error itself, not its message: serve-static hands the fs error to the
203
+ // error handler with its errno, code, syscall and path still on it, and an
204
+ // error handler doing res.send(err) sends those as JSON. Passing the string
205
+ // sent an HTML page instead.
206
+ return next(asStatError(err));
207
+ } else return next();
208
+ }
209
+ }
210
+
211
+ if (stat.isDirectory()) {
212
+ if (!req.endsWithSlash) {
213
+ if (options.redirect) {
214
+ // The query goes along, and the leading slashes are collapsed. Both were
215
+ // wrong: "/docs?page=3" redirected to "/docs/" and lost the page, and a
216
+ // request for "//assets" answered "Location: //assets/", which a browser
217
+ // reads as a protocol-relative URL and follows to the host "assets". A
218
+ // redirect that leaves this server is not a redirect this server meant.
219
+ return res.redirect(301, collapseLeadingSlashes(req._originalPath + "/") + req.urlQuery, true);
220
+ } else {
221
+ if (!options.fallthrough) {
222
+ res.status(404);
223
+ return next(httpError(404));
224
+ } else return next();
225
+ }
226
+ }
227
+ if (options.index) {
228
+ try {
229
+ stat = fs.statSync(path.join(fullpath, options.index));
230
+ _path = path.join(url, options.index);
231
+ } catch (err) {
232
+ if (!options.fallthrough) {
233
+ res.status(404);
234
+ // the fs error, as above: the index file is missing and the error handler
235
+ // is told which one and where
236
+ return next(asStatError(err));
237
+ } else return next();
238
+ }
239
+ } else {
240
+ // a directory with no index to serve is a Not Found, and saying so is the whole
241
+ // point of fallthrough: false. This moved on to the next handler instead, so the
242
+ // application's own 404 answered where serve-static's error handler should have.
243
+ if (!options.fallthrough) {
244
+ res.status(404);
245
+ return next(httpError(404));
246
+ }
247
+ return next();
248
+ }
249
+ }
250
+
251
+ options._stat = stat;
252
+
253
+ return res.sendFile(_path, options, (e) => {
254
+ if (e) {
255
+ next(options.fallthrough && FALLTHROUGH_STATUSES.has(e.status) ? undefined : e);
256
+ }
257
+ });
258
+ };
259
+ }
260
+
261
+ /**
262
+ * The decompressor for a Content-Encoding, or undefined when the body is not compressed. An
263
+ * encoding nobody knows throws, since decoding it wrong is worse than refusing.
264
+ *
265
+ * @param {string|undefined} contentEncoding
266
+ * @returns {any|undefined}
267
+ */
268
+ function createInflate(contentEncoding) {
269
+ const encoding = (contentEncoding || "identity").toLowerCase();
270
+ switch (encoding) {
271
+ case "identity":
272
+ return;
273
+ case "deflate":
274
+ return new zlib.Inflate();
275
+ case "gzip":
276
+ return new zlib.Gunzip();
277
+ case "br":
278
+ return new zlib.BrotliDecompress();
279
+ default:
280
+ return false;
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Builds one of the body parsers. All four share the same work, which is deciding whether this
286
+ * request has a body worth reading, collecting it within the size limit, decompressing it and
287
+ * handing the bytes over; they differ only in the content type they claim by default and in what
288
+ * they turn the bytes into.
289
+ *
290
+ * @param {string} defaultType the type matched when the caller names none
291
+ * @param {(...args: any[]) => any} beforeReturn turns the collected bytes into req.body. Called
292
+ * with the body, the request, the response, next and the options
293
+ * @returns {(options?: object) => Function} the middleware factory
294
+ */
295
+ function createBodyParser(defaultType, beforeReturn) {
296
+ return function (options) {
297
+ if (typeof options !== "object") {
298
+ options = new NullObject();
299
+ }
300
+ if (typeof options.limit === "undefined") options.limit = bytes("100kb");
301
+ else options.limit = bytes(options.limit);
302
+
303
+ if (typeof options.inflate === "undefined") options.inflate = true;
304
+ if (typeof options.type === "undefined") options.type = defaultType;
305
+ if (typeof options.type === "string") {
306
+ if (!options.type.includes("*")) {
307
+ options.simpleType = options.type;
308
+ }
309
+ options.type = [options.type];
310
+ } else if (typeof options.type !== "function" && !Array.isArray(options.type)) {
311
+ throw new Error("type must be a string, function or an array");
312
+ }
313
+ if (typeof options.defaultCharset === "undefined") options.defaultCharset = "utf-8";
314
+
315
+ // Whether a content-type is one this parser claims, remembered per parser.
316
+ //
317
+ // Only reached when the caller asked for a wildcard or a list, since a plain type takes the
318
+ // simpleType shortcut above and never calls type-is at all. For those callers type-is was
319
+ // 513 ns to reach the same answer about the same string on every request, against 4 ns for
320
+ // an answer already worked out. The header is the client's, so the memo needs its ceiling.
321
+ //
322
+ // typeis.is and not typeis(req, ...): the request form first checks that there is a body,
323
+ // and the caller below has established that already.
324
+ const claimsType = memoizeByString((contentType) => !!typeis.is(contentType, options.type));
325
+
326
+ let additionalMethods;
327
+
328
+ return (req, res, next) => {
329
+ next = AsyncResource.bind(next);
330
+
331
+ // skip reading body twice
332
+ if (req.bodyRead) {
333
+ return next();
334
+ }
335
+
336
+ const type = req.headers["content-type"];
337
+
338
+ // req.body is deliberately left undefined until a parser claims the request. That is
339
+ // what lets a handler tell "nothing parsed this" apart from "the body was empty",
340
+ // so it must not be seeded with an empty object first.
341
+
342
+ // skip reading body for no content type
343
+ if (!type) {
344
+ return next();
345
+ }
346
+
347
+ const length = req.headers["content-length"];
348
+
349
+ // No content-length and no transfer-encoding means the request carries no body at all,
350
+ // and a body parser must leave it alone rather than parse nothing into an empty value.
351
+ // type-is applies this before matching the type, but the simpleType shortcut below
352
+ // compares strings directly and would otherwise skip the check.
353
+ if (req.headers["transfer-encoding"] === undefined && isNaN(length)) {
354
+ return next();
355
+ }
356
+
357
+ if (options.simpleType) {
358
+ const semicolonIndex = type.indexOf(";");
359
+ const clearType = semicolonIndex !== -1 ? type.substring(0, semicolonIndex) : type;
360
+ if (clearType !== options.simpleType) {
361
+ return next();
362
+ }
363
+ } else {
364
+ if (typeof options.type === "function") {
365
+ if (!options.type(req)) {
366
+ return next();
367
+ }
368
+ } else {
369
+ if (!claimsType(type)) {
370
+ return next();
371
+ }
372
+ }
373
+ }
374
+
375
+ // an empty body still has to produce this parser's empty value the way express does -
376
+ // {} for json and urlencoded, '' for text, an empty Buffer for raw - rather than leaving
377
+ // req.body as the placeholder object. there is nothing to read, so run the tail directly
378
+ if (length == "0") {
379
+ return beforeReturn(req, res, next, options, Buffer.alloc(0));
380
+ }
381
+
382
+ // skip reading too large body
383
+ if (length && +length > options.limit) {
384
+ return next(
385
+ bodyError("request entity too large", 413, "entity.too.large", {
386
+ expected: +length,
387
+ length: +length,
388
+ limit: options.limit
389
+ })
390
+ );
391
+ }
392
+
393
+ // skip reading body for non-POST requests
394
+ // this makes it +10k req/sec faster
395
+ if (additionalMethods === undefined) additionalMethods = req.app.get("body methods") ?? null;
396
+ if (
397
+ req.method !== "POST" &&
398
+ req.method !== "PUT" &&
399
+ req.method !== "PATCH" &&
400
+ req.method !== "QUERY" &&
401
+ (!additionalMethods || !additionalMethods.includes(req.method))
402
+ ) {
403
+ return next();
404
+ }
405
+
406
+ const abs = [];
407
+ let inflate;
408
+ let totalSize = 0;
409
+ if (options.inflate) {
410
+ inflate = createInflate(req.headers["content-encoding"]);
411
+ if (inflate === false) {
412
+ return next(
413
+ bodyError(
414
+ 'unsupported content encoding "' + req.headers["content-encoding"] + '"',
415
+ 415,
416
+ "encoding.unsupported",
417
+ { encoding: req.headers["content-encoding"] }
418
+ )
419
+ );
420
+ }
421
+ }
422
+
423
+ // uWS neuters its ArrayBuffer after the callback, so every chunk has to be copied out of
424
+ // it - and then Buffer.concat copied the whole body a second time. when content-length is
425
+ // known and we aren't inflating, the final size is known up front, so chunks can go
426
+ // straight into one buffer and the body is copied once.
427
+ // the cap means a client that declares a body and never sends it costs no more than one
428
+ // that actually sends a body that size, and content-length above options.limit was
429
+ // already rejected above
430
+ const declaredLength = inflate ? -1 : Number(length);
431
+ let target =
432
+ declaredLength > 0 && declaredLength <= MAX_PREALLOCATED_BODY
433
+ ? Buffer.allocUnsafe(declaredLength)
434
+ : null;
435
+ let targetOffset = 0;
436
+
437
+ req.bodyRead = true;
438
+
439
+ // uWS keeps delivering chunks after we reject an oversized body, and the
440
+ // stream path still emits 'end', so without this every further chunk would
441
+ // call next() again and the second response would throw
442
+ let finished = false;
443
+
444
+ /**
445
+ * One chunk from uWS. Decompresses it, counts it against the limit and keeps it. The
446
+ * finished flag matters: uWS goes on delivering chunks after an oversized body has
447
+ * been refused, and without it every further chunk would answer the request again.
448
+ *
449
+ * @param {any} buf a Buffer, or an ArrayBuffer straight from uWS
450
+ */
451
+ function onData(buf) {
452
+ if (finished) {
453
+ return;
454
+ }
455
+ if (!Buffer.isBuffer(buf)) {
456
+ buf = Buffer.from(buf);
457
+ }
458
+ if (inflate) {
459
+ buf = inflate.process(buf);
460
+ }
461
+
462
+ totalSize += buf.length;
463
+ if (totalSize > options.limit) {
464
+ finished = true;
465
+ abs.length = 0;
466
+ target = null;
467
+ return next(
468
+ bodyError("request entity too large", 413, "entity.too.large", {
469
+ limit: options.limit,
470
+ received: totalSize
471
+ })
472
+ );
473
+ }
474
+
475
+ if (target) {
476
+ if (targetOffset + buf.length <= target.length) {
477
+ buf.copy(target, targetOffset);
478
+ targetOffset += buf.length;
479
+ return;
480
+ }
481
+ // more body than content-length promised: keep what we have and fall back
482
+ abs.push(Buffer.from(target.subarray(0, targetOffset)));
483
+ target = null;
484
+ }
485
+
486
+ // shallow copy, to avoid shared references for large bodies.
487
+ abs.push(Buffer.from(buf));
488
+ }
489
+
490
+ /** The body is complete: assemble it, hand it to the parser and continue routing. */
491
+ function onEnd() {
492
+ if (finished) {
493
+ return;
494
+ }
495
+ finished = true;
496
+ // target holds the whole body already; otherwise a single chunk is the body, and
497
+ // only a genuinely chunked body needs the concat
498
+ const buf = target
499
+ ? targetOffset === target.length
500
+ ? target
501
+ : target.subarray(0, targetOffset)
502
+ : abs.length === 1
503
+ ? abs[0]
504
+ : Buffer.concat(abs);
505
+ if (options.verify) {
506
+ try {
507
+ options.verify(req, res, buf);
508
+ } catch (e) {
509
+ const err = /** @type {any} */ (e);
510
+ return next(
511
+ bodyError(
512
+ err.message,
513
+ err.status ?? err.statusCode ?? 403,
514
+ err.type ?? "entity.verify.failed",
515
+ {
516
+ body: buf,
517
+ stack: err.stack
518
+ }
519
+ )
520
+ );
521
+ }
522
+ }
523
+ beforeReturn(req, res, next, options, buf);
524
+ }
525
+
526
+ // reading data directly from uWS is faster than from a stream
527
+ // if we are fast enough (not async), we can do it
528
+ // otherwise we need to use a stream since it already started streaming it
529
+ if (!req.receivedData) {
530
+ req._res.onData((ab, isLast) => {
531
+ onData(ab);
532
+ if (isLast) {
533
+ onEnd();
534
+ }
535
+ });
536
+ } else {
537
+ req.on("data", onData);
538
+ req.on("end", onEnd);
539
+ }
540
+ };
541
+ };
542
+ }
543
+
544
+ const json = createBodyParser("application/json", function (req, res, next, options, buf) {
545
+ if (buf.length === 0) {
546
+ req.body = {};
547
+ return next();
548
+ }
549
+ // A leading byte order mark is removed rather than parsed. body-parser never sees one either:
550
+ // it decodes through iconv, which strips it, so by the time the first character is looked at
551
+ // the mark is gone. JSON.parse would refuse it, so without this a body saved by an editor that
552
+ // writes a BOM is answered 400 here and 200 by Express.
553
+ const text = stripBom(buf.toString());
554
+
555
+ // "strict" means only an object or an array is a body, which is body-parser's default and was
556
+ // not honoured here at all: the check read req.body before this function had parsed anything,
557
+ // so it looked at the previous request's body and passed. `express.json()` accepted a bare
558
+ // string or number where Express answers 400.
559
+ if (options.strict !== false) {
560
+ // Exactly the four characters body-parser skips, and no more. A BOM or a non-breaking
561
+ // space is not whitespace to it, so a body starting with one is a violation rather than
562
+ // something to skip past, and a wider class here would accept bodies Express refuses.
563
+ // eslint-disable-next-line no-control-regex
564
+ const first = text.match(/^[\x20\x09\x0a\x0d]*([^\x20\x09\x0a\x0d])/)?.[1];
565
+ if (first !== "{" && first !== "[") {
566
+ return next(bodyError(strictSyntaxMessage(text, first), 400, "entity.parse.failed", { body: text }));
567
+ }
568
+ }
569
+
570
+ try {
571
+ req.body = JSON.parse(text, options.reviver);
572
+ } catch (e) {
573
+ // the JSON error's own message, which is what body-parser keeps, so an application showing
574
+ // err.message still says where the parse gave up
575
+ const err = /** @type {any} */ (e);
576
+ return next(bodyError(err.message, 400, "entity.parse.failed", { body: text }));
577
+ }
578
+
579
+ next();
580
+ });
581
+
582
+ const raw = createBodyParser("application/octet-stream", function (req, res, next, options, buf) {
583
+ req.body = buf;
584
+ next();
585
+ });
586
+
587
+ const text = createBodyParser("text/plain", function (req, res, next, options, buf) {
588
+ const contentType = req.headers["content-type"];
589
+ const charsetIndex = contentType.indexOf("charset=");
590
+ let encoding = options.defaultCharset;
591
+ if (charsetIndex !== -1) {
592
+ encoding = contentType.substring(charsetIndex + 8);
593
+ const semicolonIndex = encoding.indexOf(";");
594
+ if (semicolonIndex !== -1) {
595
+ encoding = encoding.substring(0, semicolonIndex);
596
+ }
597
+ encoding = encoding.trim().toLowerCase();
598
+ }
599
+ if (encoding !== "utf-8" && encoding !== "utf-16le" && encoding !== "latin1") {
600
+ return next(
601
+ bodyError('unsupported charset "' + encoding.toUpperCase() + '"', 415, "charset.unsupported", {
602
+ charset: encoding.toLowerCase()
603
+ })
604
+ );
605
+ }
606
+ try {
607
+ req.body = buf.toString(encoding);
608
+ } catch (e) {
609
+ return next(e);
610
+ }
611
+
612
+ next();
613
+ });
614
+
615
+ const urlencoded = createBodyParser("application/x-www-form-urlencoded", function (req, res, next, options, buf) {
616
+ try {
617
+ // Express 5 defaults extended to false, so nested keys need opting in
618
+ const extended = typeof options.extended !== "undefined" ? options.extended : false;
619
+ if (extended) {
620
+ req.body = fastQueryParse(buf.toString(), options);
621
+ } else {
622
+ req.body = querystring.parse(buf.toString());
623
+ }
624
+ } catch (e) {
625
+ return next(e);
626
+ }
627
+ next();
628
+ });
629
+
630
+ module.exports = {
631
+ static: serveStatic,
632
+ json,
633
+ raw,
634
+ text,
635
+ urlencoded
636
+ };