fulmine.js 5.12.1 → 5.12.3
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.
- package/README.md +1 -0
- package/package.json +6 -3
- package/src/application.js +2 -2
- package/src/declarative.js +16 -0
- package/src/request.js +98 -17
- package/src/router.js +137 -25
- package/src/utils.js +72 -18
package/README.md
CHANGED
|
@@ -285,6 +285,7 @@ A single-stage `node:26-trixie-slim` image works too if you `apt-get install -y
|
|
|
285
285
|
- `app.listen()` returns the app rather than a separate server object, and the app answers as an `http.Server`: `app instanceof http.Server` is true, which is what the graceful shutdown wrappers and the connection trackers look for. There is still no node server underneath, the socket belongs to µWS, so what is answered is the surface and not the plumbing. There: `close()`, `address()`, `listening`, `getConnections()`, `ref()`, `unref()`, `setTimeout()` and the `keepAliveTimeout` family. Not there: nothing emits `connection`, `request` or `upgrade`, `getConnections()` counts the requests in flight rather than sockets, and the timeouts belong to µWS and are set through `uwsOptions.idleTimeout`. Anything that wants to serve its own protocol on the socket, socket.io being the usual case, still wants `app.uwsApp`. Runnable: [`examples/graceful-shutdown.js`](./examples/graceful-shutdown.js).
|
|
286
286
|
- `x-powered-by` is disabled by default. Express sends `X-Powered-By: Express` unless you turn it off; Fulmine does not send it unless you turn it on with `app.set("x-powered-by", true)`. The header only tells anyone asking which framework is running.
|
|
287
287
|
- request body is only read for POST, PUT, PATCH and QUERY requests by default. You can add additional methods by setting `body methods` to array with uppercased methods.
|
|
288
|
+
- **A request whose framing cannot be trusted is refused by hanging up, with no answer at all.** Node's parser refuses each of these with a `400` and Fulmine refuses the same ones: a repeated `Content-Length`; one that is not a plain count of bytes, an empty value or a count past `Number.MAX_SAFE_INTEGER` included; a `Transfer-Encoding` whose last coding is not `chunked`; and a method nobody defines, which includes a lowercase one, since methods are case sensitive. µWS accepts all of them. It frames the request on the first length, or on no body at all, and it takes any token as a method, so `{"a":1}GET /path HTTP/1.1` is a request line to it. What the client sent as a body is then read as the next request on the connection: that is request smuggling, and a proxy in front disagreeing about the framing is all it takes. The answer differs from Express because it cannot be helped. µWS only skips the request it has already queued when the response is closed rather than completed, and writing the `400` completes it, so the choice is between telling the client and stopping the smuggled request. Nothing well behaved sends any of these.
|
|
288
289
|
- **A compiled route answers `connection: keep-alive` to a client that sent `Connection: close`.** A handler simple enough to be read at registration time is answered by µWS from a response written once at `listen()`, and that response cannot read the request. The socket still closes, so what is wrong is the header and not the transport. A response that would carry a validator is never compiled, so conditional requests behave as on Express; `app.set("declarative responses", false)` turns compiling off.
|
|
289
290
|
- **Informational responses go nowhere.** `res.writeEarlyHints()`, `res.writeContinue()` and `res.writeProcessing()` are all there, take what node's take and throw what node's throw once the head has gone out, but nothing reaches the wire: µWebSockets.js has no API for a `1xx`. They exist so that code written for Express keeps running rather than dying on "is not a function", which is the only thing a drop-in can honestly promise here. `res.addTrailers()` is the same story, and `res.setTimeout()` and `req.setTimeout()` register the listener without changing anything, since µWS runs its own idle timeout through `uwsOptions.idleTimeout`.
|
|
290
291
|
- For HTTPS, instead of doing this:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fulmine.js",
|
|
3
|
-
"version": "5.12.
|
|
3
|
+
"version": "5.12.3",
|
|
4
4
|
"description": "Drop-in Express 5 replacement on uWebSockets.js. Your existing middleware keeps working.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -28,7 +28,10 @@
|
|
|
28
28
|
"release:local": "node tools/release-local.js",
|
|
29
29
|
"cover:full": "nyc --silent npm run test && nyc --silent --no-clean npm run test:unit && nyc --silent --no-clean npm run test:express && nyc report",
|
|
30
30
|
"cover:check": "nyc check-coverage --statements 94.5 --branches 90 --functions 94 --lines 94.5",
|
|
31
|
-
"examples:install": "npm --prefix examples install"
|
|
31
|
+
"examples:install": "npm --prefix examples install",
|
|
32
|
+
"fuzz:wire": "node tools/wire-fuzz.js",
|
|
33
|
+
"fuzz:headers": "node tools/header-fuzz.js",
|
|
34
|
+
"fuzz:session": "node tools/session-fuzz.js"
|
|
32
35
|
},
|
|
33
36
|
"engines": {
|
|
34
37
|
"node": ">=22"
|
|
@@ -72,7 +75,7 @@
|
|
|
72
75
|
"bytes": "^3.1.2",
|
|
73
76
|
"compressible": "^2.0.18",
|
|
74
77
|
"content-disposition": "^1.1.0",
|
|
75
|
-
"cookie": "^
|
|
78
|
+
"cookie": "^0.7.2",
|
|
76
79
|
"cookie-signature": "^1.2.2",
|
|
77
80
|
"encodeurl": "^2.0.0",
|
|
78
81
|
"fast-decode-uri-component": "^1.0.1",
|
package/src/application.js
CHANGED
|
@@ -529,8 +529,8 @@ class Application extends Router {
|
|
|
529
529
|
_serveGeneric(res, req) {
|
|
530
530
|
const request = this.handleRequest(res, req);
|
|
531
531
|
const response = request.res;
|
|
532
|
-
if (request.
|
|
533
|
-
return this.
|
|
532
|
+
if (request._mustRefuse === true) {
|
|
533
|
+
return this._refuseRequest(response);
|
|
534
534
|
}
|
|
535
535
|
try {
|
|
536
536
|
this._routeRequestDirect(request, response);
|
package/src/declarative.js
CHANGED
|
@@ -54,6 +54,11 @@ const MAX_INSTRUCTION_LENGTH = 65535;
|
|
|
54
54
|
// so it cannot honour one, and a handler that sets one has to stay on the ordinary path.
|
|
55
55
|
const VALIDATOR_HEADERS = new Set(["etag", "last-modified"]);
|
|
56
56
|
|
|
57
|
+
// The statuses whose message carries no content. 205 is here for node's reason rather than
|
|
58
|
+
// express's: express strips the body for 204 and 304, and node answers a 205 with a lone
|
|
59
|
+
// Content-Length of zero, so all three come out of the ordinary path with no body at all.
|
|
60
|
+
const BODILESS_STATUSES = new Set([204, 205, 304]);
|
|
61
|
+
|
|
57
62
|
const bodyMethods = new Set(["send", "json", "end"]);
|
|
58
63
|
// and the four that finish the response, after which nothing a handler does is observable
|
|
59
64
|
const terminalMethods = new Set(["send", "json", "end", "sendStatus"]);
|
|
@@ -710,6 +715,17 @@ module.exports = function compileDeclarative(cb, app) {
|
|
|
710
715
|
return false;
|
|
711
716
|
}
|
|
712
717
|
|
|
718
|
+
// A status that carries no content, which is a rule about the message and not about the
|
|
719
|
+
// application: express drops the body and the framing headers for 204 and 304, and node
|
|
720
|
+
// writes a lone Content-Length: 0 for 205. Compiled, the body went out anyway, so
|
|
721
|
+
// res.sendStatus(204) answered "No Content" with a Content-Length of ten. A client frames
|
|
722
|
+
// a 204 as bodiless whatever the headers say, so those ten bytes were read as the start of
|
|
723
|
+
// the next answer on the connection, which is a desync on any keep-alive client. The
|
|
724
|
+
// ordinary path already writes all three the way express does: this hands them back to it.
|
|
725
|
+
if (BODILESS_STATUSES.has(statusCode) || statusCode < 200) {
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
|
|
713
729
|
let decRes = new uWSAny.DeclarativeResponse();
|
|
714
730
|
|
|
715
731
|
if (statusCode !== 200) {
|
package/src/request.js
CHANGED
|
@@ -152,6 +152,52 @@ const discardedDuplicates = new Set([
|
|
|
152
152
|
// 128 KB of body buffered before uWS is asked to pause
|
|
153
153
|
const READABLE_OPTIONS = { highWaterMark: 128 * 1024 };
|
|
154
154
|
|
|
155
|
+
// The methods node's parser accepts, which is the set a request can arrive with behind Express and
|
|
156
|
+
// the set a route can be registered for here. µWS accepts any token, so without this a line like
|
|
157
|
+
// `{"a":1}GET /path HTTP/1.1` is a request to it, with `{"A":1}GET` as the method. See _mustRefuse.
|
|
158
|
+
const KNOWN_METHODS = new Set(require("http").METHODS);
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Whether a transfer-encoding leaves the body's length knowable, which is RFC 9112's rule that
|
|
162
|
+
* `chunked` comes last. `gzip, chunked` is fine and `chunked, gzip` is not: with a coding applied
|
|
163
|
+
* after the framing one, nothing can say where the body ends, and node answers 400 rather than
|
|
164
|
+
* guess. µWS guesses, and what it guesses wrong becomes the next request on the connection.
|
|
165
|
+
*
|
|
166
|
+
* Read per header rather than over the joined value, so a request splitting the list across two
|
|
167
|
+
* transfer-encoding headers is refused even when the codings would be legal joined up. That is
|
|
168
|
+
* stricter than node by a hair, on a shape nothing sends, and stricter is the safe direction here.
|
|
169
|
+
*
|
|
170
|
+
* @param {string} value one transfer-encoding header, as uWS hands it over
|
|
171
|
+
* @returns {boolean}
|
|
172
|
+
*/
|
|
173
|
+
function endsWithChunked(value) {
|
|
174
|
+
const last = value.slice(value.lastIndexOf(",") + 1).trim();
|
|
175
|
+
// a coding may carry parameters, which are not part of its name
|
|
176
|
+
const semicolon = last.indexOf(";");
|
|
177
|
+
return (semicolon === -1 ? last : last.slice(0, semicolon)).trim().toLowerCase() === "chunked";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The path of the url a request carries right now, without the query.
|
|
182
|
+
*
|
|
183
|
+
* Express reads it off req.url on every access, so a middleware that assigns req.url is seen by
|
|
184
|
+
* whatever runs next, the callback after it in the same route included: the router takes a rewrite
|
|
185
|
+
* over at its next hop, and until then the field is a hop behind. `req.url = req.url.replace(/^\/+/,
|
|
186
|
+
* "/")` reported "//" for the rest of its own route where express reports "/". The field answers
|
|
187
|
+
* while the two agree, which is every read of a request nobody rewrote.
|
|
188
|
+
*
|
|
189
|
+
* @param {any} req
|
|
190
|
+
* @returns {string}
|
|
191
|
+
*/
|
|
192
|
+
function currentPath(req) {
|
|
193
|
+
const url = req.url;
|
|
194
|
+
if (url === req._lastUrl) {
|
|
195
|
+
return req._path;
|
|
196
|
+
}
|
|
197
|
+
const query = url.indexOf("?");
|
|
198
|
+
return query === -1 ? url : url.slice(0, query);
|
|
199
|
+
}
|
|
200
|
+
|
|
155
201
|
/**
|
|
156
202
|
* Whether a content-length is a plain count of bytes, which is the only thing RFC 9112 allows.
|
|
157
203
|
*
|
|
@@ -173,6 +219,12 @@ function isByteCount(value) {
|
|
|
173
219
|
return false;
|
|
174
220
|
}
|
|
175
221
|
}
|
|
222
|
+
// A count nothing can represent is not a count. Node refuses one that overflows, and µWS framed
|
|
223
|
+
// the request as if it had said something else, which put the bytes after it in a request of
|
|
224
|
+
// their own. The length test first, so an ordinary value never parses.
|
|
225
|
+
if (value.length > 15 && Number(value) > Number.MAX_SAFE_INTEGER) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
176
228
|
return true;
|
|
177
229
|
}
|
|
178
230
|
|
|
@@ -363,11 +415,15 @@ module.exports = class Request extends LazyReadable {
|
|
|
363
415
|
if (headerKey.length === 14) {
|
|
364
416
|
// a second content-length whatever it says, and one that is not a count of bytes:
|
|
365
417
|
// both make uWS frame the request differently from what is on the wire, see
|
|
366
|
-
//
|
|
418
|
+
// _mustRefuse and isByteCount
|
|
367
419
|
if (r._sawContentLength || !isByteCount(value)) {
|
|
368
|
-
r.
|
|
420
|
+
r._mustRefuse = true;
|
|
369
421
|
}
|
|
370
422
|
r._sawContentLength = true;
|
|
423
|
+
} else if (!endsWithChunked(value)) {
|
|
424
|
+
// chunked has to be the last coding: anything after it and the length of the body
|
|
425
|
+
// is not knowable, which node answers 400 to and µWS served. See endsWithChunked
|
|
426
|
+
r._mustRefuse = true;
|
|
371
427
|
}
|
|
372
428
|
// saying anything about framing at all, "0" included. A parser that can see a
|
|
373
429
|
// content-length answers about the body it describes, even an empty one: a zero length
|
|
@@ -480,9 +536,9 @@ module.exports = class Request extends LazyReadable {
|
|
|
480
536
|
_sawContentLength;
|
|
481
537
|
|
|
482
538
|
/**
|
|
483
|
-
* Whether
|
|
484
|
-
*
|
|
485
|
-
*
|
|
539
|
+
* Whether this request must not be routed at all. Node's parser refuses each of these outright
|
|
540
|
+
* and answers 400; every one of them is a way for bytes the client did not send as a request to
|
|
541
|
+
* be served as one, which is request smuggling.
|
|
486
542
|
*
|
|
487
543
|
* a repeated content-length uWS frames on the first and drops the rest, so a proxy in
|
|
488
544
|
* front reading the last one instead forwards bytes uWS then
|
|
@@ -491,13 +547,17 @@ module.exports = class Request extends LazyReadable {
|
|
|
491
547
|
* included, and frames the request as carrying no body at all,
|
|
492
548
|
* which turns the body the client sent into that same second
|
|
493
549
|
* request. See isByteCount
|
|
550
|
+
* a method nobody defines uWS takes any token as the method, so anything at all
|
|
551
|
+
* followed by a space and a path is a request line to it. A
|
|
552
|
+
* request with no content-length and no transfer-encoding has
|
|
553
|
+
* no body, so the bytes after it are the next request: node
|
|
554
|
+
* reads them and answers 400, uWS served them. See KNOWN_METHODS
|
|
494
555
|
*
|
|
495
|
-
*
|
|
496
|
-
* for the same reason as rawIp.
|
|
556
|
+
* Declared for the same reason as rawIp.
|
|
497
557
|
*
|
|
498
558
|
* @type {boolean|undefined}
|
|
499
559
|
*/
|
|
500
|
-
|
|
560
|
+
_mustRefuse;
|
|
501
561
|
|
|
502
562
|
/**
|
|
503
563
|
* Whether the client asked for the connection to be closed. Declared for the same reason.
|
|
@@ -567,7 +627,7 @@ module.exports = class Request extends LazyReadable {
|
|
|
567
627
|
// A content-length of "0" declares no body and used to stay on the cheap side, but
|
|
568
628
|
// getHeader only ever returns the first of a repeated header, so a duplicate cannot be
|
|
569
629
|
// seen from here, and a duplicate has to be refused rather than routed: see
|
|
570
|
-
//
|
|
630
|
+
// _mustRefuse. Anything that says a word about framing takes the full copy instead.
|
|
571
631
|
//
|
|
572
632
|
// One shape stays invisible here, a content-length present with an empty value: uWS
|
|
573
633
|
// answers "" for that and for a header that was never sent, and nothing in its API
|
|
@@ -632,7 +692,7 @@ module.exports = class Request extends LazyReadable {
|
|
|
632
692
|
}
|
|
633
693
|
if (preset) {
|
|
634
694
|
// the registration's constants: two native crossings and their strings not asked for
|
|
635
|
-
this.
|
|
695
|
+
this._path = preset.path;
|
|
636
696
|
this.originalUrl = preset.path + this.urlQuery;
|
|
637
697
|
this.url = this.originalUrl;
|
|
638
698
|
this._lastUrl = this.originalUrl;
|
|
@@ -646,17 +706,27 @@ module.exports = class Request extends LazyReadable {
|
|
|
646
706
|
// getUrl() is the path already, so the query is joined on and then not split off
|
|
647
707
|
// again. Building originalUrl and picking the path back out of it with indexOf and
|
|
648
708
|
// substring was a search and a second string for something uWS had just handed over.
|
|
649
|
-
this.
|
|
650
|
-
this.originalUrl = this.
|
|
709
|
+
this._path = req.getUrl();
|
|
710
|
+
this.originalUrl = this._path + this.urlQuery;
|
|
651
711
|
this.url = this.originalUrl;
|
|
652
712
|
// what the router last wrote to req.url. A middleware assigning something else is a
|
|
653
713
|
// rewrite, which express honours, and dispatch compares against this to notice it
|
|
654
714
|
this._lastUrl = this.originalUrl;
|
|
655
715
|
// charCodeAt rather than indexing: s[i] builds a one character string to throw away
|
|
656
|
-
this.endsWithSlash = this.
|
|
657
|
-
this._opPath = this.
|
|
658
|
-
this._originalPath = this.
|
|
659
|
-
|
|
716
|
+
this.endsWithSlash = this._path.charCodeAt(this._path.length - 1) === 0x2f;
|
|
717
|
+
this._opPath = this._path;
|
|
718
|
+
this._originalPath = this._path;
|
|
719
|
+
const rawMethod = req.getCaseSensitiveMethod();
|
|
720
|
+
this.method = rawMethod.toUpperCase();
|
|
721
|
+
// node's parser knows a fixed set and refuses everything else; µWS takes the token as
|
|
722
|
+
// it finds it, so a request line is anything with a space in it. Compared before the
|
|
723
|
+
// uppercasing on purpose: a method is case sensitive, node refuses "post", and µWS
|
|
724
|
+
// folds it to POST and serves it. Only asked of a method the framework cannot route
|
|
725
|
+
// anyway, since a route can only be registered for one of these, see the loop that
|
|
726
|
+
// builds the verb methods at the end of router.js
|
|
727
|
+
if (!KNOWN_METHODS.has(rawMethod)) {
|
|
728
|
+
this._mustRefuse = true;
|
|
729
|
+
}
|
|
660
730
|
this._isOptions = this.method === "OPTIONS";
|
|
661
731
|
this._isHead = this.method === "HEAD";
|
|
662
732
|
}
|
|
@@ -1004,6 +1074,17 @@ module.exports = class Request extends LazyReadable {
|
|
|
1004
1074
|
return index !== -1 ? header.slice(0, index).trim() : header.trim();
|
|
1005
1075
|
}
|
|
1006
1076
|
|
|
1077
|
+
/**
|
|
1078
|
+
* The path of the current url, without the query and relative to the mount the request is in.
|
|
1079
|
+
* A getter rather than a field, because express recomputes it from req.url on every read, see
|
|
1080
|
+
* currentPath.
|
|
1081
|
+
*
|
|
1082
|
+
* @returns {string}
|
|
1083
|
+
*/
|
|
1084
|
+
get path() {
|
|
1085
|
+
return currentPath(this);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1007
1088
|
/**
|
|
1008
1089
|
* Takes over what a middleware assigned to req.url: the remaining routing matches the new
|
|
1009
1090
|
* path, and req.query reflects the new query string. The assigned url is relative to the
|
|
@@ -1025,7 +1106,7 @@ module.exports = class Request extends LazyReadable {
|
|
|
1025
1106
|
// a rewrite to "/a?" keeps its "?", as one arriving that way does
|
|
1026
1107
|
this.urlQuery = queryIndex === -1 ? "" : "?" + this._rawQuery;
|
|
1027
1108
|
this._originalPath = prefix + newPath;
|
|
1028
|
-
this.
|
|
1109
|
+
this._path = newPath;
|
|
1029
1110
|
this.endsWithSlash = newPath.charCodeAt(newPath.length - 1) === 0x2f;
|
|
1030
1111
|
this._opPath = newPath;
|
|
1031
1112
|
this._opPathLower = null;
|
package/src/router.js
CHANGED
|
@@ -241,7 +241,18 @@ class Walk {
|
|
|
241
241
|
// was one closure per hop of every request not on a compiled chain
|
|
242
242
|
for (; routeIndex < routes.length; routeIndex++) {
|
|
243
243
|
const r = routes[routeIndex];
|
|
244
|
-
|
|
244
|
+
// A HEAD request enters a route whose path matched even when its verb cannot serve
|
|
245
|
+
// one: express exempts HEAD from the method check ("if (!hasMethod && method !==
|
|
246
|
+
// 'HEAD')" in router/index.js), so the layer's parameters are captured and its
|
|
247
|
+
// param() callbacks run before the route is dropped. Only asked when the router has
|
|
248
|
+
// callbacks to run, since entering a route to step straight back out of it is
|
|
249
|
+
// otherwise pure cost with nothing to show for it. runRoute steps over it.
|
|
250
|
+
if (!(
|
|
251
|
+
r.all ||
|
|
252
|
+
r.method === req.method ||
|
|
253
|
+
req._isOptions ||
|
|
254
|
+
(req._isHead && (r.gettable || r.paramCallbacks.size > 0))
|
|
255
|
+
)) {
|
|
245
256
|
// taken only to fail: _preprocessRequest decodes again and turns it into the
|
|
246
257
|
// error, so the handlers of a route this request cannot run never see it
|
|
247
258
|
if (mayFailDecode && router._pathMatches(r, req) && router._paramsFailToDecode(r, req)) {
|
|
@@ -343,6 +354,13 @@ class Walk {
|
|
|
343
354
|
runRoute(continueRoute) {
|
|
344
355
|
const req = this.req;
|
|
345
356
|
const route = this.route;
|
|
357
|
+
// A compiled chain walks into a mount rather than entering it, so the rule above needs
|
|
358
|
+
// saying here as well: everything after this marker is inside the mount, and a mount is
|
|
359
|
+
// stepped over while an error is in flight. Leaving the chain is what running out of it
|
|
360
|
+
// already means, and ordinary routing takes over after the mount.
|
|
361
|
+
if (route.keepMount === true && req._error) {
|
|
362
|
+
return this.dispatch(this.routes.length);
|
|
363
|
+
}
|
|
346
364
|
if (route.use) {
|
|
347
365
|
if (route.mountApp) {
|
|
348
366
|
// optimized chain: normal dispatch swaps req.app when it enters a mounted
|
|
@@ -473,7 +491,12 @@ class Walk {
|
|
|
473
491
|
if (!this.skipCheck && this.skipUntil && this.skipUntil.routeKey >= route.routeKey) {
|
|
474
492
|
return this.step(undefined);
|
|
475
493
|
}
|
|
476
|
-
|
|
494
|
+
// A mounted router or application is stepped over while an error is in flight. Its handle
|
|
495
|
+
// takes three arguments, so express's Layer#handleError hands the error straight on without
|
|
496
|
+
// entering it: what a mount catches is what it raised itself. Entering it ran the error
|
|
497
|
+
// handlers written inside the mount, and left req.app pointing at a mounted application,
|
|
498
|
+
// whose settings then answered. A 500 carried an ETag under app.set("etag", false).
|
|
499
|
+
if (kind === CALLBACK_ROUTER && !req._error) {
|
|
477
500
|
if (callback._isApplication) {
|
|
478
501
|
rememberApp(this, route, req);
|
|
479
502
|
useApp(req, callback);
|
|
@@ -514,20 +537,20 @@ class Walk {
|
|
|
514
537
|
if (parentMethods !== null) {
|
|
515
538
|
req._matchedMethods = parentMethods;
|
|
516
539
|
}
|
|
517
|
-
if (req._isOptions && childMethods.size) {
|
|
540
|
+
if (req._isOptions && childMethods.size && !req._error) {
|
|
518
541
|
// OPTIONS routing is different, it stops in the router if matched.
|
|
519
542
|
// Express answers as the router hands back, so a throw while answering,
|
|
520
543
|
// a head already written being the way, walks on to later error handlers
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
return this.step(err);
|
|
527
|
-
}
|
|
544
|
+
try {
|
|
545
|
+
router._sendOptionsReply(req, res, childMethods);
|
|
546
|
+
return this.resolve(true);
|
|
547
|
+
} catch (err) {
|
|
548
|
+
return this.step(err);
|
|
528
549
|
}
|
|
529
|
-
return this.resolve(false);
|
|
530
550
|
}
|
|
551
|
+
// An error carried out of the mount is not answered by the automatic reply, and
|
|
552
|
+
// stopping here handed it to the default page: express walks on to the error
|
|
553
|
+
// handlers written after the mount, for OPTIONS as for any other method.
|
|
531
554
|
this.step(undefined);
|
|
532
555
|
})
|
|
533
556
|
// a rejection out of the nested walk, or a throw above, must reject this one
|
|
@@ -550,6 +573,11 @@ class Walk {
|
|
|
550
573
|
}
|
|
551
574
|
return this.step(undefined);
|
|
552
575
|
}
|
|
576
|
+
// entered only so its param callbacks could run, see the scan in dispatch: the verb
|
|
577
|
+
// cannot serve a HEAD, so nothing here answers it
|
|
578
|
+
if (req._isHead && !route.all && !route.gettable && route.method !== "HEAD") {
|
|
579
|
+
return this.step(undefined);
|
|
580
|
+
}
|
|
553
581
|
|
|
554
582
|
const out = callback(req, res, this.next);
|
|
555
583
|
if (out instanceof Promise) {
|
|
@@ -667,10 +695,54 @@ function setMountedPath(req) {
|
|
|
667
695
|
req._opPath = req._consumed === 0 ? req._originalPath : req._originalPath.slice(req._consumed);
|
|
668
696
|
req._opPathLower = null;
|
|
669
697
|
req.url = req._opPath === "" ? "/" + req.urlQuery : req._opPath + req.urlQuery;
|
|
670
|
-
req.
|
|
698
|
+
req._path = req._opPath === "" ? "/" : req._opPath;
|
|
671
699
|
req._lastUrl = req.url;
|
|
672
700
|
}
|
|
673
701
|
|
|
702
|
+
// req.path as the request class declares it, taken off the prototype rather than written out a
|
|
703
|
+
// second time. A request the router adopts is a plain object and gets it defined on itself, see
|
|
704
|
+
// adoptPlainRequest. Enumerable, as express's own is.
|
|
705
|
+
const PATH_PROPERTY = {
|
|
706
|
+
.../** @type {PropertyDescriptor} */ (Object.getOwnPropertyDescriptor(Request.prototype, "path")),
|
|
707
|
+
enumerable: true
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
const NO_PARAM_NAMES = [];
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* The parameter names a route captures with its own pattern.
|
|
714
|
+
*
|
|
715
|
+
* This is the set express runs param callbacks for. A name that reached req.params from a mount
|
|
716
|
+
* above, through mergeParams, belongs to that mount's router and not to this one, and express does
|
|
717
|
+
* not call this router's param() for it: it walks the keys the layer itself matched. Reading
|
|
718
|
+
* req.params instead ran a callback for every inherited name too, which is visible whenever such a
|
|
719
|
+
* callback does anything, and turned a 200 into a 500 when one of them refused the value.
|
|
720
|
+
*
|
|
721
|
+
* Worked out once per route and kept, since it follows from the pattern and never changes.
|
|
722
|
+
*
|
|
723
|
+
* @param {any} route
|
|
724
|
+
* @returns {string[]}
|
|
725
|
+
*/
|
|
726
|
+
function ownParamNames(route) {
|
|
727
|
+
let names = route._ownParamNames;
|
|
728
|
+
if (names !== undefined) {
|
|
729
|
+
return names;
|
|
730
|
+
}
|
|
731
|
+
if (route.optimizedParams) {
|
|
732
|
+
// µWS matched the pattern and hands the values back by position, under these names
|
|
733
|
+
names = route.optimizedParams;
|
|
734
|
+
} else if (route.pattern instanceof RegExp) {
|
|
735
|
+
const meta = getPatternMeta(route.pattern);
|
|
736
|
+
// outputNames is what _extractParams writes into params; a RegExp the application wrote
|
|
737
|
+
// itself was never compiled here, so its capture groups are the names
|
|
738
|
+
names = meta ? meta.outputNames : regexpGroupKeys(route.pattern);
|
|
739
|
+
} else {
|
|
740
|
+
names = NO_PARAM_NAMES;
|
|
741
|
+
}
|
|
742
|
+
route._ownParamNames = names;
|
|
743
|
+
return names;
|
|
744
|
+
}
|
|
745
|
+
|
|
674
746
|
/**
|
|
675
747
|
* Whether this route reads the parameters of the mounts above it, which is its own router asking
|
|
676
748
|
* for them. The stack holds what a mergeParams router captured on the way in, and a plain router
|
|
@@ -778,7 +850,10 @@ function adoptPlainRequest(req, router) {
|
|
|
778
850
|
const path = queryIndex === -1 ? raw : raw.slice(0, queryIndex);
|
|
779
851
|
req.urlQuery = queryIndex === -1 ? "" : raw.slice(queryIndex);
|
|
780
852
|
req._rawQuery = req.urlQuery.slice(1);
|
|
781
|
-
req.
|
|
853
|
+
req._path = path;
|
|
854
|
+
// an adopted request is a plain object, so it carries no prototype of ours and reads its path
|
|
855
|
+
// off a property of its own. The class's getter itself, so there is one of it
|
|
856
|
+
Object.defineProperty(req, "path", PATH_PROPERTY);
|
|
782
857
|
req.originalUrl = req.originalUrl ?? arrived;
|
|
783
858
|
req._originalPath = path;
|
|
784
859
|
req.endsWithSlash = path.charCodeAt(path.length - 1) === 0x2f;
|
|
@@ -1796,6 +1871,16 @@ module.exports = class Router extends EventEmitter {
|
|
|
1796
1871
|
}
|
|
1797
1872
|
}
|
|
1798
1873
|
|
|
1874
|
+
// The same rule as the one just above, which a route of another method reaches by
|
|
1875
|
+
// another road. A mount's chain is inherited by every path under it, and a route that
|
|
1876
|
+
// is not itself a mount answers the mount point rather than the subtree: in the chain
|
|
1877
|
+
// it ran for the whole of it, so router.all("/:p1") answered the /posts/a-b that
|
|
1878
|
+
// belongs to the router mounted at /posts. guardsInside is written for this: only
|
|
1879
|
+
// layers with more segments than the mount path are asked about a leaf.
|
|
1880
|
+
if (route.use && !r.use && typeof route.path === "string" && couldAnswer(r, route.path)) {
|
|
1881
|
+
return false;
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1799
1884
|
// a RegExp mount runs only where its match starts the path and breaks on a separator,
|
|
1800
1885
|
// which is decidable here against a literal path and not against one with a parameter
|
|
1801
1886
|
if (r.regexMount) {
|
|
@@ -1822,13 +1907,9 @@ module.exports = class Router extends EventEmitter {
|
|
|
1822
1907
|
|
|
1823
1908
|
// check if the paths match. A route with parameters is excluded from the text test:
|
|
1824
1909
|
// its literal ":name" text would let an earlier regex in on requests it never matches.
|
|
1825
|
-
|
|
1826
|
-
// answers "/x/" as well as "/x", and an earlier pattern that matches only the first is
|
|
1827
|
-
// still an earlier pattern that answers a request this registration would take.
|
|
1910
|
+
const regexCanMatch = r.pattern instanceof RegExp && (!withParams || r.use);
|
|
1828
1911
|
if (
|
|
1829
|
-
(r.pattern
|
|
1830
|
-
(!withParams || r.use) &&
|
|
1831
|
-
(r.pattern.test(route.path) || (!strictHere && r.pattern.test(route.path + "/")))) ||
|
|
1912
|
+
(regexCanMatch && r.pattern.test(route.path)) ||
|
|
1832
1913
|
(typeof r.pattern === "string" &&
|
|
1833
1914
|
(r.pattern === route.path ||
|
|
1834
1915
|
(!caseSensitive && r.pattern.toLowerCase() === routePathFolded) ||
|
|
@@ -1840,6 +1921,15 @@ module.exports = class Router extends EventEmitter {
|
|
|
1840
1921
|
optimizedPath.push(r);
|
|
1841
1922
|
continue;
|
|
1842
1923
|
}
|
|
1924
|
+
// Without strict routing this registration answers "/x/" as well as "/x". An earlier
|
|
1925
|
+
// pattern matching only the second answers part of what the registration takes and not
|
|
1926
|
+
// the rest, which the chain has no way to say: it runs what is in it without matching
|
|
1927
|
+
// again. Both spellings used to put the route in whole, so app.all("/:p0/{:o1}/{:o2}")
|
|
1928
|
+
// answered a GET /list/Mixed that belonged to the route written after it. An ordinary
|
|
1929
|
+
// pattern answers both spellings, so only an optional group or a wildcard reaches here.
|
|
1930
|
+
if (regexCanMatch && !strictHere && r.pattern.test(route.path + "/")) {
|
|
1931
|
+
return false;
|
|
1932
|
+
}
|
|
1843
1933
|
if (!withParams) {
|
|
1844
1934
|
continue;
|
|
1845
1935
|
}
|
|
@@ -1899,6 +1989,13 @@ module.exports = class Router extends EventEmitter {
|
|
|
1899
1989
|
if (!this.uwsApp) {
|
|
1900
1990
|
return;
|
|
1901
1991
|
}
|
|
1992
|
+
// Everything below is what makes this framework fast, and every one of its decisions is a
|
|
1993
|
+
// claim that µWS answering by itself is the same answer the chain would have given. Turned
|
|
1994
|
+
// off, the claim is not made and the chain answers everything. Serving one application both
|
|
1995
|
+
// ways and comparing the answers is what tests those claims: `npm run fuzz -- --self`.
|
|
1996
|
+
if (this.get("native routes") === false) {
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1902
1999
|
|
|
1903
2000
|
// pathPrefix/chainPrefix accumulate across nested sole-callback mounts, and outerGuards
|
|
1904
2001
|
// carries what was written before them and answers only part of what is under them
|
|
@@ -2083,7 +2180,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
2083
2180
|
*
|
|
2084
2181
|
* @param {any} response
|
|
2085
2182
|
*/
|
|
2086
|
-
|
|
2183
|
+
_refuseRequest(response) {
|
|
2087
2184
|
response.finished = true;
|
|
2088
2185
|
response._res.close();
|
|
2089
2186
|
response.emit("close");
|
|
@@ -2195,8 +2292,8 @@ module.exports = class Router extends EventEmitter {
|
|
|
2195
2292
|
}
|
|
2196
2293
|
const request = this.handleRequest(res, req, preset, skipHolder);
|
|
2197
2294
|
const response = request.res;
|
|
2198
|
-
if (request.
|
|
2199
|
-
return this.
|
|
2295
|
+
if (request._mustRefuse === true) {
|
|
2296
|
+
return this._refuseRequest(response);
|
|
2200
2297
|
}
|
|
2201
2298
|
if (optimizedParams) {
|
|
2202
2299
|
request.optimizedParams = new NullObject();
|
|
@@ -2295,6 +2392,10 @@ module.exports = class Router extends EventEmitter {
|
|
|
2295
2392
|
route.callbacks.length === 1 && // must not have multiple callbacks
|
|
2296
2393
|
typeof route.callbacks[0] === "function" && // must be a function
|
|
2297
2394
|
route.paramCallbacks.size === 0 && // a param callback has to run, and this answers without running anything
|
|
2395
|
+
// a captured value is decoded when the route runs, and one that cannot be decoded is a
|
|
2396
|
+
// 400 in express and on the ordinary path here. Nothing runs to raise it on a
|
|
2397
|
+
// declarative response, so GET /a-b%5Ec@d%e came back 200 from app.get("/:p12")
|
|
2398
|
+
route.optimizedParams === undefined &&
|
|
2298
2399
|
// a declarative response is answered by µWS itself, so no javascript runs and the case
|
|
2299
2400
|
// guard could not: a route that needs one has to stay an ordinary handler
|
|
2300
2401
|
caseGuards === null &&
|
|
@@ -2514,8 +2615,14 @@ module.exports = class Router extends EventEmitter {
|
|
|
2514
2615
|
|
|
2515
2616
|
// the route's own router's callbacks: an optimized chain is walked by the app even when it
|
|
2516
2617
|
// ends in a mounted router's route
|
|
2618
|
+
//
|
|
2619
|
+
// A route an OPTIONS request reaches only to have its verb counted for the automatic reply
|
|
2620
|
+
// is not a route this request runs, and express does not run its app.param() callbacks for
|
|
2621
|
+
// it. The same condition runRoute counts the verb under, see the OPTIONS branch there. The
|
|
2622
|
+
// decoding above still happens either way, because express decodes a layer whose path
|
|
2623
|
+
// matched whatever its method is, which is what answers 400 for a malformed escape.
|
|
2517
2624
|
const paramCallbacks = route.paramCallbacks;
|
|
2518
|
-
if (paramCallbacks.size > 0) {
|
|
2625
|
+
if (paramCallbacks.size > 0 && !(req._isOptions && !route.all && route.method !== "OPTIONS")) {
|
|
2519
2626
|
return this._runParamCallbacks(req, res, route, paramCallbacks);
|
|
2520
2627
|
}
|
|
2521
2628
|
return true;
|
|
@@ -2558,9 +2665,14 @@ module.exports = class Router extends EventEmitter {
|
|
|
2558
2665
|
* @returns {Promise<true|"route">|true}
|
|
2559
2666
|
*/
|
|
2560
2667
|
_runParamCallbacks(req, res, route, paramCallbacks) {
|
|
2668
|
+
// the names this route captured itself, not everything in req.params: a merged-in name
|
|
2669
|
+
// belongs to the mount that captured it, see ownParamNames
|
|
2561
2670
|
let names;
|
|
2562
|
-
|
|
2563
|
-
|
|
2671
|
+
const own = ownParamNames(route);
|
|
2672
|
+
for (let i = 0; i < own.length; i++) {
|
|
2673
|
+
const name = own[i];
|
|
2674
|
+
// an optional group that did not match leaves no parameter to call anything for
|
|
2675
|
+
if (paramCallbacks.has(name) && req.params[name] !== undefined) {
|
|
2564
2676
|
(names ??= []).push(name);
|
|
2565
2677
|
}
|
|
2566
2678
|
}
|
package/src/utils.js
CHANGED
|
@@ -409,8 +409,21 @@ function patternToRegex(pattern, isPrefix = false, caseSensitive = true, strict
|
|
|
409
409
|
// splits /a.b.c against /:file{.:ext} as file=a.b, ext=c, which only works if ext
|
|
410
410
|
// cannot contain a dot. After static text there is nothing to give ground, so the
|
|
411
411
|
// parameter takes everything: /file{.:ext} against /file.tar.gz gives ext=tar.gz.
|
|
412
|
-
|
|
413
|
-
|
|
412
|
+
// The whole of it, not its first character: /:foo{abc:bar} against /123abcabc splits as
|
|
413
|
+
// foo=123 and bar=abc on express, and reading the separator as "a" left bar unable to
|
|
414
|
+
// match its own text, so the group never matched and foo took the segment whole. More
|
|
415
|
+
// than one character cannot go in a class, so it is written as a lookahead, and either
|
|
416
|
+
// way the parameter is still allowed to be exactly the separator, as path-to-regexp
|
|
417
|
+
// writes it.
|
|
418
|
+
const colon = groupContent.indexOf(":");
|
|
419
|
+
const separator = lastTokenWasParam && colon > 0 ? groupContent.slice(0, colon) : "";
|
|
420
|
+
const escapedSeparator = separator.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
421
|
+
const groupParamClass =
|
|
422
|
+
separator === ""
|
|
423
|
+
? "[^/]+"
|
|
424
|
+
: separator.length === 1
|
|
425
|
+
? `[^/${escapedSeparator}]+|${escapedSeparator}`
|
|
426
|
+
: `(?:(?!${escapedSeparator})[^/])+|${escapedSeparator}`;
|
|
414
427
|
|
|
415
428
|
let groupRegex = "";
|
|
416
429
|
let gi = 0;
|
|
@@ -596,13 +609,23 @@ function canBeOptimizedWithParams(pattern) {
|
|
|
596
609
|
return true;
|
|
597
610
|
}
|
|
598
611
|
|
|
612
|
+
// What makes a segment something other than the text it is written as: a parameter, a wildcard, an
|
|
613
|
+
// optional group, or an escape. Only two plain literals can prove that two paths never meet, so
|
|
614
|
+
// anything carrying one of these has to be read as "could be anything".
|
|
615
|
+
const NOT_A_LITERAL = /[:*{}\\]/;
|
|
616
|
+
|
|
599
617
|
/**
|
|
600
618
|
* Whether two paths could both match the same request.
|
|
601
619
|
*
|
|
602
|
-
*
|
|
603
|
-
*
|
|
604
|
-
*
|
|
605
|
-
*
|
|
620
|
+
* The answer is structural: no position where two different literals meet, and, when neither path
|
|
621
|
+
* can change length, the same number of segments. `/orders/:id` and `/invoices/:id` cannot both
|
|
622
|
+
* match, `/users/:id` and `/users/me` can. The caller reads "do not know" as yes, so every doubt
|
|
623
|
+
* answers true: saying two paths overlap only costs a native registration, while missing one lets
|
|
624
|
+
* µWS answer a request that belonged to an earlier route.
|
|
625
|
+
*
|
|
626
|
+
* A parameter is not the only shape that matches more than itself. A wildcard and an optional group
|
|
627
|
+
* do too, and reading `{:opt}` or `*splat` as the literal text it is written as reported "cannot
|
|
628
|
+
* overlap" for a route that plainly could, which took the earlier route's turn away.
|
|
606
629
|
*
|
|
607
630
|
* @param {string} a
|
|
608
631
|
* @param {string} b
|
|
@@ -612,15 +635,19 @@ function canBeOptimizedWithParams(pattern) {
|
|
|
612
635
|
function pathsCanOverlap(a, b, aIsPrefix = false) {
|
|
613
636
|
const left = a.split("/");
|
|
614
637
|
const right = b.split("/");
|
|
615
|
-
|
|
638
|
+
// An optional group matches its segment or nothing at all and a wildcard matches several, so a
|
|
639
|
+
// path carrying either one matches more than one length and the count settles nothing.
|
|
640
|
+
const fixedLength =
|
|
641
|
+
a.indexOf("{") === -1 && b.indexOf("{") === -1 && a.indexOf("*") === -1 && b.indexOf("*") === -1;
|
|
642
|
+
if (fixedLength && (aIsPrefix ? left.length > right.length : left.length !== right.length)) {
|
|
616
643
|
return false;
|
|
617
644
|
}
|
|
618
|
-
|
|
645
|
+
const shared = left.length < right.length ? left.length : right.length;
|
|
646
|
+
for (let i = 0; i < shared; i++) {
|
|
619
647
|
if (left[i] === right[i]) {
|
|
620
648
|
continue;
|
|
621
649
|
}
|
|
622
|
-
|
|
623
|
-
if (left[i].charCodeAt(0) === 0x3a || right[i].charCodeAt(0) === 0x3a) {
|
|
650
|
+
if (NOT_A_LITERAL.test(left[i]) || NOT_A_LITERAL.test(right[i])) {
|
|
624
651
|
continue;
|
|
625
652
|
}
|
|
626
653
|
return false;
|
|
@@ -947,6 +974,13 @@ const defaultSettings = {
|
|
|
947
974
|
// The native µWS router matches bytes, so the compiler in _compileOptimizedRoutes only hands
|
|
948
975
|
// it routes whose earlier siblings it can prove agree under either case rule.
|
|
949
976
|
"declarative responses": true,
|
|
977
|
+
// on. Off hands every request to the ordinary chain instead of letting µWS match what it can,
|
|
978
|
+
// which is slower and answers the same. Not a tuning knob: it exists so one application can be
|
|
979
|
+
// served both ways and the two sets of answers compared, which tests the optimizer against the
|
|
980
|
+
// rest of the framework without a second framework to compare with. See
|
|
981
|
+
// `npm run fuzz -- --self`. A compiled response needs a native registration to hang on, so this
|
|
982
|
+
// takes "declarative responses" with it.
|
|
983
|
+
"native routes": true,
|
|
950
984
|
// off: with a window set, the size and mtime of a file served by sendFile are remembered for
|
|
951
985
|
// it, which is one syscall less per request and a file that can be served as it was a moment
|
|
952
986
|
// ago. "stat cache ms" is the window in milliseconds, compiled from it by set()
|
|
@@ -1436,6 +1470,32 @@ function withUtf8Charset(value) {
|
|
|
1436
1470
|
const HEADER_TOKEN = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/;
|
|
1437
1471
|
const HEADER_VALUE = /[^\t\x20-\x7e\x80-\xff]/;
|
|
1438
1472
|
|
|
1473
|
+
/**
|
|
1474
|
+
* One of node's header errors, built the way node builds it.
|
|
1475
|
+
*
|
|
1476
|
+
* Assigning the code is not the whole of it. Node also puts the code in the first line of the
|
|
1477
|
+
* stack, by naming the error "TypeError [THE_CODE]" while V8 formats that line and then taking
|
|
1478
|
+
* the name back off. Whatever prints a stack therefore says which code it was, and the default
|
|
1479
|
+
* error page prints exactly that: without this, the same refusal reads "TypeError:" here and
|
|
1480
|
+
* "TypeError [ERR_INVALID_CHAR]:" behind Express. Found by fuzzing against express.
|
|
1481
|
+
*
|
|
1482
|
+
* @param {string} message
|
|
1483
|
+
* @param {string} code
|
|
1484
|
+
* @returns {NodeJS.ErrnoException}
|
|
1485
|
+
*/
|
|
1486
|
+
function headerError(message, code) {
|
|
1487
|
+
/** @type {NodeJS.ErrnoException} */
|
|
1488
|
+
const err = new TypeError(message);
|
|
1489
|
+
err.name = `TypeError [${code}]`;
|
|
1490
|
+
// reading it is what makes V8 format the line, and it formats it from the name above
|
|
1491
|
+
void err.stack;
|
|
1492
|
+
// back to the prototype's "TypeError", which is what node leaves behind. Cast because Error
|
|
1493
|
+
// declares name as always present, and this deletes the own property to uncover it again
|
|
1494
|
+
delete (/** @type {any} */ (err).name);
|
|
1495
|
+
err.code = code;
|
|
1496
|
+
return err;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1439
1499
|
/**
|
|
1440
1500
|
* Refuses a header name that is not an HTTP token, the way node's setHeader does and with its
|
|
1441
1501
|
* error, so an application catching ERR_INVALID_HTTP_TOKEN behind Express catches it here.
|
|
@@ -1446,10 +1506,7 @@ const HEADER_VALUE = /[^\t\x20-\x7e\x80-\xff]/;
|
|
|
1446
1506
|
*/
|
|
1447
1507
|
function validateHeaderName(name) {
|
|
1448
1508
|
if (typeof name !== "string" || !HEADER_TOKEN.test(name)) {
|
|
1449
|
-
|
|
1450
|
-
const err = new TypeError(`Header name must be a valid HTTP token ["${name}"]`);
|
|
1451
|
-
err.code = "ERR_INVALID_HTTP_TOKEN";
|
|
1452
|
-
throw err;
|
|
1509
|
+
throw headerError(`Header name must be a valid HTTP token ["${name}"]`, "ERR_INVALID_HTTP_TOKEN");
|
|
1453
1510
|
}
|
|
1454
1511
|
}
|
|
1455
1512
|
|
|
@@ -1471,10 +1528,7 @@ function validateHeaderValue(name, value) {
|
|
|
1471
1528
|
return;
|
|
1472
1529
|
}
|
|
1473
1530
|
if (HEADER_VALUE.test(value)) {
|
|
1474
|
-
|
|
1475
|
-
const err = new TypeError(`Invalid character in header content ["${name}"]`);
|
|
1476
|
-
err.code = "ERR_INVALID_CHAR";
|
|
1477
|
-
throw err;
|
|
1531
|
+
throw headerError(`Invalid character in header content ["${name}"]`, "ERR_INVALID_CHAR");
|
|
1478
1532
|
}
|
|
1479
1533
|
}
|
|
1480
1534
|
|