fulmine.js 5.11.1 → 5.12.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.
- package/README.md +9 -3
- package/package.json +15 -16
- package/src/application.js +57 -40
- package/src/cli.js +39 -4
- package/src/declarative.js +25 -20
- package/src/index.js +9 -0
- package/src/middlewares.js +18 -10
- package/src/request.js +83 -9
- package/src/response.js +76 -32
- package/src/router.js +198 -55
- package/src/utils.js +74 -1
package/src/request.js
CHANGED
|
@@ -152,6 +152,30 @@ 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
|
+
/**
|
|
156
|
+
* Whether a content-length is a plain count of bytes, which is the only thing RFC 9112 allows.
|
|
157
|
+
*
|
|
158
|
+
* uWS trims the spaces around the value and then takes whatever is left, so "", "abc", "+1", "-1",
|
|
159
|
+
* "0x10" and "1e2" all arrive here. Every one of them makes uWS frame the request as carrying no
|
|
160
|
+
* body, and what the client sent as a body is then read as the next request on the connection.
|
|
161
|
+
* Node's parser refuses all of them outright, and so does this.
|
|
162
|
+
*
|
|
163
|
+
* @param {string} value as uWS hands it over
|
|
164
|
+
* @returns {boolean}
|
|
165
|
+
*/
|
|
166
|
+
function isByteCount(value) {
|
|
167
|
+
if (value.length === 0) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
for (let i = 0; i < value.length; i++) {
|
|
171
|
+
const code = value.charCodeAt(i);
|
|
172
|
+
if (code < 0x30 || code > 0x39) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
155
179
|
// Whose headers the shared collector below is filling. uWS's forEach is synchronous and runs no
|
|
156
180
|
// user code, so the handoff cannot interleave; module-level so the callback exists once instead
|
|
157
181
|
// of once per request.
|
|
@@ -336,6 +360,15 @@ module.exports = class Request extends LazyReadable {
|
|
|
336
360
|
(headerKey.length === 14 && headerKey === "content-length") ||
|
|
337
361
|
(headerKey.length === 17 && headerKey === "transfer-encoding")
|
|
338
362
|
) {
|
|
363
|
+
if (headerKey.length === 14) {
|
|
364
|
+
// a second content-length whatever it says, and one that is not a count of bytes:
|
|
365
|
+
// both make uWS frame the request differently from what is on the wire, see
|
|
366
|
+
// _badFraming and isByteCount
|
|
367
|
+
if (r._sawContentLength || !isByteCount(value)) {
|
|
368
|
+
r._badFraming = true;
|
|
369
|
+
}
|
|
370
|
+
r._sawContentLength = true;
|
|
371
|
+
}
|
|
339
372
|
// saying anything about framing at all, "0" included. A parser that can see a
|
|
340
373
|
// content-length answers about the body it describes, even an empty one: a zero length
|
|
341
374
|
// with a charset nobody can decode is a 415 in express and here, so a chain may only
|
|
@@ -439,6 +472,33 @@ module.exports = class Request extends LazyReadable {
|
|
|
439
472
|
*/
|
|
440
473
|
_hasBodyHeaders;
|
|
441
474
|
|
|
475
|
+
/**
|
|
476
|
+
* Whether a content-length has already been copied, so a second one is spotted. Declared for
|
|
477
|
+
* the same reason as rawIp.
|
|
478
|
+
* @type {boolean|undefined}
|
|
479
|
+
*/
|
|
480
|
+
_sawContentLength;
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Whether the request said two different things about how long its body is, so uWS may have
|
|
484
|
+
* framed it differently from the client that sent it and the proxy that forwarded it. Two
|
|
485
|
+
* shapes reach this, and node's parser refuses both outright:
|
|
486
|
+
*
|
|
487
|
+
* a repeated content-length uWS frames on the first and drops the rest, so a proxy in
|
|
488
|
+
* front reading the last one instead forwards bytes uWS then
|
|
489
|
+
* answers as a second, pipelined request
|
|
490
|
+
* one that is not a byte count uWS keeps whatever is left after trimming, an empty value
|
|
491
|
+
* included, and frames the request as carrying no body at all,
|
|
492
|
+
* which turns the body the client sent into that same second
|
|
493
|
+
* request. See isByteCount
|
|
494
|
+
*
|
|
495
|
+
* Either way it is request smuggling, and the request is refused rather than routed. Declared
|
|
496
|
+
* for the same reason as rawIp.
|
|
497
|
+
*
|
|
498
|
+
* @type {boolean|undefined}
|
|
499
|
+
*/
|
|
500
|
+
_badFraming;
|
|
501
|
+
|
|
442
502
|
/**
|
|
443
503
|
* Whether the client asked for the connection to be closed. Declared for the same reason.
|
|
444
504
|
* @type {boolean|undefined}
|
|
@@ -504,10 +564,18 @@ module.exports = class Request extends LazyReadable {
|
|
|
504
564
|
// which is 0.2us on a request that is about to read a body.
|
|
505
565
|
const length = req.getHeader("content-length");
|
|
506
566
|
const transferEncoding = req.getHeader("transfer-encoding");
|
|
567
|
+
// A content-length of "0" declares no body and used to stay on the cheap side, but
|
|
568
|
+
// getHeader only ever returns the first of a repeated header, so a duplicate cannot be
|
|
569
|
+
// seen from here, and a duplicate has to be refused rather than routed: see
|
|
570
|
+
// _badFraming. Anything that says a word about framing takes the full copy instead.
|
|
571
|
+
//
|
|
572
|
+
// One shape stays invisible here, a content-length present with an empty value: uWS
|
|
573
|
+
// answers "" for that and for a header that was never sent, and nothing in its API
|
|
574
|
+
// tells them apart. It frames both as carrying no body, which is the right reading of
|
|
575
|
+
// the second, so this server stays consistent with itself either way. The full copy
|
|
576
|
+
// below does refuse it, which is every request except a GET whose whole chain provably
|
|
577
|
+
// reads no header at all.
|
|
507
578
|
if (length !== "" || transferEncoding !== "") {
|
|
508
|
-
this._hasBodyHeaders = true;
|
|
509
|
-
}
|
|
510
|
-
if ((length !== "" && length !== "0") || transferEncoding !== "") {
|
|
511
579
|
currentRequest = this;
|
|
512
580
|
this._req.forEach(Request.#collectHeader);
|
|
513
581
|
currentRequest = null;
|
|
@@ -592,6 +660,10 @@ module.exports = class Request extends LazyReadable {
|
|
|
592
660
|
this._isOptions = this.method === "OPTIONS";
|
|
593
661
|
this._isHead = this.method === "HEAD";
|
|
594
662
|
}
|
|
663
|
+
// the folded _opPath and the percent scan of _originalPath, built on the hop that first
|
|
664
|
+
// wants them and dropped by every rewrite, see _pathMatches and Walk#dispatch
|
|
665
|
+
this._opPathLower = null;
|
|
666
|
+
this._mayFailDecode = null;
|
|
595
667
|
this.params = {};
|
|
596
668
|
|
|
597
669
|
// Two Sets per request, for two things almost no request needs.
|
|
@@ -812,7 +884,7 @@ module.exports = class Request extends LazyReadable {
|
|
|
812
884
|
* meant to carry one value, but nothing stops a proxy from appending.
|
|
813
885
|
*/
|
|
814
886
|
get #authority() {
|
|
815
|
-
const trust = this.app.
|
|
887
|
+
const trust = this.app._hot().trustProxyFn;
|
|
816
888
|
// parsedIp is what connection.remoteAddress carries, without building the socket stand-in
|
|
817
889
|
const isTrusted = !!(trust && trust(this.parsedIp, 0));
|
|
818
890
|
const rawHeader = (isTrusted && this.headers["x-forwarded-host"]) || this.headers["host"];
|
|
@@ -886,7 +958,7 @@ module.exports = class Request extends LazyReadable {
|
|
|
886
958
|
* @returns {string|undefined} undefined on a unix socket, which has no address
|
|
887
959
|
*/
|
|
888
960
|
get ip() {
|
|
889
|
-
const trust = this.app.
|
|
961
|
+
const trust = this.app._hot().trustProxyFn;
|
|
890
962
|
if (!trust) {
|
|
891
963
|
return this.parsedIp;
|
|
892
964
|
}
|
|
@@ -899,7 +971,7 @@ module.exports = class Request extends LazyReadable {
|
|
|
899
971
|
* @returns {string[]}
|
|
900
972
|
*/
|
|
901
973
|
get ips() {
|
|
902
|
-
const trust = this.app.
|
|
974
|
+
const trust = this.app._hot().trustProxyFn;
|
|
903
975
|
if (!trust) {
|
|
904
976
|
return [];
|
|
905
977
|
}
|
|
@@ -918,7 +990,7 @@ module.exports = class Request extends LazyReadable {
|
|
|
918
990
|
// own ssl flag answers when nothing has built the stand-in yet
|
|
919
991
|
const conn = this.#cachedConnection;
|
|
920
992
|
const proto = (conn ? conn.encrypted : this.app.ssl) ? "https" : "http";
|
|
921
|
-
const trust = this.app.
|
|
993
|
+
const trust = this.app._hot().trustProxyFn;
|
|
922
994
|
if (!trust) {
|
|
923
995
|
return proto;
|
|
924
996
|
}
|
|
@@ -956,6 +1028,8 @@ module.exports = class Request extends LazyReadable {
|
|
|
956
1028
|
this.path = newPath;
|
|
957
1029
|
this.endsWithSlash = newPath.charCodeAt(newPath.length - 1) === 0x2f;
|
|
958
1030
|
this._opPath = newPath;
|
|
1031
|
+
this._opPathLower = null;
|
|
1032
|
+
this._mayFailDecode = null;
|
|
959
1033
|
this._lastUrl = newUrl;
|
|
960
1034
|
}
|
|
961
1035
|
|
|
@@ -985,7 +1059,7 @@ module.exports = class Request extends LazyReadable {
|
|
|
985
1059
|
* @returns {Record<string, any>}
|
|
986
1060
|
*/
|
|
987
1061
|
get query() {
|
|
988
|
-
const qp = this.app.
|
|
1062
|
+
const qp = this.app._hot().queryParserFn;
|
|
989
1063
|
// the vendored default already answers on a bare null prototype, so it goes out as is;
|
|
990
1064
|
// any other parser is copied onto one, which is what kept fast-querystring's result from
|
|
991
1065
|
// inspecting as "Empty <[Object: null prototype] {}>" where Express shows the bare form
|
|
@@ -1053,7 +1127,7 @@ module.exports = class Request extends LazyReadable {
|
|
|
1053
1127
|
*/
|
|
1054
1128
|
_readRawIp() {
|
|
1055
1129
|
const uwsRes = this._res;
|
|
1056
|
-
if (this.app.
|
|
1130
|
+
if (this.app._hot().trustProxyProtocol) {
|
|
1057
1131
|
const proxied = uwsRes.getProxiedRemoteAddress();
|
|
1058
1132
|
// empty unless a preamble arrived, which is the only thing that tells the two apart
|
|
1059
1133
|
if (proxied.byteLength !== 0) {
|
package/src/response.js
CHANGED
|
@@ -31,6 +31,9 @@ const {
|
|
|
31
31
|
isPreconditionFailure,
|
|
32
32
|
isRangeFresh,
|
|
33
33
|
escapeHtml,
|
|
34
|
+
validateHeaderName,
|
|
35
|
+
validateHeaderValue,
|
|
36
|
+
headerIsWritable,
|
|
34
37
|
withDefaultCharset,
|
|
35
38
|
withUtf8Charset,
|
|
36
39
|
asStatError,
|
|
@@ -293,7 +296,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
293
296
|
// "connection headers" off advertises neither, which Express always does: see below for
|
|
294
297
|
// the one this still writes.
|
|
295
298
|
this.headers =
|
|
296
|
-
res._nodeRes || app.
|
|
299
|
+
res._nodeRes || app._settings["connection headers"] === false
|
|
297
300
|
? {}
|
|
298
301
|
: {
|
|
299
302
|
connection: "keep-alive",
|
|
@@ -305,7 +308,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
305
308
|
if (req._connectionClose) {
|
|
306
309
|
this.headers.connection = "close";
|
|
307
310
|
}
|
|
308
|
-
if (
|
|
311
|
+
if (app._hot().xPoweredBy) {
|
|
309
312
|
this.headers["x-powered-by"] = "Fulmine";
|
|
310
313
|
}
|
|
311
314
|
|
|
@@ -613,6 +616,10 @@ module.exports = class Response extends LazyWritable {
|
|
|
613
616
|
* not one of them: uWS wants the length through tryEnd or endWithoutBody, so it is taken out
|
|
614
617
|
* here and kept on totalSize, where it also turns chunked framing off.
|
|
615
618
|
*
|
|
619
|
+
* One writeHeader per header on purpose. Packing the whole head into a single writeStatus
|
|
620
|
+
* works on the wire, and was measured slower: constant header strings cross the boundary
|
|
621
|
+
* already flat, while the packed head is concatenated fresh per response, see issue #11.
|
|
622
|
+
*
|
|
616
623
|
* @param {boolean} utf8 unused, kept because node's equivalent takes it and the two callers
|
|
617
624
|
* differ on what they know about the body
|
|
618
625
|
*/
|
|
@@ -881,8 +888,18 @@ module.exports = class Response extends LazyWritable {
|
|
|
881
888
|
// req.fresh, which compares If-None-Match against it.
|
|
882
889
|
// body is defined by the time it gets here, so an empty one still earns an ETag. Testing
|
|
883
890
|
// its truthiness instead meant send("") and send(null) came back without one.
|
|
884
|
-
|
|
885
|
-
|
|
891
|
+
// Every method by default, not only GET and HEAD: gating it looked safe, freshness being
|
|
892
|
+
// defined over those two alone, and express's own suite failed on it, "should send ETag
|
|
893
|
+
// in response to <METHOD> request" exists per method. The "etag methods" setting is that
|
|
894
|
+
// gate as an opt-in, see issue #10.
|
|
895
|
+
const hot = this.app._hot();
|
|
896
|
+
const etagFn = hot.etagFn;
|
|
897
|
+
if (
|
|
898
|
+
etagFn &&
|
|
899
|
+
!this.headers["etag"] &&
|
|
900
|
+
!this.req.noEtag &&
|
|
901
|
+
(hot.etagMethods === null || hot.etagMethods.has(this.req.method))
|
|
902
|
+
) {
|
|
886
903
|
const etag = etagFn(body);
|
|
887
904
|
// an application's own etag function is allowed to decline: returning nothing means no
|
|
888
905
|
// header, rather than a header saying "undefined"
|
|
@@ -1049,7 +1066,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
1049
1066
|
let stat = options._stat;
|
|
1050
1067
|
if (!stat) {
|
|
1051
1068
|
try {
|
|
1052
|
-
stat = cachedStat(fullpath, this.app.
|
|
1069
|
+
stat = cachedStat(fullpath, this.app._settings["stat cache ms"]);
|
|
1053
1070
|
} catch (err) {
|
|
1054
1071
|
// the fs error itself, carrying its errno and path, with send's status written on
|
|
1055
1072
|
// it: a missing file is the request's 404, an unreadable one is the server's 500
|
|
@@ -1318,27 +1335,51 @@ module.exports = class Response extends LazyWritable {
|
|
|
1318
1335
|
* @param {any} value an array sends the header once per entry
|
|
1319
1336
|
* @returns {this}
|
|
1320
1337
|
* @throws {Error} once the headers have gone out
|
|
1321
|
-
* @throws {TypeError} if the name is not a
|
|
1338
|
+
* @throws {TypeError} if the name is not a token, the value is undefined, or the value holds a
|
|
1339
|
+
* character that cannot go on the wire
|
|
1322
1340
|
*/
|
|
1323
1341
|
setHeader(field, value) {
|
|
1324
1342
|
if (this.headersSent) {
|
|
1325
1343
|
throw new Error("Cannot set headers after they are sent to the client");
|
|
1326
1344
|
}
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
// writeHeader would throw mid-response
|
|
1334
|
-
this.headers[field] = value.map(String);
|
|
1335
|
-
return this;
|
|
1336
|
-
}
|
|
1337
|
-
this.headers[field] = String(value);
|
|
1345
|
+
validateHeaderName(field);
|
|
1346
|
+
if (value === undefined) {
|
|
1347
|
+
/** @type {NodeJS.ErrnoException} */
|
|
1348
|
+
const err = new TypeError(`Invalid value "undefined" for header "${field}"`);
|
|
1349
|
+
err.code = "ERR_HTTP_INVALID_HEADER_VALUE";
|
|
1350
|
+
throw err;
|
|
1338
1351
|
}
|
|
1352
|
+
// each entry as text, as node serialises them: a raw number reaching uWS's writeHeader
|
|
1353
|
+
// would throw mid-response. Coerced before it is checked, since that is the string the
|
|
1354
|
+
// wire gets, and checked before it is stored: a value that got in here would be written by
|
|
1355
|
+
// whatever flushes next, and on the error path that is a second throw with nobody left to
|
|
1356
|
+
// catch it
|
|
1357
|
+
const out = Array.isArray(value) ? value.map(String) : String(value);
|
|
1358
|
+
validateHeaderValue(field, out);
|
|
1359
|
+
this.headers[field.toLowerCase()] = out;
|
|
1339
1360
|
return this;
|
|
1340
1361
|
}
|
|
1341
1362
|
|
|
1363
|
+
/**
|
|
1364
|
+
* Throws away any header that could not be written, so that flushing this response cannot fail
|
|
1365
|
+
* on one. setHeader refuses these on the way in, but `res.headers` is the live object, so an
|
|
1366
|
+
* assignment into that still gets a value in here.
|
|
1367
|
+
*
|
|
1368
|
+
* Only the error page calls it. A throw out of the flush there is not recoverable: the error
|
|
1369
|
+
* page is what runs after a throw, so it would be the second one, with nobody left to catch
|
|
1370
|
+
* it, and on the node shim that is the process. Everything writable is left alone, since a
|
|
1371
|
+
* middleware's own headers belong on the error response too.
|
|
1372
|
+
*
|
|
1373
|
+
* @returns {void}
|
|
1374
|
+
*/
|
|
1375
|
+
_dropUnwritableHeaders() {
|
|
1376
|
+
for (const header in this.headers) {
|
|
1377
|
+
if (!headerIsWritable(header, this.headers[header])) {
|
|
1378
|
+
delete this.headers[header];
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1342
1383
|
/**
|
|
1343
1384
|
* Hands the status line and the headers over now, without waiting for a body, which is node's
|
|
1344
1385
|
* flushHeaders(). Callers use it to let the client start on the head while the body is still
|
|
@@ -1387,10 +1428,6 @@ module.exports = class Response extends LazyWritable {
|
|
|
1387
1428
|
* @param {() => void} [callback]
|
|
1388
1429
|
* @returns {void}
|
|
1389
1430
|
*/
|
|
1390
|
-
|
|
1391
|
-
/**
|
|
1392
|
-
*
|
|
1393
|
-
*/
|
|
1394
1431
|
writeEarlyHints(hints, callback) {
|
|
1395
1432
|
this.#refuseInformationAfterHead();
|
|
1396
1433
|
// node writes the hints first and calls back after, so a caller that sequences work on it
|
|
@@ -1460,7 +1497,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
1460
1497
|
}
|
|
1461
1498
|
|
|
1462
1499
|
/**
|
|
1463
|
-
* node's `assignSocket
|
|
1500
|
+
* node's `assignSocket`, which the http server uses when a response is
|
|
1464
1501
|
* handed a raw socket. There is no such socket here.
|
|
1465
1502
|
*
|
|
1466
1503
|
* @param {any} [socket]
|
|
@@ -1469,6 +1506,8 @@ module.exports = class Response extends LazyWritable {
|
|
|
1469
1506
|
assignSocket(socket) {}
|
|
1470
1507
|
|
|
1471
1508
|
/**
|
|
1509
|
+
* node's `detachSocket`, which the http server uses when a response is
|
|
1510
|
+
* handed a raw socket. There is no such socket here.
|
|
1472
1511
|
* @param {any} [socket]
|
|
1473
1512
|
* @returns {void}
|
|
1474
1513
|
*/
|
|
@@ -1595,11 +1634,11 @@ module.exports = class Response extends LazyWritable {
|
|
|
1595
1634
|
this.set(header, field[header]);
|
|
1596
1635
|
}
|
|
1597
1636
|
} else {
|
|
1598
|
-
|
|
1637
|
+
const name = field.toLowerCase();
|
|
1599
1638
|
// a header is text on the wire whatever it was here, and Express coerces at this point,
|
|
1600
1639
|
// so res.get answers what was sent rather than the number or object it was given
|
|
1601
1640
|
let out = Array.isArray(value) ? value.map(String) : String(value);
|
|
1602
|
-
if (
|
|
1641
|
+
if (name === "content-type") {
|
|
1603
1642
|
if (Array.isArray(out)) {
|
|
1604
1643
|
throw new TypeError("Content-Type cannot be set to an Array");
|
|
1605
1644
|
}
|
|
@@ -1607,6 +1646,9 @@ module.exports = class Response extends LazyWritable {
|
|
|
1607
1646
|
// missing application/manifest+json among others, which Express does charset.
|
|
1608
1647
|
out = withDefaultCharset(out);
|
|
1609
1648
|
}
|
|
1649
|
+
// the name as it was written, not the lowercased one: setHeader lowercases it itself,
|
|
1650
|
+
// and it is the name that a refused header is reported by, which Express takes from
|
|
1651
|
+
// what the caller passed
|
|
1610
1652
|
this.setHeader(field, out);
|
|
1611
1653
|
}
|
|
1612
1654
|
return this;
|
|
@@ -1643,12 +1685,15 @@ module.exports = class Response extends LazyWritable {
|
|
|
1643
1685
|
}
|
|
1644
1686
|
|
|
1645
1687
|
/**
|
|
1646
|
-
* Every header set so far, as
|
|
1647
|
-
*
|
|
1688
|
+
* Every header set so far, as a shallow copy on a null prototype, which is what node's
|
|
1689
|
+
* OutgoingMessage answers. It used to hand out the live object, and a write into that
|
|
1690
|
+
* reached the wire without setHeader's validation, see issue #6; nothing in here or in the
|
|
1691
|
+
* middleware that was checked relies on the live one, so the copy costs an allocation on a
|
|
1692
|
+
* method the framework itself never calls.
|
|
1648
1693
|
* @returns {Record<string, any>}
|
|
1649
1694
|
*/
|
|
1650
1695
|
getHeaders() {
|
|
1651
|
-
return this.headers;
|
|
1696
|
+
return Object.assign({ __proto__: null }, this.headers);
|
|
1652
1697
|
}
|
|
1653
1698
|
|
|
1654
1699
|
/**
|
|
@@ -1837,10 +1882,8 @@ module.exports = class Response extends LazyWritable {
|
|
|
1837
1882
|
if (!this.headers["content-type"]) {
|
|
1838
1883
|
this.headers["content-type"] = "application/json; charset=utf-8";
|
|
1839
1884
|
}
|
|
1840
|
-
const
|
|
1841
|
-
|
|
1842
|
-
const spaces = this.app.get("json spaces");
|
|
1843
|
-
return this.send(stringify(body, replacer, spaces, escape));
|
|
1885
|
+
const hot = this.app._hot();
|
|
1886
|
+
return this.send(stringify(body, hot.jsonReplacer, hot.jsonSpaces, hot.jsonEscape));
|
|
1844
1887
|
}
|
|
1845
1888
|
|
|
1846
1889
|
/**
|
|
@@ -2002,7 +2045,8 @@ module.exports = class Response extends LazyWritable {
|
|
|
2002
2045
|
type(type) {
|
|
2003
2046
|
const ct = type.indexOf("/") === -1 ? contentTypeFor(type) : type;
|
|
2004
2047
|
|
|
2005
|
-
|
|
2048
|
+
// the name Express passes, since a refused value is reported by the name it was set under
|
|
2049
|
+
return this.set("Content-Type", ct);
|
|
2006
2050
|
}
|
|
2007
2051
|
|
|
2008
2052
|
/**
|