fulmine.js 5.15.0 → 5.16.0
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 +11 -0
- package/package.json +1 -1
- package/src/compression.js +46 -3
- package/src/middlewares.js +69 -45
- package/src/parse-query.js +29 -1
- package/src/request.js +151 -57
- package/src/response.js +56 -13
- package/src/router.js +304 -78
- package/src/types.d.ts +6 -0
- package/src/utils.js +16 -0
package/src/request.js
CHANGED
|
@@ -229,6 +229,13 @@ function endsWithChunked(value) {
|
|
|
229
229
|
* @returns {boolean}
|
|
230
230
|
*/
|
|
231
231
|
function saysClose(value) {
|
|
232
|
+
// what clients actually send, almost always: two interned compares answer before the scan
|
|
233
|
+
if (value === "keep-alive") {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
if (value === "close") {
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
232
239
|
const length = value.length;
|
|
233
240
|
let at = 0;
|
|
234
241
|
while (at < length) {
|
|
@@ -662,6 +669,19 @@ module.exports = class Request extends LazyReadable {
|
|
|
662
669
|
*/
|
|
663
670
|
noEtag;
|
|
664
671
|
|
|
672
|
+
/**
|
|
673
|
+
* The decoded pairs of the first default-parser parse of req.query, flat key,value; false
|
|
674
|
+
* when it saw a repeated key, which a flat replay cannot reproduce. See `get query`.
|
|
675
|
+
* @type {string[]|false|undefined}
|
|
676
|
+
*/
|
|
677
|
+
_querySnap;
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* The raw string _querySnap came from: a url rewrite replaces _rawQuery.
|
|
681
|
+
* @type {string|undefined}
|
|
682
|
+
*/
|
|
683
|
+
_querySnapRaw;
|
|
684
|
+
|
|
665
685
|
/**
|
|
666
686
|
* Built for every request, which is why so little happens here. The headers are copied out
|
|
667
687
|
* because uWS only lends them for this call, everything derived from them waits until something
|
|
@@ -681,23 +701,24 @@ module.exports = class Request extends LazyReadable {
|
|
|
681
701
|
super();
|
|
682
702
|
this._res = res;
|
|
683
703
|
this._req = req;
|
|
684
|
-
// the plain field behind the `readable` accessor, written rather than set so a request that
|
|
685
|
-
// never streams never builds a stream
|
|
686
|
-
this._readableFlag = true;
|
|
687
704
|
if (skipHolder !== undefined && skipHolder.skipHeaders) {
|
|
688
705
|
// The chain behind this registration provably never reads a header, so instead of
|
|
689
|
-
// copying them all out of uWS the constructor asks for the
|
|
690
|
-
// framework itself: body framing, keep-alive, and
|
|
691
|
-
//
|
|
692
|
-
//
|
|
706
|
+
// copying them all out of uWS the constructor asks for the ones that steer the
|
|
707
|
+
// framework itself: body framing, keep-alive, and the conditional pair. A GET that
|
|
708
|
+
// does declare a body is the rare case, and the parsers and the stream want the
|
|
709
|
+
// whole picture, so it takes the full copy.
|
|
693
710
|
//
|
|
694
|
-
//
|
|
695
|
-
// seven
|
|
696
|
-
// napi crossing and the scan behind it is nothing,
|
|
697
|
-
// JS per header and grows, 1.16us at four
|
|
698
|
-
// do not cross, and the gap widens
|
|
699
|
-
// sends a dozen or more. The body
|
|
700
|
-
// which is 0.2us on a request that is
|
|
711
|
+
// A handful of named reads against one forEach looks like it should lose, and does
|
|
712
|
+
// not: measured at seven reads they were flat at 0.75us however many headers are on
|
|
713
|
+
// the wire, since each one is a napi crossing and the scan behind it is nothing,
|
|
714
|
+
// while the copy pays a hop back into JS per header and grows, 1.16us at four
|
|
715
|
+
// headers, 1.61 at eight, 2.90 at sixteen. They do not cross, and the gap widens
|
|
716
|
+
// exactly where real traffic lives, since a browser sends a dozen or more. The body
|
|
717
|
+
// case pays two reads and then copies anyway, which is 0.2us on a request that is
|
|
718
|
+
// about to read a body.
|
|
719
|
+
//
|
|
720
|
+
// accept is not read: nothing on a granted chain consumes it, the error and 404
|
|
721
|
+
// pages are fixed HTML that never negotiate.
|
|
701
722
|
const length = req.getHeader("content-length");
|
|
702
723
|
const transferEncoding = req.getHeader("transfer-encoding");
|
|
703
724
|
// A content-length of "0" declares no body and used to stay on the cheap side, but
|
|
@@ -724,12 +745,8 @@ module.exports = class Request extends LazyReadable {
|
|
|
724
745
|
this._connectionClose = true;
|
|
725
746
|
}
|
|
726
747
|
}
|
|
727
|
-
const accept = req.getHeader("accept");
|
|
728
|
-
if (accept !== "") {
|
|
729
|
-
entries.push("accept", accept);
|
|
730
|
-
}
|
|
731
748
|
// send consults freshness whatever the etag setting: if-none-match can be "*"
|
|
732
|
-
// and a handler may set a validator by hand, so the conditional
|
|
749
|
+
// and a handler may set a validator by hand, so the conditional pair has to be
|
|
733
750
|
// really absent rather than merely uncopied
|
|
734
751
|
const ifNoneMatch = req.getHeader("if-none-match");
|
|
735
752
|
if (ifNoneMatch !== "") {
|
|
@@ -739,9 +756,13 @@ module.exports = class Request extends LazyReadable {
|
|
|
739
756
|
if (ifModifiedSince !== "") {
|
|
740
757
|
entries.push("if-modified-since", ifModifiedSince);
|
|
741
758
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
759
|
+
// fresh() answers false before it reads cache-control unless a conditional
|
|
760
|
+
// arrived, so the read only pays on the requests that can use it
|
|
761
|
+
if (ifNoneMatch !== "" || ifModifiedSince !== "") {
|
|
762
|
+
const cacheControl = req.getHeader("cache-control");
|
|
763
|
+
if (cacheControl !== "") {
|
|
764
|
+
entries.push("cache-control", cacheControl);
|
|
765
|
+
}
|
|
745
766
|
}
|
|
746
767
|
}
|
|
747
768
|
} else {
|
|
@@ -802,18 +823,26 @@ module.exports = class Request extends LazyReadable {
|
|
|
802
823
|
this._opPath = this._path;
|
|
803
824
|
this._originalPath = this._path;
|
|
804
825
|
const rawMethod = req.getCaseSensitiveMethod();
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
826
|
+
if (skipHolder !== undefined && rawMethod === skipHolder.method) {
|
|
827
|
+
// the registration's constant, byte for byte; any other spelling, "get" that µWS
|
|
828
|
+
// folded here included, takes the full check below
|
|
829
|
+
this.method = rawMethod;
|
|
830
|
+
this._isOptions = skipHolder.isOptions;
|
|
831
|
+
this._isHead = skipHolder.isHead;
|
|
832
|
+
} else {
|
|
833
|
+
this.method = rawMethod.toUpperCase();
|
|
834
|
+
// node's parser knows a fixed set and refuses everything else; µWS takes the token
|
|
835
|
+
// as it finds it, so a request line is anything with a space in it. Compared before
|
|
836
|
+
// the uppercasing on purpose: a method is case sensitive, node refuses "post", and
|
|
837
|
+
// µWS folds it to POST and serves it. Only asked of a method the framework cannot
|
|
838
|
+
// route anyway, since a route can only be registered for one of these, see the loop
|
|
839
|
+
// that builds the verb methods at the end of router.js
|
|
840
|
+
if (!KNOWN_METHODS.has(rawMethod)) {
|
|
841
|
+
this._mustRefuse = true;
|
|
842
|
+
}
|
|
843
|
+
this._isOptions = this.method === "OPTIONS";
|
|
844
|
+
this._isHead = this.method === "HEAD";
|
|
814
845
|
}
|
|
815
|
-
this._isOptions = this.method === "OPTIONS";
|
|
816
|
-
this._isHead = this.method === "HEAD";
|
|
817
846
|
}
|
|
818
847
|
// what the router last saw as the method. A middleware assigning another one is a rewrite,
|
|
819
848
|
// which express honours because it reads req.method at every layer, and dispatch compares
|
|
@@ -839,9 +868,6 @@ module.exports = class Request extends LazyReadable {
|
|
|
839
868
|
// null for the same reason as the two above: a request that never enters a mount never
|
|
840
869
|
// needs either array, and the push sites materialize them
|
|
841
870
|
this._stack = null;
|
|
842
|
-
// how many characters of _originalPath the mounts entered so far have taken, which is
|
|
843
|
-
// where baseUrl ends and the path below them begins
|
|
844
|
-
this._consumed = 0;
|
|
845
871
|
// whether one of them took a trailing slash, which only a RegExp mount can: see baseUrl
|
|
846
872
|
this._mountSlash = false;
|
|
847
873
|
this._paramStack = null;
|
|
@@ -925,6 +951,35 @@ module.exports = class Request extends LazyReadable {
|
|
|
925
951
|
return undefined;
|
|
926
952
|
}
|
|
927
953
|
|
|
954
|
+
/**
|
|
955
|
+
* The same, with repeats folded exactly as the headers object folds them, so a reader of one
|
|
956
|
+
* name per request does not build the whole object to stay correct on a repeated header.
|
|
957
|
+
* Not for set-cookie, whose folded form is an array.
|
|
958
|
+
*
|
|
959
|
+
* @param {string} name lowercase
|
|
960
|
+
* @returns {string|undefined}
|
|
961
|
+
*/
|
|
962
|
+
_foldedHeader(name) {
|
|
963
|
+
if (this.#cachedHeaders !== null) {
|
|
964
|
+
return this.#cachedHeaders[name];
|
|
965
|
+
}
|
|
966
|
+
const entries = this.#rawHeadersEntries;
|
|
967
|
+
let value;
|
|
968
|
+
for (let i = 0, len = entries.length; i < len; i += 2) {
|
|
969
|
+
if (entries[i] === name) {
|
|
970
|
+
if (value === undefined) {
|
|
971
|
+
value = entries[i + 1];
|
|
972
|
+
} else {
|
|
973
|
+
if (discardedDuplicates.has(name)) {
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
value += (name === "cookie" ? "; " : ", ") + entries[i + 1];
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
return value;
|
|
981
|
+
}
|
|
982
|
+
|
|
928
983
|
/**
|
|
929
984
|
* Whether there is any point still reading the body: once the response is finished or the
|
|
930
985
|
* connection is gone, uWS has nothing left to hand over.
|
|
@@ -1039,7 +1094,11 @@ module.exports = class Request extends LazyReadable {
|
|
|
1039
1094
|
// slash, a registered path having had it removed, so almost every request answers above.
|
|
1040
1095
|
let out = "";
|
|
1041
1096
|
let at = 0;
|
|
1042
|
-
for (
|
|
1097
|
+
for (let taken of this._stack) {
|
|
1098
|
+
// negative marks a mount that consumed the whole path, see the push in runRoute
|
|
1099
|
+
if (taken < 0) {
|
|
1100
|
+
taken = -taken;
|
|
1101
|
+
}
|
|
1043
1102
|
const piece = this._originalPath.slice(at, at + taken);
|
|
1044
1103
|
at += taken;
|
|
1045
1104
|
out += piece.charCodeAt(taken - 1) === 0x2f ? piece.slice(0, -1) : piece;
|
|
@@ -1199,18 +1258,30 @@ module.exports = class Request extends LazyReadable {
|
|
|
1199
1258
|
* path, and req.query reflects the new query string. The assigned url is relative to the
|
|
1200
1259
|
* mount the request is currently in, as it is in express, so the absolute path is rebuilt
|
|
1201
1260
|
* from the piece the mounts had consumed.
|
|
1261
|
+
*
|
|
1262
|
+
* @param {boolean} [leavingMount] the caller is popping the mount the rewrite happened in
|
|
1202
1263
|
*/
|
|
1203
|
-
_absorbUrlRewrite() {
|
|
1204
|
-
const
|
|
1205
|
-
|
|
1206
|
-
const newPath = queryIndex === -1 ? newUrl : newUrl.slice(0, queryIndex);
|
|
1264
|
+
_absorbUrlRewrite(leavingMount) {
|
|
1265
|
+
const assignedUrl = String(this.url);
|
|
1266
|
+
let newUrl = assignedUrl;
|
|
1207
1267
|
// the prefix the mounts consumed: everything of the absolute path the relative one was not
|
|
1208
1268
|
const lastQueryIndex = this._lastUrl.indexOf("?");
|
|
1209
1269
|
const oldPath = lastQueryIndex === -1 ? this._lastUrl : this._lastUrl.slice(0, lastQueryIndex);
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1270
|
+
let prefix;
|
|
1271
|
+
if (oldPath === "/" && !this._originalPath.endsWith("/")) {
|
|
1272
|
+
prefix = this._originalPath;
|
|
1273
|
+
// express's slashAdded restore, applied where express applies it, on the way out of a
|
|
1274
|
+
// mount that consumed the whole path: the "/" the middleware saw was invented, and the
|
|
1275
|
+
// rejoin strips the first character of whatever was assigned ("/found" + "target" is
|
|
1276
|
+
// "/foundtarget"). Inside the mount the assigned url routes as it is, as express does
|
|
1277
|
+
if (leavingMount === true) {
|
|
1278
|
+
newUrl = newUrl.slice(1);
|
|
1279
|
+
}
|
|
1280
|
+
} else {
|
|
1281
|
+
prefix = this._originalPath.slice(0, this._originalPath.length - oldPath.length);
|
|
1282
|
+
}
|
|
1283
|
+
const queryIndex = newUrl.indexOf("?");
|
|
1284
|
+
const newPath = queryIndex === -1 ? newUrl : newUrl.slice(0, queryIndex);
|
|
1214
1285
|
this._rawQuery = queryIndex === -1 ? "" : newUrl.slice(queryIndex + 1);
|
|
1215
1286
|
// a rewrite to "/a?" keeps its "?", as one arriving that way does
|
|
1216
1287
|
this.urlQuery = queryIndex === -1 ? "" : "?" + this._rawQuery;
|
|
@@ -1220,7 +1291,9 @@ module.exports = class Request extends LazyReadable {
|
|
|
1220
1291
|
this._opPath = newPath;
|
|
1221
1292
|
this._opPathLower = null;
|
|
1222
1293
|
this._mayFailDecode = null;
|
|
1223
|
-
|
|
1294
|
+
// the assigned string, not the mangled one: the compare against req.url must go quiet or
|
|
1295
|
+
// the next hop absorbs the same rewrite again with a different prefix
|
|
1296
|
+
this._lastUrl = assignedUrl;
|
|
1224
1297
|
}
|
|
1225
1298
|
|
|
1226
1299
|
/**
|
|
@@ -1251,15 +1324,16 @@ module.exports = class Request extends LazyReadable {
|
|
|
1251
1324
|
* the parse cached and handed out as itself, the sanitised value leaked into req.query here and
|
|
1252
1325
|
* a handler written against express read a trimmed value where express gives it the raw one.
|
|
1253
1326
|
*
|
|
1254
|
-
* And that is why there is no cache: the fresh object comes from
|
|
1255
|
-
* not from copying a kept parse. As first shipped this was parse-once-copy-per-read,
|
|
1256
|
-
* copy was the expensive half: Object.assign between null-prototype objects, which
|
|
1257
|
-
* V8's dictionary mode, measured 638ns for a two-parameter query where parsing the
|
|
1258
|
-
* measures 119ns, and on a benchmark whose every request carries such a query it
|
|
1259
|
-
* of CPU per request, which a public arena saw as -8% on its query-carrying rows.
|
|
1260
|
-
*
|
|
1261
|
-
*
|
|
1262
|
-
*
|
|
1327
|
+
* And that is why there is no cache of the object: the fresh object comes from the raw
|
|
1328
|
+
* string, not from copying a kept parse. As first shipped this was parse-once-copy-per-read,
|
|
1329
|
+
* and the copy was the expensive half: Object.assign between null-prototype objects, which
|
|
1330
|
+
* live in V8's dictionary mode, measured 638ns for a two-parameter query where parsing the
|
|
1331
|
+
* same string measures 119ns, and on a benchmark whose every request carries such a query it
|
|
1332
|
+
* cost +1.5us of CPU per request, which a public arena saw as -8% on its query-carrying rows.
|
|
1333
|
+
*
|
|
1334
|
+
* The default parser does keep the decoded pairs of its first parse, and a later read of the
|
|
1335
|
+
* same raw string replays the stores into a fresh null-prototype object: identical output,
|
|
1336
|
+
* still nothing shared between reads. A repeated key cannot be replayed and re-parses.
|
|
1263
1337
|
*
|
|
1264
1338
|
* @returns {Record<string, any>}
|
|
1265
1339
|
*/
|
|
@@ -1277,7 +1351,25 @@ module.exports = class Request extends LazyReadable {
|
|
|
1277
1351
|
return Object.create(null);
|
|
1278
1352
|
}
|
|
1279
1353
|
if (qp === parseQuery) {
|
|
1280
|
-
|
|
1354
|
+
const raw = this._rawQuery;
|
|
1355
|
+
if (this._querySnapRaw === raw) {
|
|
1356
|
+
const snap = this._querySnap;
|
|
1357
|
+
if (snap === false) {
|
|
1358
|
+
return parseQuery(raw);
|
|
1359
|
+
}
|
|
1360
|
+
const out = Object.create(null);
|
|
1361
|
+
const pairs = /** @type {string[]} */ (snap);
|
|
1362
|
+
for (let i = 0, len = pairs.length; i < len; i += 2) {
|
|
1363
|
+
out[pairs[i]] = pairs[i + 1];
|
|
1364
|
+
}
|
|
1365
|
+
return out;
|
|
1366
|
+
}
|
|
1367
|
+
/** @type {string[] & {invalid?: boolean}} */
|
|
1368
|
+
const capture = [];
|
|
1369
|
+
const out = parseQuery(raw, capture);
|
|
1370
|
+
this._querySnapRaw = raw;
|
|
1371
|
+
this._querySnap = capture.invalid === true ? false : capture;
|
|
1372
|
+
return out;
|
|
1281
1373
|
}
|
|
1282
1374
|
if (qp === fastQueryParse) {
|
|
1283
1375
|
return Object.assign(Object.create(null), fastQueryParse(this._rawQuery));
|
|
@@ -1686,7 +1778,9 @@ module.exports = class Request extends LazyReadable {
|
|
|
1686
1778
|
const value = entries[index + 1];
|
|
1687
1779
|
// lowercase by the entries' contract, see the field declaration
|
|
1688
1780
|
const key = entries[index];
|
|
1689
|
-
|
|
1781
|
+
// own values are never undefined, so the read answers "absent" without the hasOwn
|
|
1782
|
+
// call; a prototype-named header reads truthy and still takes the hasOwn check
|
|
1783
|
+
if (headers[key] !== undefined && Object.hasOwn(headers, key)) {
|
|
1690
1784
|
if (discardedDuplicates.has(key)) {
|
|
1691
1785
|
continue;
|
|
1692
1786
|
}
|
package/src/response.js
CHANGED
|
@@ -72,15 +72,37 @@ const symbols = Object.getOwnPropertySymbols(outgoingMessage);
|
|
|
72
72
|
// if a future node renames it, fall back to a private symbol rather than writing a property
|
|
73
73
|
// literally named "undefined", which is what indexing with undefined would do
|
|
74
74
|
const kOutHeaders = symbols.find((s) => s.toString() === "Symbol(kOutHeaders)") ?? Symbol("kOutHeaders");
|
|
75
|
+
// node's emitters tombstone a removed listener's slot instead of deleting it when this flag is
|
|
76
|
+
// set, which is what keeps _events in a stable shape. EventEmitter.init sets it, and never runs
|
|
77
|
+
// for the lazily-materialized response, so it is set by hand; a future rename degrades to the
|
|
78
|
+
// delete, not to an error
|
|
79
|
+
const kShapeMode =
|
|
80
|
+
Object.getOwnPropertySymbols(new EventEmitter()).find((s) => s.toString() === "Symbol(shapeMode)") ??
|
|
81
|
+
Symbol("shapeMode");
|
|
82
|
+
// names setHeader has validated and lowercased, so the constant names middleware writes per
|
|
83
|
+
// request are one Map hit. Insert-only after validation, bounded; only setHeader may insert,
|
|
84
|
+
// the never-throwing readers keep their plain toLowerCase
|
|
85
|
+
const VALIDATED_HEADER_NAMES = new Map();
|
|
75
86
|
const HIGH_WATERMARK = 128 * 1024;
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
const STATUSES_WITHOUT_BODY = new Set([204, 304]);
|
|
87
|
+
// the exact string json() writes, so send() can skip recomputing the charset on it
|
|
88
|
+
const JSON_UTF8 = "application/json; charset=utf-8";
|
|
79
89
|
// send's ceiling for maxAge, one year in milliseconds. Anything larger is clamped to it rather
|
|
80
90
|
// than written out, since a year is already longer than any cache will honour.
|
|
81
91
|
const MAX_MAXAGE = 60 * 60 * 24 * 365 * 1000;
|
|
82
92
|
|
|
83
93
|
class Socket extends EventEmitter {
|
|
94
|
+
/**
|
|
95
|
+
* The Socket's own error listener, shared across sockets: an error on the stand-in closes it,
|
|
96
|
+
* which is the close the connection trackers wait for. EventEmitter calls it with this = the
|
|
97
|
+
* emitter.
|
|
98
|
+
*
|
|
99
|
+
* @this {any}
|
|
100
|
+
* @param {any} err
|
|
101
|
+
*/
|
|
102
|
+
static _onError(err) {
|
|
103
|
+
this.emit("close");
|
|
104
|
+
}
|
|
105
|
+
|
|
84
106
|
/**
|
|
85
107
|
* Enough of a node socket for the middleware that reaches for one. There is no socket object
|
|
86
108
|
* in uWS to hand over, so this stands in and forwards what it can to the response.
|
|
@@ -90,10 +112,10 @@ class Socket extends EventEmitter {
|
|
|
90
112
|
constructor(response) {
|
|
91
113
|
super();
|
|
92
114
|
this.response = response;
|
|
115
|
+
this[kShapeMode] = true;
|
|
93
116
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
});
|
|
117
|
+
// shared, not an arrow: one per process instead of one per materialized socket
|
|
118
|
+
this.on("error", Socket._onError);
|
|
97
119
|
}
|
|
98
120
|
|
|
99
121
|
/** Whether anything more can be written, which stops being true once the response is done. */
|
|
@@ -276,6 +298,11 @@ module.exports = class Response extends LazyWritable {
|
|
|
276
298
|
drain: undefined
|
|
277
299
|
};
|
|
278
300
|
this._eventsCount = 0;
|
|
301
|
+
// tombstone removed listeners instead of deleting the key, as node's own streams do: the
|
|
302
|
+
// delete flipped this literal to dictionary mode on every on-finished cancel
|
|
303
|
+
this[kShapeMode] = true;
|
|
304
|
+
// on-finished stores its state here; declared so that store is not a shape change
|
|
305
|
+
this.__onFinished = null;
|
|
279
306
|
this._req = req;
|
|
280
307
|
// linked here rather than by the caller: the pair is built together, and a field the
|
|
281
308
|
// constructor leaves unset is a shape change on whoever assigns it first
|
|
@@ -801,7 +828,9 @@ module.exports = class Response extends LazyWritable {
|
|
|
801
828
|
// without one writes "Content-Length: 9223372036854775808" onto a 204. Those two paths keep
|
|
802
829
|
// µWS's own rule, which closes for a bare "close" and not for a list.
|
|
803
830
|
const closeConnection = this.req._connectionClose === true;
|
|
804
|
-
|
|
831
|
+
// 204 and 304 carry no body, so no Content-Length may describe one either; 1xx is the
|
|
832
|
+
// third case, by range
|
|
833
|
+
if (this.statusCode === 204 || this.statusCode === 304 || this.statusCode < 200) {
|
|
805
834
|
// no body and no length describing one, whatever the caller passed. node decides
|
|
806
835
|
// this the same way, from the status alone, so res.status(304).end("x") sends the
|
|
807
836
|
// status and nothing else on either.
|
|
@@ -905,9 +934,10 @@ module.exports = class Response extends LazyWritable {
|
|
|
905
934
|
if (!skipContentType) {
|
|
906
935
|
this.headers["content-type"] = "text/html; charset=utf-8";
|
|
907
936
|
}
|
|
908
|
-
} else if (typeof contentType === "string") {
|
|
937
|
+
} else if (typeof contentType === "string" && contentType !== JSON_UTF8) {
|
|
909
938
|
// replaced, not only added: the body goes out as utf-8, so a content-type saying
|
|
910
|
-
// iso-8859-1 would be describing bytes that are not there.
|
|
939
|
+
// iso-8859-1 would be describing bytes that are not there. The json() literal is
|
|
940
|
+
// already exactly that, so the common res.json answer skips the recomputation
|
|
911
941
|
this.headers["content-type"] = withUtf8Charset(contentType);
|
|
912
942
|
}
|
|
913
943
|
} else {
|
|
@@ -1374,7 +1404,16 @@ module.exports = class Response extends LazyWritable {
|
|
|
1374
1404
|
if (this.headersSent) {
|
|
1375
1405
|
throw new Error("Cannot set headers after they are sent to the client");
|
|
1376
1406
|
}
|
|
1377
|
-
|
|
1407
|
+
// one Map hit for a name already validated and lowercased: middleware writes the same
|
|
1408
|
+
// constant names on every request. Insert-only after validation, so no bad name can enter
|
|
1409
|
+
let key = VALIDATED_HEADER_NAMES.get(field);
|
|
1410
|
+
if (key === undefined) {
|
|
1411
|
+
validateHeaderName(field);
|
|
1412
|
+
key = field.toLowerCase();
|
|
1413
|
+
if (VALIDATED_HEADER_NAMES.size < 512) {
|
|
1414
|
+
VALIDATED_HEADER_NAMES.set(field, key);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1378
1417
|
if (value === undefined) {
|
|
1379
1418
|
/** @type {NodeJS.ErrnoException} */
|
|
1380
1419
|
const err = new TypeError(`Invalid value "undefined" for header "${field}"`);
|
|
@@ -1388,7 +1427,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
1388
1427
|
// catch it
|
|
1389
1428
|
const out = Array.isArray(value) ? value.map(String) : String(value);
|
|
1390
1429
|
validateHeaderValue(field, out);
|
|
1391
|
-
this.headers[
|
|
1430
|
+
this.headers[key] = out;
|
|
1392
1431
|
return this;
|
|
1393
1432
|
}
|
|
1394
1433
|
|
|
@@ -1737,7 +1776,11 @@ module.exports = class Response extends LazyWritable {
|
|
|
1737
1776
|
* @param {string} field
|
|
1738
1777
|
*/
|
|
1739
1778
|
removeHeader(field) {
|
|
1740
|
-
|
|
1779
|
+
const key = field.toLowerCase();
|
|
1780
|
+
// the delete is a runtime call, and helmet removes a header most responses never carry
|
|
1781
|
+
if (key in this.headers) {
|
|
1782
|
+
delete this.headers[key];
|
|
1783
|
+
}
|
|
1741
1784
|
}
|
|
1742
1785
|
|
|
1743
1786
|
/**
|
|
@@ -1912,7 +1955,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
1912
1955
|
*/
|
|
1913
1956
|
json(body) {
|
|
1914
1957
|
if (!this.headers["content-type"]) {
|
|
1915
|
-
this.headers["content-type"] =
|
|
1958
|
+
this.headers["content-type"] = JSON_UTF8;
|
|
1916
1959
|
}
|
|
1917
1960
|
const hot = this.app._hot();
|
|
1918
1961
|
return this.send(stringify(body, hot.jsonReplacer, hot.jsonSpaces, hot.jsonEscape));
|