fulmine.js 5.19.3 → 5.19.4
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/package.json +1 -1
- package/src/application.js +25 -36
- package/src/cli.js +22 -16
- package/src/cluster.js +2 -2
- package/src/compression.js +102 -61
- package/src/declarative.js +47 -24
- package/src/hot-settings.js +5 -5
- package/src/lazy-readable.js +8 -6
- package/src/lazy-writable.js +3 -3
- package/src/middlewares.js +94 -55
- package/src/nest.js +3 -2
- package/src/node-shim.js +10 -5
- package/src/optimizer.js +9 -7
- package/src/options.d.ts +9 -4
- package/src/request-utils.js +3 -2
- package/src/request.js +49 -38
- package/src/response.js +145 -109
- package/src/route.js +3 -3
- package/src/router-utils.js +62 -14
- package/src/router.js +46 -28
- package/src/server-shape.js +14 -10
- package/src/server-timing.js +16 -4
- package/src/socket.js +3 -3
- package/src/testing.js +4 -3
- package/src/usage.js +10 -5
- package/src/utils.js +120 -23
- package/src/verify.js +4 -3
- package/src/view.js +1 -1
- package/src/walk.js +8 -7
- package/src/websocket.js +17 -8
- package/src/work.js +3 -3
package/src/response.js
CHANGED
|
@@ -38,6 +38,8 @@ const {
|
|
|
38
38
|
withUtf8Charset,
|
|
39
39
|
asStatError,
|
|
40
40
|
httpError,
|
|
41
|
+
headersSentError,
|
|
42
|
+
applyWriteHead,
|
|
41
43
|
contentTypeFor,
|
|
42
44
|
statTag,
|
|
43
45
|
cachedStat,
|
|
@@ -79,6 +81,9 @@ const JSON_UTF8 = "application/json; charset=utf-8";
|
|
|
79
81
|
// than written out, since a year is already longer than any cache will honour.
|
|
80
82
|
const MAX_MAXAGE = 60 * 60 * 24 * 365 * 1000;
|
|
81
83
|
|
|
84
|
+
// what send takes as a range request: the bytes unit, checked on the header's text before parsing
|
|
85
|
+
const BYTES_RANGE = /^ *bytes=/;
|
|
86
|
+
|
|
82
87
|
module.exports = class Response extends LazyWritable {
|
|
83
88
|
/** @type {Socket|null} */
|
|
84
89
|
#socket = null;
|
|
@@ -106,15 +111,21 @@ module.exports = class Response extends LazyWritable {
|
|
|
106
111
|
/** Whether a flush is already booked for the end of this turn. */
|
|
107
112
|
#flushBooked = false;
|
|
108
113
|
|
|
109
|
-
/**
|
|
110
|
-
#
|
|
114
|
+
/** Whether the status line and the headers have reached uWS, which only a body write does. */
|
|
115
|
+
#headOut = false;
|
|
111
116
|
|
|
112
117
|
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* @type {
|
|
118
|
+
* The status line as writeHead settled it, which is the one the wire gets: node stores the
|
|
119
|
+
* head at writeHead, so a status set later never reaches the client.
|
|
120
|
+
* @type {number}
|
|
116
121
|
*/
|
|
117
|
-
|
|
122
|
+
#status = 200;
|
|
123
|
+
|
|
124
|
+
/** @type {string|undefined} */
|
|
125
|
+
#statusText = undefined;
|
|
126
|
+
|
|
127
|
+
/** @type {Response["headers"]|null} */
|
|
128
|
+
#outHeaders = null;
|
|
118
129
|
|
|
119
130
|
/**
|
|
120
131
|
* The request this response answers, linked so either reaches the other.
|
|
@@ -127,10 +138,9 @@ module.exports = class Response extends LazyWritable {
|
|
|
127
138
|
* two that describe the connection, since every response carries them, and x-powered-by only
|
|
128
139
|
* when the setting asks for it.
|
|
129
140
|
*
|
|
130
|
-
* @param {
|
|
131
|
-
* @param {
|
|
132
|
-
*
|
|
133
|
-
* @param {any} app the application or router this request arrived at
|
|
141
|
+
* @param {import("uWebSockets.js").HttpResponse} res the uWS response
|
|
142
|
+
* @param {InstanceType<typeof import("./request.js")>} req the Request, already built
|
|
143
|
+
* @param {import("./application.js").Application} app the application this request arrived at
|
|
134
144
|
*/
|
|
135
145
|
constructor(res, req, app) {
|
|
136
146
|
super();
|
|
@@ -139,6 +149,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
139
149
|
// the order node's own Writable constructor lays down, so the hidden class is the one every
|
|
140
150
|
// other stream in the process has, and node's init keeps this object when the state is
|
|
141
151
|
// finally built
|
|
152
|
+
/** @type {Record<string, Function|undefined>} */
|
|
142
153
|
this._events = {
|
|
143
154
|
close: undefined,
|
|
144
155
|
error: undefined,
|
|
@@ -191,7 +202,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
191
202
|
this.body = undefined;
|
|
192
203
|
// what was handed to uWS, kept so a caller asking for content-length after the fact can be
|
|
193
204
|
// answered, see get(). Undefined until the response ends, and for one that sends no body
|
|
194
|
-
/** @type {string|Buffer|undefined} */
|
|
205
|
+
/** @type {string|Buffer|Uint8Array|undefined} */
|
|
195
206
|
this._sentBody = undefined;
|
|
196
207
|
// false while the uWS route handler is still in its synchronous window, where uWS holds
|
|
197
208
|
// the socket corked itself; the two uWS entry points flip it once that window closes
|
|
@@ -206,8 +217,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
206
217
|
//
|
|
207
218
|
// The condition is the safety of it: a fresh response has no listeners, so both slots are
|
|
208
219
|
// free, and anything else falls back to on(), which finds what this wrote.
|
|
209
|
-
|
|
210
|
-
const self = /** @type {any} */ (this);
|
|
220
|
+
const self = this;
|
|
211
221
|
const events = self._events;
|
|
212
222
|
if (
|
|
213
223
|
self._eventsCount === 0 &&
|
|
@@ -252,7 +262,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
252
262
|
* aborted uWS response is a use after free. writableEnded reads true after this, where node
|
|
253
263
|
* leaves it false until end() is called.
|
|
254
264
|
*
|
|
255
|
-
* @param {
|
|
265
|
+
* @param {Error} [error] whatever the caller is destroying the response with
|
|
256
266
|
* @returns {this}
|
|
257
267
|
*/
|
|
258
268
|
destroy(error) {
|
|
@@ -282,7 +292,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
282
292
|
return;
|
|
283
293
|
}
|
|
284
294
|
this._pendingLinked = false;
|
|
285
|
-
const pending = /** @type {
|
|
295
|
+
const pending = /** @type {{_pendingIn?: {head: Response|null}}} */ (this)._pendingIn;
|
|
286
296
|
const prev = this._pendingPrev;
|
|
287
297
|
const next = this._pendingNext;
|
|
288
298
|
if (prev) {
|
|
@@ -337,7 +347,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
337
347
|
* over, as node's does; the request's `socket` is the same object and stays, so it comes
|
|
338
348
|
* through here instead.
|
|
339
349
|
*
|
|
340
|
-
* @returns {
|
|
350
|
+
* @returns {Socket}
|
|
341
351
|
*/
|
|
342
352
|
_socketShim() {
|
|
343
353
|
if (!this.#socket) {
|
|
@@ -428,13 +438,15 @@ module.exports = class Response extends LazyWritable {
|
|
|
428
438
|
|
|
429
439
|
this.writingChunk = true;
|
|
430
440
|
this._res.cork(() => {
|
|
431
|
-
if (!this
|
|
432
|
-
|
|
441
|
+
if (!this.#headOut) {
|
|
442
|
+
if (!this.headersSent) {
|
|
443
|
+
this.writeHead(this.statusCode);
|
|
444
|
+
}
|
|
433
445
|
// "unknown" and not the bare number: node writes that reason phrase for a code it
|
|
434
446
|
// has no message for, so the raw status lines match. The default 200 with no
|
|
435
447
|
// phrase is uWS's own head, byte for byte, so it is not written at all
|
|
436
|
-
if (this
|
|
437
|
-
this._res.writeStatus(statusLine(this
|
|
448
|
+
if (this.#status !== 200 || this.#statusText !== undefined) {
|
|
449
|
+
this._res.writeStatus(statusLine(this.#status, this.#statusText));
|
|
438
450
|
}
|
|
439
451
|
this.writeHeaders(typeof chunk === "string");
|
|
440
452
|
}
|
|
@@ -503,8 +515,9 @@ module.exports = class Response extends LazyWritable {
|
|
|
503
515
|
|
|
504
516
|
/**
|
|
505
517
|
* Sets the status and, optionally, a batch of headers, the way node does. The second argument
|
|
506
|
-
* is either the status message or the headers, since node allows both shapes.
|
|
507
|
-
*
|
|
518
|
+
* is either the status message or the headers, since node allows both shapes. The bytes go out
|
|
519
|
+
* with the body, but the head is settled here, as node's is: headersSent reads true from now
|
|
520
|
+
* on, what is set later throws, and a status written later never reaches the wire.
|
|
508
521
|
*
|
|
509
522
|
* Every header goes through setHeader and not through set. This is node's method, not
|
|
510
523
|
* Express's: a content-type given here keeps the value it was given, where res.set would append
|
|
@@ -512,38 +525,23 @@ module.exports = class Response extends LazyWritable {
|
|
|
512
525
|
* @sveltejs/adapter-node included, so the charset was added to pages nobody asked it for.
|
|
513
526
|
*
|
|
514
527
|
* @param {number} statusCode
|
|
515
|
-
* @param {string|
|
|
516
|
-
*
|
|
528
|
+
* @param {string|import("http").OutgoingHttpHeaders|import("http").OutgoingHttpHeader[]} [statusMessage] the
|
|
529
|
+
* reason phrase, or the headers
|
|
530
|
+
* @param {import("http").OutgoingHttpHeaders|import("http").OutgoingHttpHeader[]} [headers]
|
|
517
531
|
* @returns {this}
|
|
518
532
|
*/
|
|
519
533
|
writeHead(statusCode, statusMessage, headers) {
|
|
520
|
-
this.
|
|
521
|
-
|
|
522
|
-
this.statusText = statusMessage;
|
|
523
|
-
}
|
|
524
|
-
if (!headers) {
|
|
525
|
-
if (!statusMessage) return this;
|
|
526
|
-
// the two-argument shape, where what looked like a reason phrase is the headers. A
|
|
527
|
-
// string reaching here was already taken as the phrase above and simply has no keys.
|
|
528
|
-
headers = /** @type {Record<string, any>} */ (statusMessage);
|
|
529
|
-
}
|
|
530
|
-
if (Array.isArray(headers)) {
|
|
531
|
-
// node takes a flat list here, name then value, and not a list of pairs. An odd length
|
|
532
|
-
// is the caller's mistake and node names the argument in what it throws
|
|
533
|
-
if (headers.length % 2 !== 0) {
|
|
534
|
-
/** @type {NodeJS.ErrnoException} */
|
|
535
|
-
const err = new TypeError(`The argument 'headers' is invalid. Received ${JSON.stringify(headers)}`);
|
|
536
|
-
err.code = "ERR_INVALID_ARG_VALUE";
|
|
537
|
-
throw err;
|
|
538
|
-
}
|
|
539
|
-
for (let i = 0; i < headers.length; i += 2) {
|
|
540
|
-
this.setHeader(headers[i], headers[i + 1]);
|
|
541
|
-
}
|
|
542
|
-
return this;
|
|
534
|
+
if (this.headersSent) {
|
|
535
|
+
throw headersSentError("write");
|
|
543
536
|
}
|
|
544
|
-
|
|
545
|
-
|
|
537
|
+
this.statusCode = statusCode;
|
|
538
|
+
const reason = applyWriteHead(this, statusMessage, headers);
|
|
539
|
+
if (reason !== undefined) {
|
|
540
|
+
this.statusText = reason;
|
|
546
541
|
}
|
|
542
|
+
this.#status = statusCode;
|
|
543
|
+
this.#statusText = this.statusText;
|
|
544
|
+
this.headersSent = true;
|
|
547
545
|
return this;
|
|
548
546
|
}
|
|
549
547
|
|
|
@@ -597,16 +595,19 @@ module.exports = class Response extends LazyWritable {
|
|
|
597
595
|
}
|
|
598
596
|
}
|
|
599
597
|
this.headersSent = true;
|
|
598
|
+
this.#headOut = true;
|
|
600
599
|
}
|
|
601
600
|
|
|
602
601
|
/**
|
|
603
|
-
* What node calls before writing a body when the caller never called writeHead
|
|
604
|
-
* nothing
|
|
602
|
+
* What node calls before writing a body when the caller never called writeHead: it settles the
|
|
603
|
+
* head, and nothing is flushed, since the headers are written with the body. Not once the head
|
|
604
|
+
* is settled: the compression module calls this on the strength of node's _header, which this
|
|
605
|
+
* response does not keep, so the guard is here instead of there.
|
|
605
606
|
*/
|
|
606
607
|
_implicitHeader() {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
608
|
+
if (!this.headersSent) {
|
|
609
|
+
this.writeHead(this.statusCode);
|
|
610
|
+
}
|
|
610
611
|
}
|
|
611
612
|
|
|
612
613
|
/**
|
|
@@ -645,17 +646,29 @@ module.exports = class Response extends LazyWritable {
|
|
|
645
646
|
}
|
|
646
647
|
|
|
647
648
|
/**
|
|
648
|
-
* @param {
|
|
649
|
-
*
|
|
649
|
+
* @param {string|Buffer|Uint8Array|null|(() => void)} [data] the last body piece, or the callback in
|
|
650
|
+
* node's one-argument shape
|
|
651
|
+
* @param {BufferEncoding|(() => void)} [encoding] how a string body is encoded, or the callback in
|
|
652
|
+
* node's two-argument shape
|
|
653
|
+
* @param {() => void} [cb]
|
|
650
654
|
* @returns {this}
|
|
651
655
|
*/
|
|
652
|
-
end(data, cb) {
|
|
656
|
+
end(data, encoding, cb) {
|
|
653
657
|
if (typeof data === "function") {
|
|
654
658
|
cb = data;
|
|
655
659
|
data = undefined;
|
|
660
|
+
encoding = undefined;
|
|
661
|
+
} else if (typeof encoding === "function") {
|
|
662
|
+
cb = encoding;
|
|
663
|
+
encoding = undefined;
|
|
656
664
|
}
|
|
657
665
|
if (typeof cb !== "function") {
|
|
658
|
-
cb = undefined;
|
|
666
|
+
cb = undefined;
|
|
667
|
+
}
|
|
668
|
+
// uWS takes a string as utf-8 and nothing else, so any other encoding is applied here, the
|
|
669
|
+
// way write() applies it: res.end(data, "binary") is how old code sends an image
|
|
670
|
+
if (typeof data === "string" && encoding !== undefined && encoding !== "utf8" && encoding !== "utf-8") {
|
|
671
|
+
data = Buffer.from(data, encoding);
|
|
659
672
|
}
|
|
660
673
|
|
|
661
674
|
if (this.writingChunk) {
|
|
@@ -667,7 +680,11 @@ module.exports = class Response extends LazyWritable {
|
|
|
667
680
|
if (this.finished) {
|
|
668
681
|
return this;
|
|
669
682
|
}
|
|
670
|
-
|
|
683
|
+
// as node's end() calls _implicitHeader: not after an explicit writeHead, which settled the
|
|
684
|
+
// head already and told on-headers' listeners
|
|
685
|
+
if (!this.headersSent) {
|
|
686
|
+
this.writeHead(this.statusCode);
|
|
687
|
+
}
|
|
671
688
|
// uWS holds the socket corked for the synchronous window of its route handler, and
|
|
672
689
|
// cork inside cork is a passthrough: the wrapper and its closure are only paid once the
|
|
673
690
|
// answer has outlived that window, which is what _corkNeeded records
|
|
@@ -683,23 +700,23 @@ module.exports = class Response extends LazyWritable {
|
|
|
683
700
|
* The corked tail of end(): status, headers, body and the finish events. Split out so a
|
|
684
701
|
* synchronous answer calls it straight, already inside uWS's own cork.
|
|
685
702
|
*
|
|
686
|
-
* @param {
|
|
687
|
-
* @param {
|
|
703
|
+
* @param {string|Buffer|Uint8Array|null|undefined} data the last body piece
|
|
704
|
+
* @param {(() => void)|undefined} cb
|
|
688
705
|
*/
|
|
689
706
|
_finish(data, cb) {
|
|
690
707
|
// read before the head is written below, which is what sets the flag: what matters further
|
|
691
708
|
// down is whether something had already committed the framing, a flushHeaders() or a first
|
|
692
709
|
// res.write(), not whether this call is about to write the head itself
|
|
693
|
-
const headWasAlreadyOut = this
|
|
694
|
-
if (!this
|
|
710
|
+
const headWasAlreadyOut = this.#headOut;
|
|
711
|
+
if (!this.#headOut) {
|
|
695
712
|
// freshness is not decided here. node's end() knows nothing about conditional requests,
|
|
696
713
|
// and Express answers 304 from send() and from sendFile(), each of which strips the
|
|
697
714
|
// entity headers first. Deciding it here made res.end("body") answer 304 and drop the
|
|
698
715
|
// body the caller had just written.
|
|
699
716
|
// "unknown" for a code without a message, as node's status line has it. The default 200
|
|
700
717
|
// with no phrase is not written at all: uWS emits the identical head on its own
|
|
701
|
-
if (this
|
|
702
|
-
this._res.writeStatus(statusLine(this
|
|
718
|
+
if (this.#status !== 200 || this.#statusText !== undefined) {
|
|
719
|
+
this._res.writeStatus(statusLine(this.#status, this.#statusText));
|
|
703
720
|
}
|
|
704
721
|
this.writeHeaders(true);
|
|
705
722
|
}
|
|
@@ -714,7 +731,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
714
731
|
const closeConnection = this.req._connectionClose === true;
|
|
715
732
|
// 204 and 304 carry no body, so no Content-Length may describe one either; 1xx is the
|
|
716
733
|
// third case, by range
|
|
717
|
-
if (this
|
|
734
|
+
if (this.#status === 204 || this.#status === 304 || this.#status < 200) {
|
|
718
735
|
// no body and no length describing one, whatever the caller passed. node decides
|
|
719
736
|
// this the same way, from the status alone, so res.status(304).end("x") sends the
|
|
720
737
|
// status and nothing else on either.
|
|
@@ -739,7 +756,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
739
756
|
if (this.req.method === "HEAD") {
|
|
740
757
|
const length = Buffer.byteLength(data ?? "");
|
|
741
758
|
this.headers["content-length"] = String(length);
|
|
742
|
-
this._res.endWithoutBody(length
|
|
759
|
+
this._res.endWithoutBody(length, closeConnection);
|
|
743
760
|
} else {
|
|
744
761
|
// remembered rather than measured: only a caller that asks for content-length pays
|
|
745
762
|
// for it, and uWS is measuring the same bytes for the wire anyway
|
|
@@ -774,7 +791,8 @@ module.exports = class Response extends LazyWritable {
|
|
|
774
791
|
*/
|
|
775
792
|
send(body) {
|
|
776
793
|
if (this.headersSent) {
|
|
777
|
-
|
|
794
|
+
// what express's send meets first once the head is out is setHeader's refusal
|
|
795
|
+
throw headersSentError("set");
|
|
778
796
|
}
|
|
779
797
|
// a typed array is bytes to send, not an object to serialise: res.send(new Uint8Array([104,
|
|
780
798
|
// 101, 121])) is "hey" and not {"0":104,"1":101,"2":121}. Uint8Array and not every view
|
|
@@ -874,10 +892,11 @@ module.exports = class Response extends LazyWritable {
|
|
|
874
892
|
* options position is the callback.
|
|
875
893
|
*
|
|
876
894
|
* Options: `root`, `maxAge`, `lastModified`, `headers`, `dotfiles` ("allow", "deny" or
|
|
877
|
-
* "ignore"), `acceptRanges`, `cacheControl`, `immutable
|
|
895
|
+
* "ignore"), `acceptRanges`, `cacheControl`, `immutable` and `etag`.
|
|
878
896
|
*
|
|
879
897
|
* @param {string} path
|
|
880
|
-
* @param {import("./options").SendFileOptions} [options]
|
|
898
|
+
* @param {import("./options").SendFileOptions|((err?: Error) => void)} [options] or the callback in
|
|
899
|
+
* its place
|
|
881
900
|
* @param {(err?: Error) => void} [callback] called once sent, or with the error
|
|
882
901
|
*/
|
|
883
902
|
sendFile(path, options = new NullObject(), callback) {
|
|
@@ -890,7 +909,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
890
909
|
throw new TypeError("path must be a string to res.sendFile");
|
|
891
910
|
}
|
|
892
911
|
if (typeof options === "function") {
|
|
893
|
-
callback =
|
|
912
|
+
callback = options;
|
|
894
913
|
options = new NullObject();
|
|
895
914
|
}
|
|
896
915
|
if (!options) options = new NullObject();
|
|
@@ -900,7 +919,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
900
919
|
// The router's next and not the route's: express reports a file it could not serve past the
|
|
901
920
|
// rest of the route, so a four argument handler inside the route never sees it
|
|
902
921
|
const next = this.req._leaveRoute ?? this.req.next;
|
|
903
|
-
const done = /** @type {(err?:
|
|
922
|
+
const done = /** @type {(err?: NodeJS.ErrnoException) => void} */ (
|
|
904
923
|
(err) => {
|
|
905
924
|
if (callback) return callback(err);
|
|
906
925
|
if (err && err.code === "EISDIR") return next();
|
|
@@ -914,7 +933,9 @@ module.exports = class Response extends LazyWritable {
|
|
|
914
933
|
// the branch that is already a number: ms() answers undefined for a duration it cannot
|
|
915
934
|
// read, and Number.isNaN(undefined) is false, so an unreadable string reached it as NaN
|
|
916
935
|
const maxAge = Number(
|
|
917
|
-
typeof options.maxAge === "string"
|
|
936
|
+
typeof options.maxAge === "string"
|
|
937
|
+
? ms(/** @type {import("ms").StringValue} */ (options.maxAge))
|
|
938
|
+
: options.maxAge
|
|
918
939
|
);
|
|
919
940
|
options.maxAge = Number.isNaN(maxAge) ? 0 : Math.min(Math.max(0, maxAge), MAX_MAXAGE);
|
|
920
941
|
if (typeof options.lastModified === "undefined") {
|
|
@@ -1005,14 +1026,15 @@ module.exports = class Response extends LazyWritable {
|
|
|
1005
1026
|
} catch (err) {
|
|
1006
1027
|
// the fs error itself, carrying its errno and path, with send's status written on
|
|
1007
1028
|
// it: a missing file is the request's 404, an unreadable one is the server's 500
|
|
1008
|
-
return done(asStatError(/** @type {
|
|
1029
|
+
return done(asStatError(/** @type {import("./utils.js").HttpError} */ (err)));
|
|
1009
1030
|
}
|
|
1010
1031
|
if (stat.isDirectory()) {
|
|
1011
1032
|
// Express reports a directory as an EISDIR with no status, because send tells it
|
|
1012
1033
|
// apart from an error: it emits "directory", and res.sendFile has no listener for
|
|
1013
1034
|
// one. So this is not a 404, and an error handler reading err.code sees the code
|
|
1014
1035
|
// it expects. Without a callback, done() turns it into a plain next().
|
|
1015
|
-
|
|
1036
|
+
/** @type {NodeJS.ErrnoException} */
|
|
1037
|
+
const err = new Error("EISDIR, read");
|
|
1016
1038
|
err.code = "EISDIR";
|
|
1017
1039
|
return done(err);
|
|
1018
1040
|
}
|
|
@@ -1040,8 +1062,10 @@ module.exports = class Response extends LazyWritable {
|
|
|
1040
1062
|
this.setHeader(header, options.headers[header]);
|
|
1041
1063
|
}
|
|
1042
1064
|
}
|
|
1043
|
-
|
|
1044
|
-
|
|
1065
|
+
// express.static's setHeaders, which res.sendFile does not take: express ignores it here,
|
|
1066
|
+
// so it travels under a name only the middleware writes
|
|
1067
|
+
if (options._setHeaders) {
|
|
1068
|
+
options._setHeaders(this, fullpath, stat);
|
|
1045
1069
|
}
|
|
1046
1070
|
|
|
1047
1071
|
// etag, from the stat and never from the app's "etag fn". send computes this itself with
|
|
@@ -1090,7 +1114,10 @@ module.exports = class Response extends LazyWritable {
|
|
|
1090
1114
|
|
|
1091
1115
|
// range requests
|
|
1092
1116
|
if (options.acceptRanges) {
|
|
1093
|
-
|
|
1117
|
+
// only the bytes unit, and send checks the header's text for it before parsing:
|
|
1118
|
+
// "items=0-1" or "Bytes=0-1" is not a range request, and answers the whole file
|
|
1119
|
+
const rangeHeader = this.req.headers.range;
|
|
1120
|
+
if (rangeHeader !== undefined && BYTES_RANGE.test(rangeHeader)) {
|
|
1094
1121
|
// the branch above established the header is there, so range() cannot answer
|
|
1095
1122
|
// the undefined it uses to mean "no Range header"
|
|
1096
1123
|
let ranges = /** @type {ReturnType<typeof import("range-parser")>} */ (
|
|
@@ -1149,7 +1176,8 @@ module.exports = class Response extends LazyWritable {
|
|
|
1149
1176
|
// ECONNABORTED to a callback and never to next(), so aborts stay out of
|
|
1150
1177
|
// the error middleware.
|
|
1151
1178
|
if (this.aborted && callback) {
|
|
1152
|
-
|
|
1179
|
+
/** @type {NodeJS.ErrnoException} */
|
|
1180
|
+
const err = new Error("Request aborted");
|
|
1153
1181
|
err.code = "ECONNABORTED";
|
|
1154
1182
|
callback(err);
|
|
1155
1183
|
}
|
|
@@ -1267,7 +1295,8 @@ module.exports = class Response extends LazyWritable {
|
|
|
1267
1295
|
* what a media type is. res.set does that, and is what Express code should use.
|
|
1268
1296
|
*
|
|
1269
1297
|
* @param {string} field
|
|
1270
|
-
* @param {
|
|
1298
|
+
* @param {number|string|readonly string[]|undefined} value an array sends the header once per
|
|
1299
|
+
* entry; undefined is refused, as node refuses it
|
|
1271
1300
|
* @returns {this}
|
|
1272
1301
|
* @throws {Error} once the headers have gone out
|
|
1273
1302
|
* @throws {TypeError} if the name is not a token, the value is undefined, or the value holds a
|
|
@@ -1275,7 +1304,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
1275
1304
|
*/
|
|
1276
1305
|
setHeader(field, value) {
|
|
1277
1306
|
if (this.headersSent) {
|
|
1278
|
-
throw
|
|
1307
|
+
throw headersSentError("set");
|
|
1279
1308
|
}
|
|
1280
1309
|
// one Map hit for a name already validated and lowercased: middleware writes the same
|
|
1281
1310
|
// constant names on every request. Insert-only after validation, so no bad name can enter
|
|
@@ -1338,15 +1367,17 @@ module.exports = class Response extends LazyWritable {
|
|
|
1338
1367
|
* @returns {void}
|
|
1339
1368
|
*/
|
|
1340
1369
|
flushHeaders() {
|
|
1341
|
-
if (this
|
|
1370
|
+
if (this.#headOut || this.finished || this.aborted) {
|
|
1342
1371
|
return;
|
|
1343
1372
|
}
|
|
1344
1373
|
this._res.cork(() => {
|
|
1345
|
-
|
|
1374
|
+
if (!this.headersSent) {
|
|
1375
|
+
this.writeHead(this.statusCode);
|
|
1376
|
+
}
|
|
1346
1377
|
// the same rule the chunked write path follows: uWS emits the 200 head itself, byte for
|
|
1347
1378
|
// byte, so writing it again would only cost a crossing
|
|
1348
|
-
if (this
|
|
1349
|
-
this._res.writeStatus(statusLine(this
|
|
1379
|
+
if (this.#status !== 200 || this.#statusText !== undefined) {
|
|
1380
|
+
this._res.writeStatus(statusLine(this.#status, this.#statusText));
|
|
1350
1381
|
}
|
|
1351
1382
|
// true, as the chunked path passes for a string chunk: what follows a flush is a body
|
|
1352
1383
|
// written in pieces, and the framing has to be the one that allows them
|
|
@@ -1403,10 +1434,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
1403
1434
|
*/
|
|
1404
1435
|
#refuseInformationAfterHead() {
|
|
1405
1436
|
if (this.headersSent) {
|
|
1406
|
-
|
|
1407
|
-
const err = new Error("Cannot write headers after they are sent to the client");
|
|
1408
|
-
err.code = "ERR_HTTP_HEADERS_SENT";
|
|
1409
|
-
throw err;
|
|
1437
|
+
throw headersSentError("write");
|
|
1410
1438
|
}
|
|
1411
1439
|
}
|
|
1412
1440
|
|
|
@@ -1439,7 +1467,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
1439
1467
|
* node's `assignSocket`, which the http server uses when a response is
|
|
1440
1468
|
* handed a raw socket. There is no such socket here.
|
|
1441
1469
|
*
|
|
1442
|
-
* @param {
|
|
1470
|
+
* @param {import("net").Socket} [socket] node takes one here and there is none to take
|
|
1443
1471
|
* @returns {void}
|
|
1444
1472
|
*/
|
|
1445
1473
|
assignSocket(socket) {}
|
|
@@ -1447,7 +1475,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
1447
1475
|
/**
|
|
1448
1476
|
* node's `detachSocket`, which the http server uses when a response is
|
|
1449
1477
|
* handed a raw socket. There is no such socket here.
|
|
1450
|
-
* @param {
|
|
1478
|
+
* @param {import("net").Socket} [socket] node takes one here and there is none to take
|
|
1451
1479
|
* @returns {void}
|
|
1452
1480
|
*/
|
|
1453
1481
|
detachSocket(socket) {}
|
|
@@ -1508,10 +1536,10 @@ module.exports = class Response extends LazyWritable {
|
|
|
1508
1536
|
const key = name.toLowerCase();
|
|
1509
1537
|
const current = this.headers[key];
|
|
1510
1538
|
if (current === undefined) {
|
|
1511
|
-
return this.setHeader(name,
|
|
1539
|
+
return this.setHeader(name, value);
|
|
1512
1540
|
}
|
|
1513
|
-
const merged =
|
|
1514
|
-
return this.setHeader(name,
|
|
1541
|
+
const merged = /** @type {string[]} */ ([]).concat(current, value);
|
|
1542
|
+
return this.setHeader(name, merged);
|
|
1515
1543
|
}
|
|
1516
1544
|
|
|
1517
1545
|
/**
|
|
@@ -1526,15 +1554,15 @@ module.exports = class Response extends LazyWritable {
|
|
|
1526
1554
|
if (typeof Headers === "function" && headers instanceof Headers) {
|
|
1527
1555
|
for (const name of new Set([...headers.keys()])) {
|
|
1528
1556
|
if (name === "set-cookie") {
|
|
1529
|
-
this.setHeader(name,
|
|
1557
|
+
this.setHeader(name, headers.getSetCookie());
|
|
1530
1558
|
} else {
|
|
1531
|
-
this.setHeader(name, /** @type {
|
|
1559
|
+
this.setHeader(name, /** @type {string} */ (headers.get(name)));
|
|
1532
1560
|
}
|
|
1533
1561
|
}
|
|
1534
1562
|
return this;
|
|
1535
1563
|
}
|
|
1536
1564
|
for (const [name, value] of headers) {
|
|
1537
|
-
this.setHeader(name,
|
|
1565
|
+
this.setHeader(name, value);
|
|
1538
1566
|
}
|
|
1539
1567
|
return this;
|
|
1540
1568
|
}
|
|
@@ -1551,8 +1579,8 @@ module.exports = class Response extends LazyWritable {
|
|
|
1551
1579
|
|
|
1552
1580
|
/**
|
|
1553
1581
|
* The Express name for set(), including the charset it adds to a content-type.
|
|
1554
|
-
* @param {
|
|
1555
|
-
* @param {
|
|
1582
|
+
* @param {string|object} field a header name, or an object of them
|
|
1583
|
+
* @param {string|string[]} [value] the header value, or nothing when the first argument is an object
|
|
1556
1584
|
* @returns {this}
|
|
1557
1585
|
*/
|
|
1558
1586
|
header(field, value) {
|
|
@@ -1644,6 +1672,9 @@ module.exports = class Response extends LazyWritable {
|
|
|
1644
1672
|
* @param {string} field
|
|
1645
1673
|
*/
|
|
1646
1674
|
removeHeader(field) {
|
|
1675
|
+
if (this.headersSent) {
|
|
1676
|
+
throw headersSentError("remove");
|
|
1677
|
+
}
|
|
1647
1678
|
const key = field.toLowerCase();
|
|
1648
1679
|
// the delete is a runtime call, and helmet removes a header most responses never carry
|
|
1649
1680
|
if (key in this.headers) {
|
|
@@ -1677,12 +1708,13 @@ module.exports = class Response extends LazyWritable {
|
|
|
1677
1708
|
* Renders a view and sends it. With a callback the result goes to the callback instead, and
|
|
1678
1709
|
* nothing is sent. A function in the options position is taken as the callback.
|
|
1679
1710
|
* @param {string} view view name
|
|
1680
|
-
* @param {Record<string, any
|
|
1711
|
+
* @param {Record<string, any>|((err: Error|null, html?: string) => void)} [options] locals for the
|
|
1712
|
+
* view, or the callback in its place
|
|
1681
1713
|
* @param {(err: Error|null, html?: string) => void} [callback]
|
|
1682
1714
|
*/
|
|
1683
1715
|
render(view, options, callback) {
|
|
1684
1716
|
if (typeof options === "function") {
|
|
1685
|
-
callback = /** @type {
|
|
1717
|
+
callback = /** @type {(err: Error|null, html?: string) => void} */ (options);
|
|
1686
1718
|
options = {};
|
|
1687
1719
|
}
|
|
1688
1720
|
if (!options) {
|
|
@@ -1720,7 +1752,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
1720
1752
|
const opt = { ...(options ?? {}) }; // create a new ref because we change original object (https://github.com/dimdenGD/ultimate-express/issues/68)
|
|
1721
1753
|
// cookie-parser hangs the secret on the request, so it is read off it rather than
|
|
1722
1754
|
// declared here: without that middleware there is none, which is what this checks
|
|
1723
|
-
const req = /** @type {
|
|
1755
|
+
const req = /** @type {{secret?: string}} */ (this.req);
|
|
1724
1756
|
if (opt.signed && !req.secret) {
|
|
1725
1757
|
// the message has to read like this: it is the one Express throws, and it names the
|
|
1726
1758
|
// thing that is actually missing rather than the library that noticed
|
|
@@ -1739,7 +1771,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
1739
1771
|
delete opt.maxAge;
|
|
1740
1772
|
}
|
|
1741
1773
|
if (opt.signed) {
|
|
1742
|
-
val = "s:" + sign(val, req.secret);
|
|
1774
|
+
val = "s:" + sign(val, /** @type {string} */ (req.secret));
|
|
1743
1775
|
}
|
|
1744
1776
|
|
|
1745
1777
|
if (opt.path == null) {
|
|
@@ -1784,7 +1816,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
1784
1816
|
* Answers according to the Accept header, calling the handler whose key matches best. A
|
|
1785
1817
|
* `default` key catches everything else; without one an unmatched request gets 406.
|
|
1786
1818
|
* Sets Vary: Accept.
|
|
1787
|
-
* @param {Record<string,
|
|
1819
|
+
* @param {Record<string, Function>} object handlers keyed by extension or mime type
|
|
1788
1820
|
* @returns {this}
|
|
1789
1821
|
*/
|
|
1790
1822
|
format(object) {
|
|
@@ -1822,11 +1854,14 @@ module.exports = class Response extends LazyWritable {
|
|
|
1822
1854
|
* @returns {this}
|
|
1823
1855
|
*/
|
|
1824
1856
|
json(body) {
|
|
1857
|
+
const hot = this.app._hot();
|
|
1858
|
+
// serialised before the type is set, as express orders it: a body JSON.stringify refuses,
|
|
1859
|
+
// a BigInt for one, throws out of here with the response's headers as they were
|
|
1860
|
+
const json = stringify(body, hot.jsonReplacer, hot.jsonSpaces, hot.jsonEscape);
|
|
1825
1861
|
if (!this.headers["content-type"]) {
|
|
1826
1862
|
this.headers["content-type"] = JSON_UTF8;
|
|
1827
1863
|
}
|
|
1828
|
-
|
|
1829
|
-
return this.send(stringify(body, hot.jsonReplacer, hot.jsonSpaces, hot.jsonEscape));
|
|
1864
|
+
return this.send(json);
|
|
1830
1865
|
}
|
|
1831
1866
|
|
|
1832
1867
|
/**
|
|
@@ -1878,7 +1913,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
1878
1913
|
|
|
1879
1914
|
/**
|
|
1880
1915
|
* Adds to the Link header, one entry per key, the key being the rel.
|
|
1881
|
-
* @param {Record<string,
|
|
1916
|
+
* @param {Record<string, string>} links rel to url
|
|
1882
1917
|
* @returns {this}
|
|
1883
1918
|
*/
|
|
1884
1919
|
links(links) {
|
|
@@ -2001,7 +2036,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
2001
2036
|
vary(field) {
|
|
2002
2037
|
// the vary package decides: it throws when there is no field at all, and does nothing at
|
|
2003
2038
|
// all for an empty list, which is not the same thing and used to be refused here as well
|
|
2004
|
-
vary(/** @type {
|
|
2039
|
+
vary(/** @type {import("http").ServerResponse} */ (/** @type {unknown} */ (this)), field);
|
|
2005
2040
|
return this;
|
|
2006
2041
|
}
|
|
2007
2042
|
|
|
@@ -2036,4 +2071,5 @@ module.exports = class Response extends LazyWritable {
|
|
|
2036
2071
|
|
|
2037
2072
|
// res.contentType is res.type under express's other name. On the prototype rather than an instance
|
|
2038
2073
|
// field, which wrote one own property per response in the constructor.
|
|
2039
|
-
/** @type {
|
|
2074
|
+
/** @type {{contentType?: typeof module.exports.prototype.type}} */ (module.exports.prototype).contentType =
|
|
2075
|
+
module.exports.prototype.type;
|
package/src/route.js
CHANGED
|
@@ -70,7 +70,7 @@ class Route {
|
|
|
70
70
|
*
|
|
71
71
|
* @param {any} req a Request, or whatever a caller that built this Route by hand is serving
|
|
72
72
|
* @param {any} res the matching response
|
|
73
|
-
* @param {(err?:
|
|
73
|
+
* @param {(err?: unknown) => void} done called when the route is finished with the request, with
|
|
74
74
|
* whatever error it ended on
|
|
75
75
|
*/
|
|
76
76
|
dispatch(req, res, done) {
|
|
@@ -113,7 +113,7 @@ class Route {
|
|
|
113
113
|
return done(err);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
const handle = /** @type {
|
|
116
|
+
const handle = /** @type {{handle: Function}} */ (layer).handle;
|
|
117
117
|
// an error only reaches the four-argument handlers, and everything else only runs
|
|
118
118
|
// while there is no error, which is the same rule ordinary dispatch follows
|
|
119
119
|
if (err) {
|
|
@@ -146,7 +146,7 @@ class Route {
|
|
|
146
146
|
* Refuses a handler that could never be called, worded as express words it.
|
|
147
147
|
*
|
|
148
148
|
* @param {string} method the verb this was registered for, for the message
|
|
149
|
-
* @param {
|
|
149
|
+
* @param {unknown[]} handlers
|
|
150
150
|
*/
|
|
151
151
|
function checkRouteHandlers(method, handlers) {
|
|
152
152
|
for (const handle of handlers) {
|