fulmine.js 5.19.2 → 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 +2 -1
- package/src/adopt.js +20 -26
- package/src/application.js +63 -73
- package/src/cli.js +48 -48
- package/src/cluster.js +18 -27
- package/src/compression.js +141 -112
- package/src/declarative.js +611 -540
- package/src/hot-settings.js +80 -0
- package/src/index.js +11 -17
- package/src/lazy-readable.js +131 -0
- package/src/lazy-writable.js +97 -0
- package/src/middlewares.js +153 -130
- package/src/nest.js +22 -36
- package/src/node-shim.js +19 -16
- package/src/optimizer.js +600 -0
- package/src/options.d.ts +9 -4
- package/src/parse-query.js +3 -3
- package/src/request-utils.js +307 -0
- package/src/request.js +147 -548
- package/src/response-utils.js +88 -0
- package/src/response.js +228 -535
- package/src/route.js +7 -8
- package/src/router-utils.js +998 -0
- package/src/router.js +166 -2170
- package/src/server-shape.js +40 -51
- package/src/server-timing.js +32 -33
- package/src/socket.js +208 -0
- package/src/testing.js +43 -45
- package/src/usage.js +25 -25
- package/src/utils.js +165 -78
- package/src/verify.js +22 -31
- package/src/view.js +6 -8
- package/src/walk.js +581 -0
- package/src/websocket.js +34 -26
- package/src/work.js +22 -28
package/src/response.js
CHANGED
|
@@ -38,22 +38,29 @@ const {
|
|
|
38
38
|
withUtf8Charset,
|
|
39
39
|
asStatError,
|
|
40
40
|
httpError,
|
|
41
|
+
headersSentError,
|
|
42
|
+
applyWriteHead,
|
|
41
43
|
contentTypeFor,
|
|
42
44
|
statTag,
|
|
43
45
|
cachedStat,
|
|
44
46
|
NullObject
|
|
45
47
|
} = require("./utils.js");
|
|
46
|
-
const { Writable } = require("stream");
|
|
47
48
|
const { isAbsolute } = require("path");
|
|
48
49
|
const fs = require("fs");
|
|
49
50
|
const Path = require("path");
|
|
50
51
|
const statuses = require("statuses");
|
|
51
52
|
const { sign } = require("cookie-signature");
|
|
52
|
-
// events is faster at init, tseep is faster at sending events
|
|
53
|
-
// since we create a ton of objects and dont send a ton of events, its better to use events here
|
|
54
|
-
const { EventEmitter } = require("events");
|
|
55
|
-
const http = require("http");
|
|
56
53
|
const ms = require("ms");
|
|
54
|
+
const Socket = require("./socket.js");
|
|
55
|
+
const { LazyWritable } = require("./lazy-writable.js");
|
|
56
|
+
const {
|
|
57
|
+
kOutHeaders,
|
|
58
|
+
kShapeMode,
|
|
59
|
+
VALIDATED_HEADER_NAMES,
|
|
60
|
+
HEADER_NAME_BUF,
|
|
61
|
+
HEADER_VALUE_BUF,
|
|
62
|
+
statusLine
|
|
63
|
+
} = require("./response-utils.js");
|
|
57
64
|
|
|
58
65
|
// How much a chunked response may gather before it is handed to uWS. Measured on uWS alone with
|
|
59
66
|
// 33 KB written in 500 pieces: 16.2ms handed over one piece at a time, 0.79ms in 4 KB blocks,
|
|
@@ -67,48 +74,6 @@ const COALESCE_LIMIT = 16 * 1024;
|
|
|
67
74
|
// here up goes straight through.
|
|
68
75
|
const COALESCE_BELOW = 4 * 1024;
|
|
69
76
|
|
|
70
|
-
const outgoingMessage = new http.OutgoingMessage();
|
|
71
|
-
const symbols = Object.getOwnPropertySymbols(outgoingMessage);
|
|
72
|
-
// if a future node renames it, fall back to a private symbol rather than writing a property
|
|
73
|
-
// literally named "undefined", which is what indexing with undefined would do
|
|
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();
|
|
86
|
-
// The names and values that recur on every response, kept as Buffers for the uWS crossing: a
|
|
87
|
-
// Buffer is memcpy'd as it is, a string pays a UTF-8 scan and copy per call. A header that is
|
|
88
|
-
// not here just misses the lookup and crosses as the string it was. Names must stay lowercase,
|
|
89
|
-
// which is how writeHeaders receives them.
|
|
90
|
-
const HEADER_NAME_BUF = { __proto__: null };
|
|
91
|
-
const HEADER_VALUE_BUF = { __proto__: null };
|
|
92
|
-
for (const s of ["connection", "keep-alive", "content-type", "vary", "x-powered-by", "content-encoding"]) {
|
|
93
|
-
HEADER_NAME_BUF[s] = Buffer.from(s);
|
|
94
|
-
}
|
|
95
|
-
for (const s of [
|
|
96
|
-
"keep-alive",
|
|
97
|
-
"timeout=10",
|
|
98
|
-
"close",
|
|
99
|
-
"Fulmine",
|
|
100
|
-
"Accept-Encoding",
|
|
101
|
-
"text/html; charset=utf-8",
|
|
102
|
-
"text/plain; charset=utf-8",
|
|
103
|
-
"application/json; charset=utf-8",
|
|
104
|
-
"application/octet-stream",
|
|
105
|
-
"gzip",
|
|
106
|
-
"br",
|
|
107
|
-
"deflate",
|
|
108
|
-
"zstd"
|
|
109
|
-
]) {
|
|
110
|
-
HEADER_VALUE_BUF[s] = Buffer.from(s);
|
|
111
|
-
}
|
|
112
77
|
const HIGH_WATERMARK = 128 * 1024;
|
|
113
78
|
// the exact string json() writes, so send() can skip recomputing the charset on it
|
|
114
79
|
const JSON_UTF8 = "application/json; charset=utf-8";
|
|
@@ -116,283 +81,8 @@ const JSON_UTF8 = "application/json; charset=utf-8";
|
|
|
116
81
|
// than written out, since a year is already longer than any cache will honour.
|
|
117
82
|
const MAX_MAXAGE = 60 * 60 * 24 * 365 * 1000;
|
|
118
83
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
* The Socket's own error listener, shared across sockets: an error on the stand-in closes it,
|
|
122
|
-
* which is the close the connection trackers wait for. EventEmitter calls it with this = the
|
|
123
|
-
* emitter.
|
|
124
|
-
*
|
|
125
|
-
* @this {any}
|
|
126
|
-
* @param {any} err
|
|
127
|
-
*/
|
|
128
|
-
static _onError(err) {
|
|
129
|
-
this.emit("close");
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Enough of a node socket for the middleware that reaches for one. There is no socket object
|
|
134
|
-
* in uWS to hand over, so this stands in and forwards what it can to the response.
|
|
135
|
-
*
|
|
136
|
-
* @param {any} response
|
|
137
|
-
*/
|
|
138
|
-
constructor(response) {
|
|
139
|
-
super();
|
|
140
|
-
this.response = response;
|
|
141
|
-
this[kShapeMode] = true;
|
|
142
|
-
// middleware assigns to this one, which is why it is a field rather than a getter: express
|
|
143
|
-
// reads socket.encrypted for req.protocol and a proxy shim writes it
|
|
144
|
-
this.encrypted = response.req.app.ssl;
|
|
145
|
-
this.localPort = response.req.app.port;
|
|
146
|
-
// on-finished reads socket.readable before anything else, and a socket without one reads
|
|
147
|
-
// as a request that is already over
|
|
148
|
-
this.readable = true;
|
|
149
|
-
|
|
150
|
-
// shared, not an arrow: one per process instead of one per materialized socket
|
|
151
|
-
this.on("error", Socket._onError);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/** Whether anything more can be written, which stops being true once the response is done. */
|
|
155
|
-
get writable() {
|
|
156
|
-
return !this.response.finished;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/** The peer, as node reports it. Reading it out of µWS is slow, so the request caches it. */
|
|
160
|
-
get remoteAddress() {
|
|
161
|
-
return this.response.req.parsedIp;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/** A native µWS call almost no caller makes, so it stays behind its getter. */
|
|
165
|
-
get remotePort() {
|
|
166
|
-
return this.response.req._res.getRemotePort();
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* node's socket carries these three and applications call them on a request they mean to hold
|
|
171
|
-
* open, almost always to take the timeout off. µWS has no per socket timeout reachable from
|
|
172
|
-
* javascript, so they do nothing and hand the socket back the way node's do. n8n's chat trigger
|
|
173
|
-
* calls setTimeout on every webhook, and without it the workflow answered 500.
|
|
174
|
-
* @returns {this}
|
|
175
|
-
*/
|
|
176
|
-
setTimeout() {
|
|
177
|
-
return this;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/** @returns {this} */
|
|
181
|
-
setKeepAlive() {
|
|
182
|
-
return this;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/** @returns {this} */
|
|
186
|
-
setNoDelay() {
|
|
187
|
-
return this;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Finishes the response through the socket, which is how the middleware that only knows
|
|
192
|
-
* about sockets ends one.
|
|
193
|
-
* @param {any} [body]
|
|
194
|
-
*/
|
|
195
|
-
end(body) {
|
|
196
|
-
this.response.end(body);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* What a server side socket answers about itself. µWS owns the connection, so these follow the
|
|
201
|
-
* response: it is open until the response is over, and it was never a socket being dialled.
|
|
202
|
-
*/
|
|
203
|
-
get destroyed() {
|
|
204
|
-
return this.response.finished === true;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/** @returns {string} "open" until the response is over, as a served socket reads. */
|
|
208
|
-
get readyState() {
|
|
209
|
-
return this.response.finished === true ? "closed" : "open";
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/** @returns {boolean} never: this end was accepted, not dialled. */
|
|
213
|
-
get connecting() {
|
|
214
|
-
return false;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/** @returns {boolean} never, for the same reason. */
|
|
218
|
-
get pending() {
|
|
219
|
-
return false;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* The end of the connection node reports here. There is no address to read back from µWS, so
|
|
224
|
-
* this is the port the application bound and the family the peer arrived on.
|
|
225
|
-
* @returns {{address: string|undefined, family: string, port: number|undefined}}
|
|
226
|
-
*/
|
|
227
|
-
address() {
|
|
228
|
-
const remote = this.response.req.parsedIp;
|
|
229
|
-
return {
|
|
230
|
-
address: this.response.req.app._listenHost,
|
|
231
|
-
family: remote?.includes(":") ? "IPv6" : "IPv4",
|
|
232
|
-
port: this.localPort
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Drops the connection, which is what an application does to a client it will not serve. node
|
|
238
|
-
* takes an error and re-emits it; this closes and says so through 'close', since there is no
|
|
239
|
-
* socket underneath to carry an error of its own.
|
|
240
|
-
* @returns {this}
|
|
241
|
-
*/
|
|
242
|
-
destroy() {
|
|
243
|
-
this.close();
|
|
244
|
-
return this;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/** @returns {this} */
|
|
248
|
-
destroySoon() {
|
|
249
|
-
this.close();
|
|
250
|
-
return this;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Holds and resumes the body arriving on this connection, which is the only half of node's
|
|
255
|
-
* pause() that means anything here: the response is written when the application writes it.
|
|
256
|
-
* @returns {this}
|
|
257
|
-
*/
|
|
258
|
-
pause() {
|
|
259
|
-
this.response.req.pause();
|
|
260
|
-
return this;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/** @returns {this} */
|
|
264
|
-
resume() {
|
|
265
|
-
this.response.req.resume();
|
|
266
|
-
return this;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* node writes these bytes past the response, straight onto the connection. There is no way
|
|
271
|
-
* past µWS's framing here, so they go through the response instead, which is what the
|
|
272
|
-
* middleware writing to a socket means by it.
|
|
273
|
-
*
|
|
274
|
-
* @param {any} chunk
|
|
275
|
-
* @param {any} [encoding]
|
|
276
|
-
* @param {any} [callback]
|
|
277
|
-
* @returns {boolean}
|
|
278
|
-
*/
|
|
279
|
-
write(chunk, encoding, callback) {
|
|
280
|
-
return this.response.write(chunk, encoding, callback);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/** The event loop is µWS's, so there is nothing to hold open or let go. @returns {this} */
|
|
284
|
-
ref() {
|
|
285
|
-
return this;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/** @returns {this} */
|
|
289
|
-
unref() {
|
|
290
|
-
return this;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/** Closes the connection outright, without finishing a response first. */
|
|
294
|
-
close() {
|
|
295
|
-
if (this.response.finished) {
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
this.response.finished = true;
|
|
299
|
-
this.emit("close");
|
|
300
|
-
this.response._res.close();
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// One status line per code, built on first use: the default path, with no custom reason phrase,
|
|
305
|
-
// paid a template string and a trim per request for a line that never changes. Bounded to real
|
|
306
|
-
// HTTP codes so a wild writeHead value cannot grow the array or flip it into dictionary mode.
|
|
307
|
-
const STATUS_LINES = [];
|
|
308
|
-
/**
|
|
309
|
-
* @param {number} code
|
|
310
|
-
* @param {string|undefined} text an explicit reason phrase, which bypasses the cache
|
|
311
|
-
* @returns {string} the uWS status line, e.g. "200 OK"
|
|
312
|
-
*/
|
|
313
|
-
function statusLine(code, text) {
|
|
314
|
-
if (text === undefined && Number.isInteger(code) && code >= 100 && code <= 999) {
|
|
315
|
-
return STATUS_LINES[code] ?? (STATUS_LINES[code] = code + " " + (statuses.message[code] ?? "unknown"));
|
|
316
|
-
}
|
|
317
|
-
return `${code} ${text ?? statuses.message[code] ?? "unknown"}`.trim();
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* A Writable that has not built its state yet, the mirror of LazyReadable in request.js and there
|
|
322
|
-
* for the same reason: a response is a Writable because middleware expects one, and the ordinary
|
|
323
|
-
* one never uses it. `send()` reaches `end()`, which is overridden here and goes straight to
|
|
324
|
-
* _finish, so the WritableState is allocated for every response and read by nobody. It is needed
|
|
325
|
-
* only by res.write(), by a stream piped into the response, by cork and by the writableX getters.
|
|
326
|
-
*
|
|
327
|
-
* `Response extends LazyWritable`, whose prototype is Writable's, so `res instanceof Writable`
|
|
328
|
-
* stays true and every Writable method is reachable; what is missing is `_writableState`, built on
|
|
329
|
-
* the first touch. Measured at 45 nanoseconds a response on the machine this was written on.
|
|
330
|
-
*
|
|
331
|
-
* As on the Readable side the wrapping is generated rather than written out: every own member of
|
|
332
|
-
* Writable's prototype gets a version that materialises first, so there is no list to keep in step.
|
|
333
|
-
* Missing one would not be a slow path, it would be a TypeError on `undefined._writableState`.
|
|
334
|
-
*/
|
|
335
|
-
class LazyWritableBase {}
|
|
336
|
-
Object.setPrototypeOf(LazyWritableBase.prototype, Writable.prototype);
|
|
337
|
-
Object.setPrototypeOf(LazyWritableBase, Writable);
|
|
338
|
-
|
|
339
|
-
// what the chain says at runtime, said again for the type checker, which cannot see a prototype
|
|
340
|
-
// being reassigned
|
|
341
|
-
const LazyWritable = /** @type {typeof Writable} */ (/** @type {unknown} */ (LazyWritableBase));
|
|
342
|
-
|
|
343
|
-
/**
|
|
344
|
-
* Builds the stream this object has been pretending to be. Idempotent: everything reachable from
|
|
345
|
-
* outside goes through it, so it is called far more often than it does anything.
|
|
346
|
-
*
|
|
347
|
-
* EventEmitter's init keeps an _events that is already there, so both the shape the constructor
|
|
348
|
-
* wrote and any listener added before this survive it.
|
|
349
|
-
*
|
|
350
|
-
* @param {any} stream
|
|
351
|
-
*/
|
|
352
|
-
function materialiseWritable(stream) {
|
|
353
|
-
if (stream._writableState === undefined) {
|
|
354
|
-
Writable.call(stream);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
for (const member of [
|
|
359
|
-
...Object.getOwnPropertyNames(Writable.prototype),
|
|
360
|
-
...Object.getOwnPropertySymbols(Writable.prototype)
|
|
361
|
-
]) {
|
|
362
|
-
if (member === "constructor") {
|
|
363
|
-
continue;
|
|
364
|
-
}
|
|
365
|
-
const descriptor = /** @type {PropertyDescriptor} */ (Object.getOwnPropertyDescriptor(Writable.prototype, member));
|
|
366
|
-
if (typeof descriptor.value === "function") {
|
|
367
|
-
const inner = descriptor.value;
|
|
368
|
-
Object.defineProperty(LazyWritableBase.prototype, member, {
|
|
369
|
-
...descriptor,
|
|
370
|
-
/** @this {any} @param {...any} args */
|
|
371
|
-
value: function (...args) {
|
|
372
|
-
materialiseWritable(this);
|
|
373
|
-
return inner.apply(this, args);
|
|
374
|
-
}
|
|
375
|
-
});
|
|
376
|
-
} else if (descriptor.get || descriptor.set) {
|
|
377
|
-
const innerGet = descriptor.get;
|
|
378
|
-
const innerSet = descriptor.set;
|
|
379
|
-
Object.defineProperty(LazyWritableBase.prototype, member, {
|
|
380
|
-
...descriptor,
|
|
381
|
-
get: innerGet
|
|
382
|
-
? /** @this {any} */ function () {
|
|
383
|
-
materialiseWritable(this);
|
|
384
|
-
return innerGet.call(this);
|
|
385
|
-
}
|
|
386
|
-
: undefined,
|
|
387
|
-
set: innerSet
|
|
388
|
-
? /** @this {any} @param {any} value */ function (value) {
|
|
389
|
-
materialiseWritable(this);
|
|
390
|
-
innerSet.call(this, value);
|
|
391
|
-
}
|
|
392
|
-
: undefined
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
}
|
|
84
|
+
// what send takes as a range request: the bytes unit, checked on the header's text before parsing
|
|
85
|
+
const BYTES_RANGE = /^ *bytes=/;
|
|
396
86
|
|
|
397
87
|
module.exports = class Response extends LazyWritable {
|
|
398
88
|
/** @type {Socket|null} */
|
|
@@ -409,10 +99,8 @@ module.exports = class Response extends LazyWritable {
|
|
|
409
99
|
*
|
|
410
100
|
* uWS charges for a write against everything already buffered behind it, so a chunked response
|
|
411
101
|
* written in many small pieces costs quadratically once it passes the socket's own buffer:
|
|
412
|
-
* measured on uWS alone, 500 writes of 66 bytes take 13ms against 0.06ms for 100 of them
|
|
413
|
-
*
|
|
414
|
-
* only how many calls it takes to put it there.
|
|
415
|
-
* Null until the first chunked write, since a res.send never queues anything.
|
|
102
|
+
* measured on uWS alone, 500 writes of 66 bytes take 13ms against 0.06ms for 100 of them, and
|
|
103
|
+
* the same bytes in blocks cost 0.4ms. Null until the first chunked write.
|
|
416
104
|
* @type {Buffer[]|null}
|
|
417
105
|
*/
|
|
418
106
|
#queued = null;
|
|
@@ -423,7 +111,20 @@ module.exports = class Response extends LazyWritable {
|
|
|
423
111
|
/** Whether a flush is already booked for the end of this turn. */
|
|
424
112
|
#flushBooked = false;
|
|
425
113
|
|
|
426
|
-
/**
|
|
114
|
+
/** Whether the status line and the headers have reached uWS, which only a body write does. */
|
|
115
|
+
#headOut = false;
|
|
116
|
+
|
|
117
|
+
/**
|
|
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}
|
|
121
|
+
*/
|
|
122
|
+
#status = 200;
|
|
123
|
+
|
|
124
|
+
/** @type {string|undefined} */
|
|
125
|
+
#statusText = undefined;
|
|
126
|
+
|
|
127
|
+
/** @type {Response["headers"]|null} */
|
|
427
128
|
#outHeaders = null;
|
|
428
129
|
|
|
429
130
|
/**
|
|
@@ -437,9 +138,9 @@ module.exports = class Response extends LazyWritable {
|
|
|
437
138
|
* two that describe the connection, since every response carries them, and x-powered-by only
|
|
438
139
|
* when the setting asks for it.
|
|
439
140
|
*
|
|
440
|
-
* @param {
|
|
441
|
-
* @param {
|
|
442
|
-
* @param {
|
|
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
|
|
443
144
|
*/
|
|
444
145
|
constructor(res, req, app) {
|
|
445
146
|
super();
|
|
@@ -448,6 +149,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
448
149
|
// the order node's own Writable constructor lays down, so the hidden class is the one every
|
|
449
150
|
// other stream in the process has, and node's init keeps this object when the state is
|
|
450
151
|
// finally built
|
|
152
|
+
/** @type {Record<string, Function|undefined>} */
|
|
451
153
|
this._events = {
|
|
452
154
|
close: undefined,
|
|
453
155
|
error: undefined,
|
|
@@ -500,26 +202,22 @@ module.exports = class Response extends LazyWritable {
|
|
|
500
202
|
this.body = undefined;
|
|
501
203
|
// what was handed to uWS, kept so a caller asking for content-length after the fact can be
|
|
502
204
|
// answered, see get(). Undefined until the response ends, and for one that sends no body
|
|
503
|
-
/** @type {string|Buffer|undefined} */
|
|
205
|
+
/** @type {string|Buffer|Uint8Array|undefined} */
|
|
504
206
|
this._sentBody = undefined;
|
|
505
207
|
// false while the uWS route handler is still in its synchronous window, where uWS holds
|
|
506
208
|
// the socket corked itself; the two uWS entry points flip it once that window closes
|
|
507
209
|
this._corkNeeded = false;
|
|
508
|
-
// shared methods, not arrows: two closures and a once() wrapper here were four
|
|
509
|
-
//
|
|
210
|
+
// shared methods, not arrows: two closures and a once() wrapper here were four allocations
|
|
211
|
+
// per request. EventEmitter calls listeners with this = the emitter.
|
|
510
212
|
//
|
|
511
213
|
// Written into the map rather than through on(). A stream arrives with its _events already
|
|
512
214
|
// shaped, "close" and "error" among the keys and every value undefined, so filling two of
|
|
513
|
-
// them is the same hidden class on() would
|
|
514
|
-
//
|
|
515
|
-
// count. Two calls per response, and the profile put them at 6% of a hello-world.
|
|
215
|
+
// them is the same hidden class on() would produce and none of its work. Two calls per
|
|
216
|
+
// response, and the profile put them at 6% of a hello-world.
|
|
516
217
|
//
|
|
517
|
-
// The condition is the
|
|
518
|
-
//
|
|
519
|
-
|
|
520
|
-
// cast because _events and _eventsCount are EventEmitter's own bookkeeping and carry no
|
|
521
|
-
// type: they are what on() writes, and this writes the same two entries
|
|
522
|
-
const self = /** @type {any} */ (this);
|
|
218
|
+
// The condition is the safety of it: a fresh response has no listeners, so both slots are
|
|
219
|
+
// free, and anything else falls back to on(), which finds what this wrote.
|
|
220
|
+
const self = this;
|
|
523
221
|
const events = self._events;
|
|
524
222
|
if (
|
|
525
223
|
self._eventsCount === 0 &&
|
|
@@ -556,17 +254,15 @@ module.exports = class Response extends LazyWritable {
|
|
|
556
254
|
/**
|
|
557
255
|
* Drops the connection, which is how an application abandons a response it cannot finish: a
|
|
558
256
|
* download whose source dies mid-transfer has to leave the client with a reset rather than a
|
|
559
|
-
* truncated body
|
|
560
|
-
*
|
|
561
|
-
*
|
|
562
|
-
* way, `stream.on("error", () => res.destroy())`.
|
|
257
|
+
* truncated body. node destroys the socket here and uWS's close() is the same thing; without it
|
|
258
|
+
* the client waited for bytes that never came. LibreChat's download route is written that way,
|
|
259
|
+
* `stream.on("error", () => res.destroy())`.
|
|
563
260
|
*
|
|
564
|
-
* A response that is over, or one whose client is
|
|
565
|
-
*
|
|
566
|
-
*
|
|
567
|
-
* from the same finished flag the close sets, where node leaves it false until end() is called.
|
|
261
|
+
* A response that is over, or one whose client is gone, only tears the stream down: touching an
|
|
262
|
+
* aborted uWS response is a use after free. writableEnded reads true after this, where node
|
|
263
|
+
* leaves it false until end() is called.
|
|
568
264
|
*
|
|
569
|
-
* @param {
|
|
265
|
+
* @param {Error} [error] whatever the caller is destroying the response with
|
|
570
266
|
* @returns {this}
|
|
571
267
|
*/
|
|
572
268
|
destroy(error) {
|
|
@@ -596,7 +292,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
596
292
|
return;
|
|
597
293
|
}
|
|
598
294
|
this._pendingLinked = false;
|
|
599
|
-
const pending = /** @type {
|
|
295
|
+
const pending = /** @type {{_pendingIn?: {head: Response|null}}} */ (this)._pendingIn;
|
|
600
296
|
const prev = this._pendingPrev;
|
|
601
297
|
const next = this._pendingNext;
|
|
602
298
|
if (prev) {
|
|
@@ -613,12 +309,9 @@ module.exports = class Response extends LazyWritable {
|
|
|
613
309
|
|
|
614
310
|
/**
|
|
615
311
|
* Where node keeps the outgoing headers of an OutgoingMessage. Only code going through node's
|
|
616
|
-
* own header path
|
|
617
|
-
*
|
|
618
|
-
*
|
|
619
|
-
*
|
|
620
|
-
* A setter as well, because node assigns to this slot when it resets the headers, and a getter
|
|
621
|
-
* on its own would make that throw.
|
|
312
|
+
* own header path looks, cookie-session being the one in this project's tests, so the proxy
|
|
313
|
+
* standing in for it is built on the first look: it was a proxy, a handler object and two
|
|
314
|
+
* closures per response otherwise. A setter too, because node assigns to this slot on a reset.
|
|
622
315
|
*/
|
|
623
316
|
get [kOutHeaders]() {
|
|
624
317
|
if (!this.#outHeaders) {
|
|
@@ -654,7 +347,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
654
347
|
* over, as node's does; the request's `socket` is the same object and stays, so it comes
|
|
655
348
|
* through here instead.
|
|
656
349
|
*
|
|
657
|
-
* @returns {
|
|
350
|
+
* @returns {Socket}
|
|
658
351
|
*/
|
|
659
352
|
_socketShim() {
|
|
660
353
|
if (!this.#socket) {
|
|
@@ -712,7 +405,8 @@ module.exports = class Response extends LazyWritable {
|
|
|
712
405
|
* The booked flush. Static, so a response that never writes in pieces allocates nothing for it:
|
|
713
406
|
* nextTick forwards the receiver as an argument.
|
|
714
407
|
*
|
|
715
|
-
* @param {any} res
|
|
408
|
+
* @param {any} res the response whose queue is being flushed. Loose because naming the class
|
|
409
|
+
* inside its own body makes the checker see two unrelated `this` types
|
|
716
410
|
*/
|
|
717
411
|
static #flushOnTick(res) {
|
|
718
412
|
res.#flushBooked = false;
|
|
@@ -726,7 +420,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
726
420
|
* said how much there would be. Backpressure comes back as onWritable, which is what defers the
|
|
727
421
|
* callback rather than dropping the chunk.
|
|
728
422
|
*
|
|
729
|
-
* @param {any} chunk
|
|
423
|
+
* @param {any} chunk whatever a Writable was handed, which node does not narrow
|
|
730
424
|
* @param {BufferEncoding} encoding
|
|
731
425
|
* @param {(err?: Error|null) => void} callback
|
|
732
426
|
*/
|
|
@@ -744,13 +438,15 @@ module.exports = class Response extends LazyWritable {
|
|
|
744
438
|
|
|
745
439
|
this.writingChunk = true;
|
|
746
440
|
this._res.cork(() => {
|
|
747
|
-
if (!this
|
|
748
|
-
|
|
441
|
+
if (!this.#headOut) {
|
|
442
|
+
if (!this.headersSent) {
|
|
443
|
+
this.writeHead(this.statusCode);
|
|
444
|
+
}
|
|
749
445
|
// "unknown" and not the bare number: node writes that reason phrase for a code it
|
|
750
446
|
// has no message for, so the raw status lines match. The default 200 with no
|
|
751
447
|
// phrase is uWS's own head, byte for byte, so it is not written at all
|
|
752
|
-
if (this
|
|
753
|
-
this._res.writeStatus(statusLine(this
|
|
448
|
+
if (this.#status !== 200 || this.#statusText !== undefined) {
|
|
449
|
+
this._res.writeStatus(statusLine(this.#status, this.#statusText));
|
|
754
450
|
}
|
|
755
451
|
this.writeHeaders(typeof chunk === "string");
|
|
756
452
|
}
|
|
@@ -819,61 +515,43 @@ module.exports = class Response extends LazyWritable {
|
|
|
819
515
|
|
|
820
516
|
/**
|
|
821
517
|
* Sets the status and, optionally, a batch of headers, the way node does. The second argument
|
|
822
|
-
* is either the status message or the headers, since node allows both shapes.
|
|
823
|
-
*
|
|
824
|
-
*
|
|
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.
|
|
825
521
|
*
|
|
826
522
|
* Every header goes through setHeader and not through set. This is node's method, not
|
|
827
|
-
* Express's:
|
|
828
|
-
*
|
|
829
|
-
*
|
|
830
|
-
* on top of Express answers, @astrojs/node and @sveltejs/adapter-node included, so the charset
|
|
831
|
-
* was being added to pages nobody asked it for.
|
|
523
|
+
* Express's: a content-type given here keeps the value it was given, where res.set would append
|
|
524
|
+
* a charset. Every meta-framework on Express answers with writeHead, @astrojs/node and
|
|
525
|
+
* @sveltejs/adapter-node included, so the charset was added to pages nobody asked it for.
|
|
832
526
|
*
|
|
833
527
|
* @param {number} statusCode
|
|
834
|
-
* @param {string|
|
|
835
|
-
*
|
|
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]
|
|
836
531
|
* @returns {this}
|
|
837
532
|
*/
|
|
838
533
|
writeHead(statusCode, statusMessage, headers) {
|
|
839
|
-
this.
|
|
840
|
-
|
|
841
|
-
this.statusText = statusMessage;
|
|
842
|
-
}
|
|
843
|
-
if (!headers) {
|
|
844
|
-
if (!statusMessage) return this;
|
|
845
|
-
// the two-argument shape, where what looked like a reason phrase is the headers. A
|
|
846
|
-
// string reaching here was already taken as the phrase above and simply has no keys.
|
|
847
|
-
headers = /** @type {Record<string, any>} */ (statusMessage);
|
|
848
|
-
}
|
|
849
|
-
if (Array.isArray(headers)) {
|
|
850
|
-
// node takes a flat list here, name then value, and not a list of pairs. An odd length
|
|
851
|
-
// is the caller's mistake and node names the argument in what it throws
|
|
852
|
-
if (headers.length % 2 !== 0) {
|
|
853
|
-
/** @type {NodeJS.ErrnoException} */
|
|
854
|
-
const err = new TypeError(`The argument 'headers' is invalid. Received ${JSON.stringify(headers)}`);
|
|
855
|
-
err.code = "ERR_INVALID_ARG_VALUE";
|
|
856
|
-
throw err;
|
|
857
|
-
}
|
|
858
|
-
for (let i = 0; i < headers.length; i += 2) {
|
|
859
|
-
this.setHeader(headers[i], headers[i + 1]);
|
|
860
|
-
}
|
|
861
|
-
return this;
|
|
534
|
+
if (this.headersSent) {
|
|
535
|
+
throw headersSentError("write");
|
|
862
536
|
}
|
|
863
|
-
|
|
864
|
-
|
|
537
|
+
this.statusCode = statusCode;
|
|
538
|
+
const reason = applyWriteHead(this, statusMessage, headers);
|
|
539
|
+
if (reason !== undefined) {
|
|
540
|
+
this.statusText = reason;
|
|
865
541
|
}
|
|
542
|
+
this.#status = statusCode;
|
|
543
|
+
this.#statusText = this.statusText;
|
|
544
|
+
this.headersSent = true;
|
|
866
545
|
return this;
|
|
867
546
|
}
|
|
868
547
|
|
|
869
548
|
/**
|
|
870
|
-
* Writes every header set so far to uWS, which is the point of no return. Content-Length is
|
|
871
|
-
*
|
|
872
|
-
*
|
|
549
|
+
* Writes every header set so far to uWS, which is the point of no return. Content-Length is not
|
|
550
|
+
* one of them: uWS wants the length through tryEnd or endWithoutBody, so it is taken out here
|
|
551
|
+
* and kept on totalSize, where it also turns chunked framing off.
|
|
873
552
|
*
|
|
874
|
-
* One writeHeader per header on purpose. Packing the whole head into a single writeStatus
|
|
875
|
-
*
|
|
876
|
-
* already flat, while the packed head is concatenated fresh per response, see issue #11.
|
|
553
|
+
* One writeHeader per header on purpose. Packing the whole head into a single writeStatus works
|
|
554
|
+
* on the wire and measured slower: constant header strings cross already flat, see issue #11.
|
|
877
555
|
*
|
|
878
556
|
* @param {boolean} utf8 unused, kept because node's equivalent takes it and the two callers
|
|
879
557
|
* differ on what they know about the body
|
|
@@ -890,12 +568,10 @@ module.exports = class Response extends LazyWritable {
|
|
|
890
568
|
// the value here is nearly always the 10-char "keep-alive", which paid a scan per response
|
|
891
569
|
const closing =
|
|
892
570
|
typeof connection === "string" && connection.length === 5 && connection.toLowerCase() === "close";
|
|
893
|
-
// for..in over an object some responses delete from,
|
|
894
|
-
//
|
|
895
|
-
//
|
|
896
|
-
//
|
|
897
|
-
// of them made costs that response and nothing after it. The request side deleted on every
|
|
898
|
-
// request, which is what made it worth a different structure
|
|
571
|
+
// for..in over an object some responses delete from, the shape the request side was taken
|
|
572
|
+
// off for #rawHeadersEntries. It stays here because the deletes are rare: only 204, 304,
|
|
573
|
+
// 205, the freshness branch of sendFile and removeHeader do it, and this object is built
|
|
574
|
+
// fresh per response. The request side deleted on every request
|
|
899
575
|
for (const header in headers) {
|
|
900
576
|
if (closing && header === "keep-alive") {
|
|
901
577
|
continue;
|
|
@@ -919,16 +595,19 @@ module.exports = class Response extends LazyWritable {
|
|
|
919
595
|
}
|
|
920
596
|
}
|
|
921
597
|
this.headersSent = true;
|
|
598
|
+
this.#headOut = true;
|
|
922
599
|
}
|
|
923
600
|
|
|
924
601
|
/**
|
|
925
|
-
* What node calls before writing a body when the caller never called writeHead
|
|
926
|
-
* 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.
|
|
927
606
|
*/
|
|
928
607
|
_implicitHeader() {
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
608
|
+
if (!this.headersSent) {
|
|
609
|
+
this.writeHead(this.statusCode);
|
|
610
|
+
}
|
|
932
611
|
}
|
|
933
612
|
|
|
934
613
|
/**
|
|
@@ -967,17 +646,29 @@ module.exports = class Response extends LazyWritable {
|
|
|
967
646
|
}
|
|
968
647
|
|
|
969
648
|
/**
|
|
970
|
-
* @param {
|
|
971
|
-
*
|
|
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]
|
|
972
654
|
* @returns {this}
|
|
973
655
|
*/
|
|
974
|
-
end(data, cb) {
|
|
656
|
+
end(data, encoding, cb) {
|
|
975
657
|
if (typeof data === "function") {
|
|
976
658
|
cb = data;
|
|
977
659
|
data = undefined;
|
|
660
|
+
encoding = undefined;
|
|
661
|
+
} else if (typeof encoding === "function") {
|
|
662
|
+
cb = encoding;
|
|
663
|
+
encoding = undefined;
|
|
978
664
|
}
|
|
979
665
|
if (typeof cb !== "function") {
|
|
980
|
-
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);
|
|
981
672
|
}
|
|
982
673
|
|
|
983
674
|
if (this.writingChunk) {
|
|
@@ -989,7 +680,11 @@ module.exports = class Response extends LazyWritable {
|
|
|
989
680
|
if (this.finished) {
|
|
990
681
|
return this;
|
|
991
682
|
}
|
|
992
|
-
|
|
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
|
+
}
|
|
993
688
|
// uWS holds the socket corked for the synchronous window of its route handler, and
|
|
994
689
|
// cork inside cork is a passthrough: the wrapper and its closure are only paid once the
|
|
995
690
|
// answer has outlived that window, which is what _corkNeeded records
|
|
@@ -1005,42 +700,38 @@ module.exports = class Response extends LazyWritable {
|
|
|
1005
700
|
* The corked tail of end(): status, headers, body and the finish events. Split out so a
|
|
1006
701
|
* synchronous answer calls it straight, already inside uWS's own cork.
|
|
1007
702
|
*
|
|
1008
|
-
* @param {
|
|
1009
|
-
* @param {
|
|
703
|
+
* @param {string|Buffer|Uint8Array|null|undefined} data the last body piece
|
|
704
|
+
* @param {(() => void)|undefined} cb
|
|
1010
705
|
*/
|
|
1011
706
|
_finish(data, cb) {
|
|
1012
707
|
// read before the head is written below, which is what sets the flag: what matters further
|
|
1013
708
|
// down is whether something had already committed the framing, a flushHeaders() or a first
|
|
1014
709
|
// res.write(), not whether this call is about to write the head itself
|
|
1015
|
-
const headWasAlreadyOut = this
|
|
1016
|
-
if (!this
|
|
1017
|
-
// freshness is not decided here. node's end() knows nothing about conditional
|
|
1018
|
-
//
|
|
1019
|
-
//
|
|
1020
|
-
//
|
|
1021
|
-
// "unknown" for a code without a message, as node's status line has it.
|
|
1022
|
-
//
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
this._res.writeStatus(statusLine(this.statusCode, this.statusText));
|
|
710
|
+
const headWasAlreadyOut = this.#headOut;
|
|
711
|
+
if (!this.#headOut) {
|
|
712
|
+
// freshness is not decided here. node's end() knows nothing about conditional requests,
|
|
713
|
+
// and Express answers 304 from send() and from sendFile(), each of which strips the
|
|
714
|
+
// entity headers first. Deciding it here made res.end("body") answer 304 and drop the
|
|
715
|
+
// body the caller had just written.
|
|
716
|
+
// "unknown" for a code without a message, as node's status line has it. The default 200
|
|
717
|
+
// with no phrase is not written at all: uWS emits the identical head on its own
|
|
718
|
+
if (this.#status !== 200 || this.#statusText !== undefined) {
|
|
719
|
+
this._res.writeStatus(statusLine(this.#status, this.#statusText));
|
|
1026
720
|
}
|
|
1027
721
|
this.writeHeaders(true);
|
|
1028
722
|
}
|
|
1029
723
|
const contentLength = this.headers["content-length"];
|
|
1030
724
|
// The client said this connection ends here, and it is this end() that has to make it so.
|
|
1031
|
-
//
|
|
1032
|
-
// the socket open and the bytes after that request were read as another one
|
|
1033
|
-
// there, and a server that does not is a server the client and it disagree with about how
|
|
1034
|
-
// many requests were sent. See saysClose.
|
|
725
|
+
// uWS closes by itself for a bare "close" and not for a list, so "keep-alive, close" left
|
|
726
|
+
// the socket open and the bytes after that request were read as another one. See saysClose.
|
|
1035
727
|
//
|
|
1036
|
-
// Only where a length goes out with it
|
|
728
|
+
// Only where a length goes out with it: endWithoutBody takes the flag as its second
|
|
1037
729
|
// argument and reads the first as the length whatever it holds, so asking it to close
|
|
1038
|
-
// without one writes "Content-Length: 9223372036854775808" onto a 204.
|
|
1039
|
-
// µWS's own rule, which closes for a bare "close" and not for a list.
|
|
730
|
+
// without one writes "Content-Length: 9223372036854775808" onto a 204.
|
|
1040
731
|
const closeConnection = this.req._connectionClose === true;
|
|
1041
732
|
// 204 and 304 carry no body, so no Content-Length may describe one either; 1xx is the
|
|
1042
733
|
// third case, by range
|
|
1043
|
-
if (this
|
|
734
|
+
if (this.#status === 204 || this.#status === 304 || this.#status < 200) {
|
|
1044
735
|
// no body and no length describing one, whatever the caller passed. node decides
|
|
1045
736
|
// this the same way, from the status alone, so res.status(304).end("x") sends the
|
|
1046
737
|
// status and nothing else on either.
|
|
@@ -1051,11 +742,9 @@ module.exports = class Response extends LazyWritable {
|
|
|
1051
742
|
// whatever is still queued goes first: end() must not overtake the body written before it
|
|
1052
743
|
this.#flushQueued(null);
|
|
1053
744
|
// The head has already gone out without a length, which is what flushHeaders() and the
|
|
1054
|
-
// first res.write() both do, so this response is committed to chunked framing
|
|
1055
|
-
//
|
|
1056
|
-
//
|
|
1057
|
-
// here would have it append a length to a head that already said otherwise, which is
|
|
1058
|
-
// what the comparison test caught.
|
|
745
|
+
// first res.write() both do, so this response is committed to chunked framing. node is
|
|
746
|
+
// committed the same way: after a flush, res.end("body") sends a chunk. Handing the
|
|
747
|
+
// body to uWS's end() here would append a length to a head that already said otherwise
|
|
1059
748
|
if (data) {
|
|
1060
749
|
this._res.write(data);
|
|
1061
750
|
this._sentBody = data;
|
|
@@ -1067,7 +756,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
1067
756
|
if (this.req.method === "HEAD") {
|
|
1068
757
|
const length = Buffer.byteLength(data ?? "");
|
|
1069
758
|
this.headers["content-length"] = String(length);
|
|
1070
|
-
this._res.endWithoutBody(length
|
|
759
|
+
this._res.endWithoutBody(length, closeConnection);
|
|
1071
760
|
} else {
|
|
1072
761
|
// remembered rather than measured: only a caller that asks for content-length pays
|
|
1073
762
|
// for it, and uWS is measuring the same bytes for the wire anyway
|
|
@@ -1102,7 +791,8 @@ module.exports = class Response extends LazyWritable {
|
|
|
1102
791
|
*/
|
|
1103
792
|
send(body) {
|
|
1104
793
|
if (this.headersSent) {
|
|
1105
|
-
|
|
794
|
+
// what express's send meets first once the head is out is setHeader's refusal
|
|
795
|
+
throw headersSentError("set");
|
|
1106
796
|
}
|
|
1107
797
|
// a typed array is bytes to send, not an object to serialise: res.send(new Uint8Array([104,
|
|
1108
798
|
// 101, 121])) is "hey" and not {"0":104,"1":101,"2":121}. Uint8Array and not every view
|
|
@@ -1157,13 +847,10 @@ module.exports = class Response extends LazyWritable {
|
|
|
1157
847
|
}
|
|
1158
848
|
// the ETag belongs here rather than in end(): node's end() does not produce one, so
|
|
1159
849
|
// res.end() and res.redirect() must not either. It has to be set before end() reads
|
|
1160
|
-
// req.fresh, which compares If-None-Match against it.
|
|
1161
|
-
//
|
|
1162
|
-
//
|
|
1163
|
-
//
|
|
1164
|
-
// defined over those two alone, and express's own suite failed on it, "should send ETag
|
|
1165
|
-
// in response to <METHOD> request" exists per method. The "etag methods" setting is that
|
|
1166
|
-
// gate as an opt-in, see issue #10.
|
|
850
|
+
// req.fresh, which compares If-None-Match against it. body is defined by the time it gets
|
|
851
|
+
// here, so an empty one still earns an ETag; testing truthiness left send("") and
|
|
852
|
+
// send(null) without one. Every method by default, not only GET and HEAD: express's own
|
|
853
|
+
// suite has "should send ETag in response to <METHOD> request" per method, see issue #10
|
|
1167
854
|
const hot = this.app._hot();
|
|
1168
855
|
const etagFn = hot.etagFn;
|
|
1169
856
|
if (
|
|
@@ -1205,10 +892,11 @@ module.exports = class Response extends LazyWritable {
|
|
|
1205
892
|
* options position is the callback.
|
|
1206
893
|
*
|
|
1207
894
|
* Options: `root`, `maxAge`, `lastModified`, `headers`, `dotfiles` ("allow", "deny" or
|
|
1208
|
-
* "ignore"), `acceptRanges`, `cacheControl`, `immutable
|
|
895
|
+
* "ignore"), `acceptRanges`, `cacheControl`, `immutable` and `etag`.
|
|
1209
896
|
*
|
|
1210
897
|
* @param {string} path
|
|
1211
|
-
* @param {import("./options").SendFileOptions} [options]
|
|
898
|
+
* @param {import("./options").SendFileOptions|((err?: Error) => void)} [options] or the callback in
|
|
899
|
+
* its place
|
|
1212
900
|
* @param {(err?: Error) => void} [callback] called once sent, or with the error
|
|
1213
901
|
*/
|
|
1214
902
|
sendFile(path, options = new NullObject(), callback) {
|
|
@@ -1221,20 +909,17 @@ module.exports = class Response extends LazyWritable {
|
|
|
1221
909
|
throw new TypeError("path must be a string to res.sendFile");
|
|
1222
910
|
}
|
|
1223
911
|
if (typeof options === "function") {
|
|
1224
|
-
callback =
|
|
912
|
+
callback = options;
|
|
1225
913
|
options = new NullObject();
|
|
1226
914
|
}
|
|
1227
915
|
if (!options) options = new NullObject();
|
|
1228
|
-
// the callback is optional: without one, errors go to next().
|
|
1229
|
-
//
|
|
1230
|
-
//
|
|
1231
|
-
//
|
|
1232
|
-
//
|
|
1233
|
-
// the router's next and not the route's: express reports a file it could not serve past
|
|
1234
|
-
// the rest of the route, so a four argument handler written inside the route never sees
|
|
1235
|
-
// it. _leaveRoute is that; req.next is kept for everything else, see Walk#stepOutOfRoute
|
|
916
|
+
// the callback is optional: without one, errors go to next(). Express's completion handler
|
|
917
|
+
// exactly: a callback hears everything and the response is left alone, so it can still
|
|
918
|
+
// answer 200 after a 404 error. Without one, a directory falls through as a plain next().
|
|
919
|
+
// The router's next and not the route's: express reports a file it could not serve past the
|
|
920
|
+
// rest of the route, so a four argument handler inside the route never sees it
|
|
1236
921
|
const next = this.req._leaveRoute ?? this.req.next;
|
|
1237
|
-
const done = /** @type {(err?:
|
|
922
|
+
const done = /** @type {(err?: NodeJS.ErrnoException) => void} */ (
|
|
1238
923
|
(err) => {
|
|
1239
924
|
if (callback) return callback(err);
|
|
1240
925
|
if (err && err.code === "EISDIR") return next();
|
|
@@ -1242,16 +927,15 @@ module.exports = class Response extends LazyWritable {
|
|
|
1242
927
|
}
|
|
1243
928
|
);
|
|
1244
929
|
// default options
|
|
1245
|
-
// Normalised the way send does
|
|
1246
|
-
//
|
|
1247
|
-
//
|
|
1248
|
-
//
|
|
1249
|
-
//
|
|
1250
|
-
// Number() around the lot, and not only around the branch that is already a number: ms()
|
|
1251
|
-
// answers undefined for a duration it cannot read, and Number.isNaN(undefined) is false,
|
|
1252
|
-
// so an unreadable string reached the header as "max-age=NaN".
|
|
930
|
+
// Normalised the way send does: max-age takes a non-negative integer count of seconds, so
|
|
931
|
+
// 0.5, -1 and Infinity are all invalid, and a client that cannot read the directive may
|
|
932
|
+
// throw away the whole Cache-Control header. Number() around the lot and not only around
|
|
933
|
+
// the branch that is already a number: ms() answers undefined for a duration it cannot
|
|
934
|
+
// read, and Number.isNaN(undefined) is false, so an unreadable string reached it as NaN
|
|
1253
935
|
const maxAge = Number(
|
|
1254
|
-
typeof options.maxAge === "string"
|
|
936
|
+
typeof options.maxAge === "string"
|
|
937
|
+
? ms(/** @type {import("ms").StringValue} */ (options.maxAge))
|
|
938
|
+
: options.maxAge
|
|
1255
939
|
);
|
|
1256
940
|
options.maxAge = Number.isNaN(maxAge) ? 0 : Math.min(Math.max(0, maxAge), MAX_MAXAGE);
|
|
1257
941
|
if (typeof options.lastModified === "undefined") {
|
|
@@ -1342,14 +1026,15 @@ module.exports = class Response extends LazyWritable {
|
|
|
1342
1026
|
} catch (err) {
|
|
1343
1027
|
// the fs error itself, carrying its errno and path, with send's status written on
|
|
1344
1028
|
// it: a missing file is the request's 404, an unreadable one is the server's 500
|
|
1345
|
-
return done(asStatError(/** @type {
|
|
1029
|
+
return done(asStatError(/** @type {import("./utils.js").HttpError} */ (err)));
|
|
1346
1030
|
}
|
|
1347
1031
|
if (stat.isDirectory()) {
|
|
1348
1032
|
// Express reports a directory as an EISDIR with no status, because send tells it
|
|
1349
1033
|
// apart from an error: it emits "directory", and res.sendFile has no listener for
|
|
1350
1034
|
// one. So this is not a 404, and an error handler reading err.code sees the code
|
|
1351
1035
|
// it expects. Without a callback, done() turns it into a plain next().
|
|
1352
|
-
|
|
1036
|
+
/** @type {NodeJS.ErrnoException} */
|
|
1037
|
+
const err = new Error("EISDIR, read");
|
|
1353
1038
|
err.code = "EISDIR";
|
|
1354
1039
|
return done(err);
|
|
1355
1040
|
}
|
|
@@ -1377,8 +1062,10 @@ module.exports = class Response extends LazyWritable {
|
|
|
1377
1062
|
this.setHeader(header, options.headers[header]);
|
|
1378
1063
|
}
|
|
1379
1064
|
}
|
|
1380
|
-
|
|
1381
|
-
|
|
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);
|
|
1382
1069
|
}
|
|
1383
1070
|
|
|
1384
1071
|
// etag, from the stat and never from the app's "etag fn". send computes this itself with
|
|
@@ -1427,7 +1114,10 @@ module.exports = class Response extends LazyWritable {
|
|
|
1427
1114
|
|
|
1428
1115
|
// range requests
|
|
1429
1116
|
if (options.acceptRanges) {
|
|
1430
|
-
|
|
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)) {
|
|
1431
1121
|
// the branch above established the header is there, so range() cannot answer
|
|
1432
1122
|
// the undefined it uses to mean "no Range header"
|
|
1433
1123
|
let ranges = /** @type {ReturnType<typeof import("range-parser")>} */ (
|
|
@@ -1486,7 +1176,8 @@ module.exports = class Response extends LazyWritable {
|
|
|
1486
1176
|
// ECONNABORTED to a callback and never to next(), so aborts stay out of
|
|
1487
1177
|
// the error middleware.
|
|
1488
1178
|
if (this.aborted && callback) {
|
|
1489
|
-
|
|
1179
|
+
/** @type {NodeJS.ErrnoException} */
|
|
1180
|
+
const err = new Error("Request aborted");
|
|
1490
1181
|
err.code = "ECONNABORTED";
|
|
1491
1182
|
callback(err);
|
|
1492
1183
|
}
|
|
@@ -1604,7 +1295,8 @@ module.exports = class Response extends LazyWritable {
|
|
|
1604
1295
|
* what a media type is. res.set does that, and is what Express code should use.
|
|
1605
1296
|
*
|
|
1606
1297
|
* @param {string} field
|
|
1607
|
-
* @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
|
|
1608
1300
|
* @returns {this}
|
|
1609
1301
|
* @throws {Error} once the headers have gone out
|
|
1610
1302
|
* @throws {TypeError} if the name is not a token, the value is undefined, or the value holds a
|
|
@@ -1612,7 +1304,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
1612
1304
|
*/
|
|
1613
1305
|
setHeader(field, value) {
|
|
1614
1306
|
if (this.headersSent) {
|
|
1615
|
-
throw
|
|
1307
|
+
throw headersSentError("set");
|
|
1616
1308
|
}
|
|
1617
1309
|
// one Map hit for a name already validated and lowercased: middleware writes the same
|
|
1618
1310
|
// constant names on every request. Insert-only after validation, so no bad name can enter
|
|
@@ -1642,14 +1334,13 @@ module.exports = class Response extends LazyWritable {
|
|
|
1642
1334
|
}
|
|
1643
1335
|
|
|
1644
1336
|
/**
|
|
1645
|
-
* Throws away any header that could not be written, so
|
|
1646
|
-
*
|
|
1337
|
+
* Throws away any header that could not be written, so flushing this response cannot fail on
|
|
1338
|
+
* one. setHeader refuses these on the way in, but `res.headers` is the live object, so an
|
|
1647
1339
|
* assignment into that still gets a value in here.
|
|
1648
1340
|
*
|
|
1649
1341
|
* Only the error page calls it. A throw out of the flush there is not recoverable: the error
|
|
1650
|
-
* page is what runs after a throw, so
|
|
1651
|
-
*
|
|
1652
|
-
* middleware's own headers belong on the error response too.
|
|
1342
|
+
* page is what runs after a throw, so nobody is left to catch it. Everything writable is left
|
|
1343
|
+
* alone, since a middleware's own headers belong on the error response too.
|
|
1653
1344
|
*
|
|
1654
1345
|
* @returns {void}
|
|
1655
1346
|
*/
|
|
@@ -1663,30 +1354,30 @@ module.exports = class Response extends LazyWritable {
|
|
|
1663
1354
|
|
|
1664
1355
|
/**
|
|
1665
1356
|
* Hands the status line and the headers over now, without waiting for a body, which is node's
|
|
1666
|
-
* flushHeaders().
|
|
1667
|
-
* being produced, and one of them is `@angular/ssr`'s writeResponseToNodeResponse, which calls
|
|
1668
|
-
* it before streaming a rendered page.
|
|
1357
|
+
* flushHeaders(). `@angular/ssr`'s writeResponseToNodeResponse calls it before streaming a page.
|
|
1669
1358
|
*
|
|
1670
1359
|
* **The head does not reach the wire here.** uWS holds it until the first body chunk, so a
|
|
1671
1360
|
* client sees nothing until then, where express answers at once. `beginWrite` is uWS's API for
|
|
1672
1361
|
* this and is unusable: it emits a stray CRLF before the first chunk size, which node's parser
|
|
1673
|
-
* rejects as HPE_INVALID_CHUNK_SIZE. Checked against v20.69.0
|
|
1362
|
+
* rejects as HPE_INVALID_CHUNK_SIZE. Checked against v20.69.0.
|
|
1674
1363
|
*
|
|
1675
1364
|
* A second call does nothing, as node's does. Nothing is written for a response already
|
|
1676
|
-
* finished or aborted
|
|
1365
|
+
* finished or aborted.
|
|
1677
1366
|
*
|
|
1678
1367
|
* @returns {void}
|
|
1679
1368
|
*/
|
|
1680
1369
|
flushHeaders() {
|
|
1681
|
-
if (this
|
|
1370
|
+
if (this.#headOut || this.finished || this.aborted) {
|
|
1682
1371
|
return;
|
|
1683
1372
|
}
|
|
1684
1373
|
this._res.cork(() => {
|
|
1685
|
-
|
|
1374
|
+
if (!this.headersSent) {
|
|
1375
|
+
this.writeHead(this.statusCode);
|
|
1376
|
+
}
|
|
1686
1377
|
// the same rule the chunked write path follows: uWS emits the 200 head itself, byte for
|
|
1687
1378
|
// byte, so writing it again would only cost a crossing
|
|
1688
|
-
if (this
|
|
1689
|
-
this._res.writeStatus(statusLine(this
|
|
1379
|
+
if (this.#status !== 200 || this.#statusText !== undefined) {
|
|
1380
|
+
this._res.writeStatus(statusLine(this.#status, this.#statusText));
|
|
1690
1381
|
}
|
|
1691
1382
|
// true, as the chunked path passes for a string chunk: what follows a flush is a body
|
|
1692
1383
|
// written in pieces, and the framing has to be the one that allows them
|
|
@@ -1695,13 +1386,11 @@ module.exports = class Response extends LazyWritable {
|
|
|
1695
1386
|
}
|
|
1696
1387
|
|
|
1697
1388
|
/**
|
|
1698
|
-
* node's `writeEarlyHints`, which sends a `103` carrying the resources the page will want
|
|
1699
|
-
* browser can start fetching them while the server is still rendering.
|
|
1389
|
+
* node's `writeEarlyHints`, which sends a `103` carrying the resources the page will want.
|
|
1700
1390
|
*
|
|
1701
|
-
* **Nothing is sent here.**
|
|
1702
|
-
*
|
|
1703
|
-
*
|
|
1704
|
-
* because node calls it once the hints are out and a caller may be waiting on it.
|
|
1391
|
+
* **Nothing is sent here.** uWebSockets.js has no API for an informational response, so this
|
|
1392
|
+
* exists only so code written for Express keeps running instead of dying on
|
|
1393
|
+
* "res.writeEarlyHints is not a function". The callback is still called, as node calls it.
|
|
1705
1394
|
*
|
|
1706
1395
|
* `writeContinue` and `writeProcessing` below are the same story with `100` and `102`.
|
|
1707
1396
|
*
|
|
@@ -1745,10 +1434,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
1745
1434
|
*/
|
|
1746
1435
|
#refuseInformationAfterHead() {
|
|
1747
1436
|
if (this.headersSent) {
|
|
1748
|
-
|
|
1749
|
-
const err = new Error("Cannot write headers after they are sent to the client");
|
|
1750
|
-
err.code = "ERR_HTTP_HEADERS_SENT";
|
|
1751
|
-
throw err;
|
|
1437
|
+
throw headersSentError("write");
|
|
1752
1438
|
}
|
|
1753
1439
|
}
|
|
1754
1440
|
|
|
@@ -1781,7 +1467,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
1781
1467
|
* node's `assignSocket`, which the http server uses when a response is
|
|
1782
1468
|
* handed a raw socket. There is no such socket here.
|
|
1783
1469
|
*
|
|
1784
|
-
* @param {
|
|
1470
|
+
* @param {import("net").Socket} [socket] node takes one here and there is none to take
|
|
1785
1471
|
* @returns {void}
|
|
1786
1472
|
*/
|
|
1787
1473
|
assignSocket(socket) {}
|
|
@@ -1789,7 +1475,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
1789
1475
|
/**
|
|
1790
1476
|
* node's `detachSocket`, which the http server uses when a response is
|
|
1791
1477
|
* handed a raw socket. There is no such socket here.
|
|
1792
|
-
* @param {
|
|
1478
|
+
* @param {import("net").Socket} [socket] node takes one here and there is none to take
|
|
1793
1479
|
* @returns {void}
|
|
1794
1480
|
*/
|
|
1795
1481
|
detachSocket(socket) {}
|
|
@@ -1850,10 +1536,10 @@ module.exports = class Response extends LazyWritable {
|
|
|
1850
1536
|
const key = name.toLowerCase();
|
|
1851
1537
|
const current = this.headers[key];
|
|
1852
1538
|
if (current === undefined) {
|
|
1853
|
-
return this.setHeader(name,
|
|
1539
|
+
return this.setHeader(name, value);
|
|
1854
1540
|
}
|
|
1855
|
-
const merged =
|
|
1856
|
-
return this.setHeader(name,
|
|
1541
|
+
const merged = /** @type {string[]} */ ([]).concat(current, value);
|
|
1542
|
+
return this.setHeader(name, merged);
|
|
1857
1543
|
}
|
|
1858
1544
|
|
|
1859
1545
|
/**
|
|
@@ -1868,15 +1554,15 @@ module.exports = class Response extends LazyWritable {
|
|
|
1868
1554
|
if (typeof Headers === "function" && headers instanceof Headers) {
|
|
1869
1555
|
for (const name of new Set([...headers.keys()])) {
|
|
1870
1556
|
if (name === "set-cookie") {
|
|
1871
|
-
this.setHeader(name,
|
|
1557
|
+
this.setHeader(name, headers.getSetCookie());
|
|
1872
1558
|
} else {
|
|
1873
|
-
this.setHeader(name, /** @type {
|
|
1559
|
+
this.setHeader(name, /** @type {string} */ (headers.get(name)));
|
|
1874
1560
|
}
|
|
1875
1561
|
}
|
|
1876
1562
|
return this;
|
|
1877
1563
|
}
|
|
1878
1564
|
for (const [name, value] of headers) {
|
|
1879
|
-
this.setHeader(name,
|
|
1565
|
+
this.setHeader(name, value);
|
|
1880
1566
|
}
|
|
1881
1567
|
return this;
|
|
1882
1568
|
}
|
|
@@ -1893,8 +1579,8 @@ module.exports = class Response extends LazyWritable {
|
|
|
1893
1579
|
|
|
1894
1580
|
/**
|
|
1895
1581
|
* The Express name for set(), including the charset it adds to a content-type.
|
|
1896
|
-
* @param {
|
|
1897
|
-
* @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
|
|
1898
1584
|
* @returns {this}
|
|
1899
1585
|
*/
|
|
1900
1586
|
header(field, value) {
|
|
@@ -1986,6 +1672,9 @@ module.exports = class Response extends LazyWritable {
|
|
|
1986
1672
|
* @param {string} field
|
|
1987
1673
|
*/
|
|
1988
1674
|
removeHeader(field) {
|
|
1675
|
+
if (this.headersSent) {
|
|
1676
|
+
throw headersSentError("remove");
|
|
1677
|
+
}
|
|
1989
1678
|
const key = field.toLowerCase();
|
|
1990
1679
|
// the delete is a runtime call, and helmet removes a header most responses never carry
|
|
1991
1680
|
if (key in this.headers) {
|
|
@@ -2019,12 +1708,13 @@ module.exports = class Response extends LazyWritable {
|
|
|
2019
1708
|
* Renders a view and sends it. With a callback the result goes to the callback instead, and
|
|
2020
1709
|
* nothing is sent. A function in the options position is taken as the callback.
|
|
2021
1710
|
* @param {string} view view name
|
|
2022
|
-
* @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
|
|
2023
1713
|
* @param {(err: Error|null, html?: string) => void} [callback]
|
|
2024
1714
|
*/
|
|
2025
1715
|
render(view, options, callback) {
|
|
2026
1716
|
if (typeof options === "function") {
|
|
2027
|
-
callback = /** @type {
|
|
1717
|
+
callback = /** @type {(err: Error|null, html?: string) => void} */ (options);
|
|
2028
1718
|
options = {};
|
|
2029
1719
|
}
|
|
2030
1720
|
if (!options) {
|
|
@@ -2062,7 +1752,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
2062
1752
|
const opt = { ...(options ?? {}) }; // create a new ref because we change original object (https://github.com/dimdenGD/ultimate-express/issues/68)
|
|
2063
1753
|
// cookie-parser hangs the secret on the request, so it is read off it rather than
|
|
2064
1754
|
// declared here: without that middleware there is none, which is what this checks
|
|
2065
|
-
const req = /** @type {
|
|
1755
|
+
const req = /** @type {{secret?: string}} */ (this.req);
|
|
2066
1756
|
if (opt.signed && !req.secret) {
|
|
2067
1757
|
// the message has to read like this: it is the one Express throws, and it names the
|
|
2068
1758
|
// thing that is actually missing rather than the library that noticed
|
|
@@ -2081,7 +1771,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
2081
1771
|
delete opt.maxAge;
|
|
2082
1772
|
}
|
|
2083
1773
|
if (opt.signed) {
|
|
2084
|
-
val = "s:" + sign(val, req.secret);
|
|
1774
|
+
val = "s:" + sign(val, /** @type {string} */ (req.secret));
|
|
2085
1775
|
}
|
|
2086
1776
|
|
|
2087
1777
|
if (opt.path == null) {
|
|
@@ -2126,7 +1816,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
2126
1816
|
* Answers according to the Accept header, calling the handler whose key matches best. A
|
|
2127
1817
|
* `default` key catches everything else; without one an unmatched request gets 406.
|
|
2128
1818
|
* Sets Vary: Accept.
|
|
2129
|
-
* @param {Record<string,
|
|
1819
|
+
* @param {Record<string, Function>} object handlers keyed by extension or mime type
|
|
2130
1820
|
* @returns {this}
|
|
2131
1821
|
*/
|
|
2132
1822
|
format(object) {
|
|
@@ -2164,11 +1854,14 @@ module.exports = class Response extends LazyWritable {
|
|
|
2164
1854
|
* @returns {this}
|
|
2165
1855
|
*/
|
|
2166
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);
|
|
2167
1861
|
if (!this.headers["content-type"]) {
|
|
2168
1862
|
this.headers["content-type"] = JSON_UTF8;
|
|
2169
1863
|
}
|
|
2170
|
-
|
|
2171
|
-
return this.send(stringify(body, hot.jsonReplacer, hot.jsonSpaces, hot.jsonEscape));
|
|
1864
|
+
return this.send(json);
|
|
2172
1865
|
}
|
|
2173
1866
|
|
|
2174
1867
|
/**
|
|
@@ -2220,7 +1913,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
2220
1913
|
|
|
2221
1914
|
/**
|
|
2222
1915
|
* Adds to the Link header, one entry per key, the key being the rel.
|
|
2223
|
-
* @param {Record<string,
|
|
1916
|
+
* @param {Record<string, string>} links rel to url
|
|
2224
1917
|
* @returns {this}
|
|
2225
1918
|
*/
|
|
2226
1919
|
links(links) {
|
|
@@ -2343,7 +2036,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
2343
2036
|
vary(field) {
|
|
2344
2037
|
// the vary package decides: it throws when there is no field at all, and does nothing at
|
|
2345
2038
|
// all for an empty list, which is not the same thing and used to be refused here as well
|
|
2346
|
-
vary(/** @type {
|
|
2039
|
+
vary(/** @type {import("http").ServerResponse} */ (/** @type {unknown} */ (this)), field);
|
|
2347
2040
|
return this;
|
|
2348
2041
|
}
|
|
2349
2042
|
|
|
@@ -2365,10 +2058,9 @@ module.exports = class Response extends LazyWritable {
|
|
|
2365
2058
|
|
|
2366
2059
|
/**
|
|
2367
2060
|
* Whether end() has been called. node sets this one there and writableFinished later, once the
|
|
2368
|
-
* bytes are out; here end() hands the whole response to
|
|
2061
|
+
* bytes are out; here end() hands the whole response to uWS, so the two are the same moment.
|
|
2369
2062
|
* Without it the base property answered false forever, and an application that asks whether it
|
|
2370
|
-
* has already answered
|
|
2371
|
-
* subscription - carried on writing to a response that was over.
|
|
2063
|
+
* has already answered, as LibreChat's agent stream does, kept writing to a dead response.
|
|
2372
2064
|
*/
|
|
2373
2065
|
// @ts-expect-error TS2611, the accessor replacing the base property is deliberate. Expect
|
|
2374
2066
|
// rather than ignore, so it fails loudly if it ever stops applying.
|
|
@@ -2379,4 +2071,5 @@ module.exports = class Response extends LazyWritable {
|
|
|
2379
2071
|
|
|
2380
2072
|
// res.contentType is res.type under express's other name. On the prototype rather than an instance
|
|
2381
2073
|
// field, which wrote one own property per response in the constructor.
|
|
2382
|
-
/** @type {
|
|
2074
|
+
/** @type {{contentType?: typeof module.exports.prototype.type}} */ (module.exports.prototype).contentType =
|
|
2075
|
+
module.exports.prototype.type;
|