fulmine.js 5.0.0 → 5.1.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 +26 -8
- package/package.json +1 -1
- package/src/application.js +114 -20
- package/src/cli.js +3 -3
- package/src/index.js +5 -0
- package/src/middlewares.js +87 -36
- package/src/node-shim.js +2 -1
- package/src/request.js +102 -42
- package/src/response.js +204 -114
- package/src/route.js +180 -0
- package/src/router.js +507 -124
- package/src/utils.js +41 -3
package/src/response.js
CHANGED
|
@@ -103,6 +103,22 @@ class Socket extends EventEmitter {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
// One status line per code, built on first use: the default path, with no custom reason phrase,
|
|
107
|
+
// paid a template string and a trim per request for a line that never changes. Bounded to real
|
|
108
|
+
// HTTP codes so a wild writeHead value cannot grow the array or flip it into dictionary mode.
|
|
109
|
+
const STATUS_LINES = [];
|
|
110
|
+
/**
|
|
111
|
+
* @param {number} code
|
|
112
|
+
* @param {string|undefined} text an explicit reason phrase, which bypasses the cache
|
|
113
|
+
* @returns {string} the uWS status line, e.g. "200 OK"
|
|
114
|
+
*/
|
|
115
|
+
function statusLine(code, text) {
|
|
116
|
+
if (text === undefined && Number.isInteger(code) && code >= 100 && code <= 999) {
|
|
117
|
+
return STATUS_LINES[code] ?? (STATUS_LINES[code] = code + " " + (statuses.message[code] ?? "unknown"));
|
|
118
|
+
}
|
|
119
|
+
return `${code} ${text ?? statuses.message[code] ?? "unknown"}`.trim();
|
|
120
|
+
}
|
|
121
|
+
|
|
106
122
|
module.exports = class Response extends Writable {
|
|
107
123
|
/** @type {Socket|null} */
|
|
108
124
|
#socket = null;
|
|
@@ -117,9 +133,6 @@ module.exports = class Response extends Writable {
|
|
|
117
133
|
|
|
118
134
|
req;
|
|
119
135
|
|
|
120
|
-
/** @type {Set<any>|undefined} the app's live-response set, see Application#handleRequest */
|
|
121
|
-
_pendingIn;
|
|
122
|
-
|
|
123
136
|
/**
|
|
124
137
|
* Built for every request, right after the Request it belongs to. The headers start with the
|
|
125
138
|
* two that describe the connection, since every response carries them, and x-powered-by only
|
|
@@ -162,23 +175,38 @@ module.exports = class Response extends Writable {
|
|
|
162
175
|
}
|
|
163
176
|
|
|
164
177
|
this.body = undefined;
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
178
|
+
// false while the uWS route handler is still in its synchronous window, where uWS holds
|
|
179
|
+
// the socket corked itself; the two uWS entry points flip it once that window closes
|
|
180
|
+
this._corkNeeded = false;
|
|
181
|
+
// shared methods, not arrows: two closures and a once() wrapper here were four
|
|
182
|
+
// allocations per request. EventEmitter calls listeners with this = the emitter.
|
|
183
|
+
this.on("error", this._onAbortError);
|
|
184
|
+
this.on("close", this._onCloseCleanup);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** @param {Error} err */
|
|
188
|
+
_onAbortError(err) {
|
|
189
|
+
if (this.finished) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
this._res.cork(() => {
|
|
193
|
+
this._res.close();
|
|
194
|
+
this.finished = true;
|
|
195
|
+
this.#socket?.emit("close");
|
|
179
196
|
});
|
|
180
197
|
}
|
|
181
198
|
|
|
199
|
+
/**
|
|
200
|
+
* on(), not once(), so this must stay idempotent: end() emits 'close' by hand and a later
|
|
201
|
+
* destroy() makes Writable emit it again.
|
|
202
|
+
*/
|
|
203
|
+
_onCloseCleanup() {
|
|
204
|
+
this.#ended = true;
|
|
205
|
+
// the application's graceful close() waits on this set, which lives on the per-app
|
|
206
|
+
// response prototype layer, see the Application constructor
|
|
207
|
+
/** @type {any} */ (this)._pendingIn?.delete(this);
|
|
208
|
+
}
|
|
209
|
+
|
|
182
210
|
/**
|
|
183
211
|
* Where node keeps the outgoing headers of an OutgoingMessage. Only code going through node's
|
|
184
212
|
* own header path ever looks, cookie-session being the one in this project's tests, so the
|
|
@@ -247,9 +275,11 @@ module.exports = class Response extends Writable {
|
|
|
247
275
|
if (!this.headersSent) {
|
|
248
276
|
this.writeHead(this.statusCode);
|
|
249
277
|
// "unknown" and not the bare number: node writes that reason phrase for a code it
|
|
250
|
-
// has no message for, so the raw status lines match
|
|
251
|
-
|
|
252
|
-
|
|
278
|
+
// has no message for, so the raw status lines match. The default 200 with no
|
|
279
|
+
// phrase is uWS's own head, byte for byte, so it is not written at all
|
|
280
|
+
if (this.statusCode !== 200 || this.statusText !== undefined) {
|
|
281
|
+
this._res.writeStatus(statusLine(this.statusCode, this.statusText));
|
|
282
|
+
}
|
|
253
283
|
this.writeHeaders(typeof chunk === "string");
|
|
254
284
|
}
|
|
255
285
|
|
|
@@ -353,13 +383,18 @@ module.exports = class Response extends Writable {
|
|
|
353
383
|
// the connection is closing. That happens both when the client asked and when something
|
|
354
384
|
// else set the header on the way out, which is what a proxy passing an upstream response
|
|
355
385
|
// through does.
|
|
356
|
-
const
|
|
357
|
-
const
|
|
358
|
-
|
|
386
|
+
const headers = this.headers;
|
|
387
|
+
const res = this._res;
|
|
388
|
+
const connection = headers["connection"];
|
|
389
|
+
// length before lowercasing: no string of another length can lowercase to "close", and
|
|
390
|
+
// the value here is nearly always the 10-char "keep-alive", which paid a scan per response
|
|
391
|
+
const closing =
|
|
392
|
+
typeof connection === "string" && connection.length === 5 && connection.toLowerCase() === "close";
|
|
393
|
+
for (const header in headers) {
|
|
359
394
|
if (closing && header === "keep-alive") {
|
|
360
395
|
continue;
|
|
361
396
|
}
|
|
362
|
-
const value =
|
|
397
|
+
const value = headers[header];
|
|
363
398
|
if (header === "content-length") {
|
|
364
399
|
// if content-length is set, disable chunked transfer encoding, since size is known
|
|
365
400
|
this.chunkedTransfer = false;
|
|
@@ -368,10 +403,10 @@ module.exports = class Response extends Writable {
|
|
|
368
403
|
}
|
|
369
404
|
if (Array.isArray(value)) {
|
|
370
405
|
for (const val of value) {
|
|
371
|
-
|
|
406
|
+
res.writeHeader(header, val);
|
|
372
407
|
}
|
|
373
408
|
} else {
|
|
374
|
-
|
|
409
|
+
res.writeHeader(header, value);
|
|
375
410
|
}
|
|
376
411
|
}
|
|
377
412
|
this.headersSent = true;
|
|
@@ -446,48 +481,67 @@ module.exports = class Response extends Writable {
|
|
|
446
481
|
return this;
|
|
447
482
|
}
|
|
448
483
|
this.writeHead(this.statusCode);
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
484
|
+
// uWS holds the socket corked for the synchronous window of its route handler, and
|
|
485
|
+
// cork inside cork is a passthrough: the wrapper and its closure are only paid once the
|
|
486
|
+
// answer has outlived that window, which is what _corkNeeded records
|
|
487
|
+
if (this._corkNeeded) {
|
|
488
|
+
this._res.cork(() => this._finish(data, cb));
|
|
489
|
+
} else {
|
|
490
|
+
this._finish(data, cb);
|
|
491
|
+
}
|
|
492
|
+
return this;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* The corked tail of end(): status, headers, body and the finish events. Split out so a
|
|
497
|
+
* synchronous answer calls it straight, already inside uWS's own cork.
|
|
498
|
+
*
|
|
499
|
+
* @param {any} data
|
|
500
|
+
* @param {any} cb
|
|
501
|
+
*/
|
|
502
|
+
_finish(data, cb) {
|
|
503
|
+
if (!this.headersSent) {
|
|
504
|
+
// freshness is not decided here. node's end() knows nothing about conditional
|
|
505
|
+
// requests, and Express answers 304 from send() and from sendFile(), each of
|
|
506
|
+
// which strips the entity headers first. Deciding it here meant res.end("body")
|
|
507
|
+
// answered 304 and dropped the body that the caller had just written.
|
|
508
|
+
// "unknown" for a code without a message, as node's status line has it.
|
|
509
|
+
// The default 200 with no phrase is not written at all: uWS emits the identical
|
|
510
|
+
// "HTTP/1.1 200 OK" head on its own, and the crossing costs more than it says
|
|
511
|
+
if (this.statusCode !== 200 || this.statusText !== undefined) {
|
|
512
|
+
this._res.writeStatus(statusLine(this.statusCode, this.statusText));
|
|
459
513
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
514
|
+
this.writeHeaders(true);
|
|
515
|
+
}
|
|
516
|
+
const contentLength = this.headers["content-length"];
|
|
517
|
+
if (STATUSES_WITHOUT_BODY.has(this.statusCode) || this.statusCode < 200) {
|
|
518
|
+
// no body and no length describing one, whatever the caller passed. node decides
|
|
519
|
+
// this the same way, from the status alone, so res.status(304).end("x") sends the
|
|
520
|
+
// status and nothing else on either.
|
|
521
|
+
this._res.endWithoutBody();
|
|
522
|
+
} else if (!data && contentLength) {
|
|
523
|
+
this._res.endWithoutBody(contentLength.toString());
|
|
524
|
+
} else {
|
|
525
|
+
if (data instanceof Buffer) {
|
|
526
|
+
data = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
527
|
+
}
|
|
528
|
+
if (this.req.method === "HEAD") {
|
|
529
|
+
const length = Buffer.byteLength(data ?? "");
|
|
530
|
+
this._res.endWithoutBody(length.toString());
|
|
468
531
|
} else {
|
|
469
|
-
|
|
470
|
-
data = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
471
|
-
}
|
|
472
|
-
if (this.req.method === "HEAD") {
|
|
473
|
-
const length = Buffer.byteLength(data ?? "");
|
|
474
|
-
this._res.endWithoutBody(length.toString());
|
|
475
|
-
} else {
|
|
476
|
-
this._res.end(data);
|
|
477
|
-
}
|
|
532
|
+
this._res.end(data);
|
|
478
533
|
}
|
|
534
|
+
}
|
|
479
535
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
});
|
|
490
|
-
return this;
|
|
536
|
+
this.finished = true;
|
|
537
|
+
this.#socket?.emit("close");
|
|
538
|
+
this.emit("finish");
|
|
539
|
+
this.emit("close");
|
|
540
|
+
cb &&
|
|
541
|
+
queueMicrotask(() => {
|
|
542
|
+
this.#ended = true;
|
|
543
|
+
cb();
|
|
544
|
+
});
|
|
491
545
|
}
|
|
492
546
|
|
|
493
547
|
/**
|
|
@@ -616,7 +670,17 @@ module.exports = class Response extends Writable {
|
|
|
616
670
|
if (!options) options = new NullObject();
|
|
617
671
|
// the callback is optional: without one, errors go to next(). The router assigns req.next
|
|
618
672
|
// before any handler can run, so by the time sendFile is reachable it is always there.
|
|
619
|
-
|
|
673
|
+
// Express's completion handler, exactly: a callback hears everything and the response is
|
|
674
|
+
// left alone, so it can still answer 200 after a 404 error. Without one, a directory
|
|
675
|
+
// falls through as a plain next(), and aborts and write errors go nowhere.
|
|
676
|
+
const next = this.req.next;
|
|
677
|
+
const done = /** @type {(err?: any) => void} */ (
|
|
678
|
+
(err) => {
|
|
679
|
+
if (callback) return callback(err);
|
|
680
|
+
if (err && err.code === "EISDIR") return next();
|
|
681
|
+
if (err && err.code !== "ECONNABORTED" && err.syscall !== "write") next(err);
|
|
682
|
+
}
|
|
683
|
+
);
|
|
620
684
|
// default options
|
|
621
685
|
// Normalised the way send does, and it is not fussiness: max-age takes a non-negative
|
|
622
686
|
// integer count of seconds, so 0.5, -1 and Infinity are all invalid, and a client that
|
|
@@ -661,12 +725,10 @@ module.exports = class Response extends Writable {
|
|
|
661
725
|
// it can go back into path
|
|
662
726
|
const decoded = decode(path);
|
|
663
727
|
if (decoded === -1) {
|
|
664
|
-
this.status(400);
|
|
665
728
|
return done(httpError(400));
|
|
666
729
|
}
|
|
667
730
|
path = decoded;
|
|
668
731
|
if (~path.indexOf("\0")) {
|
|
669
|
-
this.status(400);
|
|
670
732
|
return done(httpError(400));
|
|
671
733
|
}
|
|
672
734
|
// send's two branches: with a root the path is normalized first, so an in-root ".." like
|
|
@@ -676,18 +738,15 @@ module.exports = class Response extends Writable {
|
|
|
676
738
|
if (options.root) {
|
|
677
739
|
path = Path.normalize("." + Path.sep + path);
|
|
678
740
|
if (UP_PATH_REGEXP.test(path)) {
|
|
679
|
-
this.status(403);
|
|
680
741
|
return done(httpError(403));
|
|
681
742
|
}
|
|
682
743
|
parts = path.split(Path.sep);
|
|
683
744
|
fullpath = Path.resolve(Path.join(options.root, path));
|
|
684
745
|
if (!fullpath.startsWith(Path.resolve(options.root))) {
|
|
685
|
-
this.status(403);
|
|
686
746
|
return done(httpError(403));
|
|
687
747
|
}
|
|
688
748
|
} else {
|
|
689
749
|
if (UP_PATH_REGEXP.test(path)) {
|
|
690
|
-
this.status(403);
|
|
691
750
|
return done(httpError(403));
|
|
692
751
|
}
|
|
693
752
|
parts = Path.normalize(path).split(Path.sep);
|
|
@@ -700,21 +759,18 @@ module.exports = class Response extends Writable {
|
|
|
700
759
|
case "allow":
|
|
701
760
|
break;
|
|
702
761
|
case "deny":
|
|
703
|
-
this.status(403);
|
|
704
762
|
return done(httpError(403));
|
|
705
763
|
case "ignore_files": {
|
|
706
764
|
// the file segment alone: with a root the normalized parts no longer carry a
|
|
707
765
|
// leading empty segment, so a bare dotfile can be the only part
|
|
708
766
|
const len = parts.length;
|
|
709
767
|
if (parts[len - 1].startsWith(".")) {
|
|
710
|
-
this.status(404);
|
|
711
768
|
return done(httpError(404));
|
|
712
769
|
}
|
|
713
770
|
break;
|
|
714
771
|
}
|
|
715
772
|
case "ignore":
|
|
716
773
|
default:
|
|
717
|
-
this.status(404);
|
|
718
774
|
return done(httpError(404));
|
|
719
775
|
}
|
|
720
776
|
}
|
|
@@ -732,8 +788,7 @@ module.exports = class Response extends Writable {
|
|
|
732
788
|
// Express reports a directory as an EISDIR with no status, because send tells it
|
|
733
789
|
// apart from an error: it emits "directory", and res.sendFile has no listener for
|
|
734
790
|
// one. So this is not a 404, and an error handler reading err.code sees the code
|
|
735
|
-
// it expects.
|
|
736
|
-
this.status(404);
|
|
791
|
+
// it expects. Without a callback, done() turns it into a plain next().
|
|
737
792
|
const err = /** @type {any} */ (new Error("EISDIR, read"));
|
|
738
793
|
err.code = "EISDIR";
|
|
739
794
|
return done(err);
|
|
@@ -785,17 +840,38 @@ module.exports = class Response extends Writable {
|
|
|
785
840
|
|
|
786
841
|
// conditional requests
|
|
787
842
|
if (isPreconditionFailure(this.req, this)) {
|
|
788
|
-
this.status(412);
|
|
789
843
|
return done(httpError(412));
|
|
790
844
|
}
|
|
791
845
|
|
|
846
|
+
// if-modified-since, if-none-match. Before range handling, as send orders it: a fresh
|
|
847
|
+
// request with an unsatisfiable Range gets the 304, never the 416.
|
|
848
|
+
if (this.req.fresh) {
|
|
849
|
+
// the same fields send removes: everything describing a body that is not being sent.
|
|
850
|
+
delete this.headers["content-type"];
|
|
851
|
+
delete this.headers["content-encoding"];
|
|
852
|
+
delete this.headers["content-language"];
|
|
853
|
+
delete this.headers["content-length"];
|
|
854
|
+
this.status(304);
|
|
855
|
+
this.end();
|
|
856
|
+
// the response is complete, so a callback hears about it. Never done(): on success
|
|
857
|
+
// that would be next(), and the request would fall through to the next handler.
|
|
858
|
+
if (callback) callback();
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// the start and end options, before the Range header: send serves ranges relative to the
|
|
863
|
+
// window they select, so both the parse and the Content-Range total use the windowed len
|
|
864
|
+
let offset = options.start || 0;
|
|
865
|
+
let len = Math.max(0, stat.size - offset);
|
|
866
|
+
if (options.end !== undefined) {
|
|
867
|
+
const bytes = options.end - offset + 1;
|
|
868
|
+
if (len > bytes) len = bytes;
|
|
869
|
+
}
|
|
870
|
+
|
|
792
871
|
// range requests
|
|
793
|
-
let offset = 0,
|
|
794
|
-
len = stat.size,
|
|
795
|
-
ranged = false;
|
|
796
872
|
if (options.acceptRanges) {
|
|
797
873
|
if (this.req.headers.range) {
|
|
798
|
-
let ranges = this.req.range(
|
|
874
|
+
let ranges = this.req.range(len, {
|
|
799
875
|
combine: true
|
|
800
876
|
});
|
|
801
877
|
|
|
@@ -805,44 +881,36 @@ module.exports = class Response extends Writable {
|
|
|
805
881
|
}
|
|
806
882
|
|
|
807
883
|
if (ranges === -1) {
|
|
808
|
-
|
|
809
|
-
|
|
884
|
+
// the header goes on the response itself, as send writes it before raising
|
|
885
|
+
// the error, and the status stays on the error for the handler to apply
|
|
886
|
+
this.headers["content-range"] = `bytes */${len}`;
|
|
810
887
|
return done(httpError(416));
|
|
811
888
|
}
|
|
812
889
|
if (ranges !== -2 && ranges.length === 1) {
|
|
813
890
|
this.status(206);
|
|
814
891
|
const range = ranges[0];
|
|
815
|
-
this.headers["content-range"] = `bytes ${range.start}-${range.end}/${
|
|
816
|
-
offset
|
|
892
|
+
this.headers["content-range"] = `bytes ${range.start}-${range.end}/${len}`;
|
|
893
|
+
offset += range.start;
|
|
817
894
|
len = range.end - range.start + 1;
|
|
818
|
-
ranged = true;
|
|
819
895
|
}
|
|
820
896
|
}
|
|
821
897
|
}
|
|
822
898
|
|
|
823
|
-
//
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
// Content-Range goes too, since a 304 answers the whole conditional request and not
|
|
827
|
-
// the range that was asked for.
|
|
828
|
-
delete this.headers["content-type"];
|
|
829
|
-
delete this.headers["content-encoding"];
|
|
830
|
-
delete this.headers["content-language"];
|
|
831
|
-
delete this.headers["content-length"];
|
|
832
|
-
delete this.headers["content-range"];
|
|
833
|
-
this.status(304);
|
|
834
|
-
return this.end();
|
|
835
|
-
}
|
|
899
|
+
// anything but the whole file, whether from a Range header or the start/end options,
|
|
900
|
+
// has to go through the read stream with explicit bounds
|
|
901
|
+
const partial = offset > 0 || len < stat.size;
|
|
836
902
|
|
|
837
903
|
if (this.req.method === "HEAD") {
|
|
838
904
|
// len, not stat.size: a ranged HEAD answers with the length of the selected part,
|
|
839
905
|
// as send sets it before ending
|
|
840
|
-
this.set("Content-Length", len);
|
|
841
|
-
|
|
906
|
+
this.set("Content-Length", String(len));
|
|
907
|
+
this.end();
|
|
908
|
+
if (callback) callback();
|
|
909
|
+
return;
|
|
842
910
|
}
|
|
843
911
|
|
|
844
912
|
// serve smaller files using workers
|
|
845
|
-
if (this.app.workers.length && stat.size < 768 * 1024 && !
|
|
913
|
+
if (this.app.workers.length && stat.size < 768 * 1024 && !partial) {
|
|
846
914
|
this.app
|
|
847
915
|
.readFileWithWorker(fullpath)
|
|
848
916
|
.then((data) => {
|
|
@@ -872,12 +940,12 @@ module.exports = class Response extends Writable {
|
|
|
872
940
|
const opts = {
|
|
873
941
|
highWaterMark: HIGH_WATERMARK
|
|
874
942
|
};
|
|
875
|
-
if (
|
|
943
|
+
if (partial) {
|
|
876
944
|
opts.start = offset;
|
|
877
945
|
opts.end = Math.max(offset, offset + len - 1);
|
|
878
946
|
}
|
|
879
947
|
const file = fs.createReadStream(fullpath, opts);
|
|
880
|
-
this.set("Content-Length", len);
|
|
948
|
+
this.set("Content-Length", String(len));
|
|
881
949
|
// pipe() forwards nothing from the source, so a read error after the stat, the file
|
|
882
950
|
// gone or unreadable, must reach next()/the callback instead of crashing the process
|
|
883
951
|
file.on("error", (err) => {
|
|
@@ -896,6 +964,11 @@ module.exports = class Response extends Writable {
|
|
|
896
964
|
this.removeListener("close", cleanup);
|
|
897
965
|
socket?.removeListener("close", cleanup);
|
|
898
966
|
});
|
|
967
|
+
// "end" only fires on a full read, and never together with "error", so the callback
|
|
968
|
+
// hears about completion exactly once, as it does on the worker path
|
|
969
|
+
if (callback) {
|
|
970
|
+
file.once("end", () => callback());
|
|
971
|
+
}
|
|
899
972
|
file.pipe(this);
|
|
900
973
|
}
|
|
901
974
|
}
|
|
@@ -915,16 +988,16 @@ module.exports = class Response extends Writable {
|
|
|
915
988
|
let done = callback;
|
|
916
989
|
/** @type {string|null|undefined} */
|
|
917
990
|
let name = filename;
|
|
918
|
-
let opts = options ||
|
|
991
|
+
let opts = options || null;
|
|
919
992
|
|
|
920
993
|
// support function as second or third arg
|
|
921
994
|
if (typeof filename === "function") {
|
|
922
995
|
done = /** @type {any} */ (filename);
|
|
923
996
|
name = null;
|
|
924
|
-
opts =
|
|
997
|
+
opts = null;
|
|
925
998
|
} else if (typeof options === "function") {
|
|
926
999
|
done = /** @type {any} */ (options);
|
|
927
|
-
opts =
|
|
1000
|
+
opts = null;
|
|
928
1001
|
}
|
|
929
1002
|
|
|
930
1003
|
// support optional filename, where options may be in it's place
|
|
@@ -932,15 +1005,31 @@ module.exports = class Response extends Writable {
|
|
|
932
1005
|
name = null;
|
|
933
1006
|
opts = filename;
|
|
934
1007
|
}
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1008
|
+
|
|
1009
|
+
// Handed to sendFile as a header option rather than set here, as Express does: headers
|
|
1010
|
+
// from the options only go out once the stat has succeeded, so a 404 or a 403 carries no
|
|
1011
|
+
// Content-Disposition. The Content-Type still comes from the file's own extension.
|
|
1012
|
+
const headers = {
|
|
1013
|
+
"Content-Disposition": contentDisposition(name || path)
|
|
1014
|
+
};
|
|
1015
|
+
|
|
1016
|
+
// merge user-provided headers, which never get to override the disposition
|
|
1017
|
+
if (opts && opts.headers) {
|
|
1018
|
+
for (const key of Object.keys(opts.headers)) {
|
|
1019
|
+
if (key.toLowerCase() !== "content-disposition") {
|
|
1020
|
+
headers[key] = opts.headers[key];
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
940
1023
|
}
|
|
941
1024
|
|
|
942
|
-
|
|
943
|
-
|
|
1025
|
+
// merge user-provided options
|
|
1026
|
+
const merged = Object.create(opts ?? null);
|
|
1027
|
+
merged.headers = headers;
|
|
1028
|
+
|
|
1029
|
+
// resolved here so a relative path works against cwd, exactly as Express resolves it
|
|
1030
|
+
const fullPath = !merged.root ? Path.resolve(path) : path;
|
|
1031
|
+
|
|
1032
|
+
return this.sendFile(fullPath, merged, done);
|
|
944
1033
|
}
|
|
945
1034
|
|
|
946
1035
|
/**
|
|
@@ -1362,7 +1451,8 @@ module.exports = class Response extends Writable {
|
|
|
1362
1451
|
body = `<p>${statuses.message[status]}. Redirecting to ${escapeHtml(address)}</p>`;
|
|
1363
1452
|
},
|
|
1364
1453
|
default: () => {
|
|
1365
|
-
|
|
1454
|
+
// no Content-Type on purpose: Express leaves the header off entirely when
|
|
1455
|
+
// the client accepts neither text nor html
|
|
1366
1456
|
body = "";
|
|
1367
1457
|
}
|
|
1368
1458
|
});
|