fulmine.js 5.19.2 → 5.19.3
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 +46 -45
- package/src/cli.js +28 -34
- package/src/cluster.js +16 -25
- package/src/compression.js +39 -51
- package/src/declarative.js +586 -538
- package/src/hot-settings.js +80 -0
- package/src/index.js +11 -17
- package/src/lazy-readable.js +129 -0
- package/src/lazy-writable.js +97 -0
- package/src/middlewares.js +61 -77
- package/src/nest.js +19 -34
- package/src/node-shim.js +11 -13
- package/src/optimizer.js +598 -0
- package/src/parse-query.js +3 -3
- package/src/request-utils.js +306 -0
- package/src/request.js +101 -513
- package/src/response-utils.js +88 -0
- package/src/response.js +100 -443
- package/src/route.js +4 -5
- package/src/router-utils.js +950 -0
- package/src/router.js +126 -2148
- package/src/server-shape.js +26 -41
- package/src/server-timing.js +16 -29
- package/src/socket.js +208 -0
- package/src/testing.js +39 -42
- package/src/usage.js +16 -21
- package/src/utils.js +49 -59
- package/src/verify.js +18 -28
- package/src/view.js +5 -7
- package/src/walk.js +580 -0
- package/src/websocket.js +19 -20
- package/src/work.js +21 -27
package/src/response.js
CHANGED
|
@@ -43,17 +43,22 @@ const {
|
|
|
43
43
|
cachedStat,
|
|
44
44
|
NullObject
|
|
45
45
|
} = require("./utils.js");
|
|
46
|
-
const { Writable } = require("stream");
|
|
47
46
|
const { isAbsolute } = require("path");
|
|
48
47
|
const fs = require("fs");
|
|
49
48
|
const Path = require("path");
|
|
50
49
|
const statuses = require("statuses");
|
|
51
50
|
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
51
|
const ms = require("ms");
|
|
52
|
+
const Socket = require("./socket.js");
|
|
53
|
+
const { LazyWritable } = require("./lazy-writable.js");
|
|
54
|
+
const {
|
|
55
|
+
kOutHeaders,
|
|
56
|
+
kShapeMode,
|
|
57
|
+
VALIDATED_HEADER_NAMES,
|
|
58
|
+
HEADER_NAME_BUF,
|
|
59
|
+
HEADER_VALUE_BUF,
|
|
60
|
+
statusLine
|
|
61
|
+
} = require("./response-utils.js");
|
|
57
62
|
|
|
58
63
|
// How much a chunked response may gather before it is handed to uWS. Measured on uWS alone with
|
|
59
64
|
// 33 KB written in 500 pieces: 16.2ms handed over one piece at a time, 0.79ms in 4 KB blocks,
|
|
@@ -67,48 +72,6 @@ const COALESCE_LIMIT = 16 * 1024;
|
|
|
67
72
|
// here up goes straight through.
|
|
68
73
|
const COALESCE_BELOW = 4 * 1024;
|
|
69
74
|
|
|
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
75
|
const HIGH_WATERMARK = 128 * 1024;
|
|
113
76
|
// the exact string json() writes, so send() can skip recomputing the charset on it
|
|
114
77
|
const JSON_UTF8 = "application/json; charset=utf-8";
|
|
@@ -116,284 +79,6 @@ const JSON_UTF8 = "application/json; charset=utf-8";
|
|
|
116
79
|
// than written out, since a year is already longer than any cache will honour.
|
|
117
80
|
const MAX_MAXAGE = 60 * 60 * 24 * 365 * 1000;
|
|
118
81
|
|
|
119
|
-
class Socket extends EventEmitter {
|
|
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
|
-
}
|
|
396
|
-
|
|
397
82
|
module.exports = class Response extends LazyWritable {
|
|
398
83
|
/** @type {Socket|null} */
|
|
399
84
|
#socket = null;
|
|
@@ -409,10 +94,8 @@ module.exports = class Response extends LazyWritable {
|
|
|
409
94
|
*
|
|
410
95
|
* uWS charges for a write against everything already buffered behind it, so a chunked response
|
|
411
96
|
* 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.
|
|
97
|
+
* measured on uWS alone, 500 writes of 66 bytes take 13ms against 0.06ms for 100 of them, and
|
|
98
|
+
* the same bytes in blocks cost 0.4ms. Null until the first chunked write.
|
|
416
99
|
* @type {Buffer[]|null}
|
|
417
100
|
*/
|
|
418
101
|
#queued = null;
|
|
@@ -426,6 +109,13 @@ module.exports = class Response extends LazyWritable {
|
|
|
426
109
|
/** @type {any} */
|
|
427
110
|
#outHeaders = null;
|
|
428
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Whether node's writeHead has run, which only the per-app subclass sets. _sendOptionsReply
|
|
114
|
+
* refuses to write a second head over it, as node's setHeader does.
|
|
115
|
+
* @type {boolean|undefined}
|
|
116
|
+
*/
|
|
117
|
+
_headWritten;
|
|
118
|
+
|
|
429
119
|
/**
|
|
430
120
|
* The request this response answers, linked so either reaches the other.
|
|
431
121
|
* @type {InstanceType<typeof import("./request.js")>}
|
|
@@ -438,7 +128,8 @@ module.exports = class Response extends LazyWritable {
|
|
|
438
128
|
* when the setting asks for it.
|
|
439
129
|
*
|
|
440
130
|
* @param {any} res the uWS response
|
|
441
|
-
* @param {any} req the Request, already built
|
|
131
|
+
* @param {any} req the Request, already built. Loose because the per-app subclass in
|
|
132
|
+
* application.js inherits this constructor and its own shape has to stay assignable
|
|
442
133
|
* @param {any} app the application or router this request arrived at
|
|
443
134
|
*/
|
|
444
135
|
constructor(res, req, app) {
|
|
@@ -505,20 +196,17 @@ module.exports = class Response extends LazyWritable {
|
|
|
505
196
|
// false while the uWS route handler is still in its synchronous window, where uWS holds
|
|
506
197
|
// the socket corked itself; the two uWS entry points flip it once that window closes
|
|
507
198
|
this._corkNeeded = false;
|
|
508
|
-
// shared methods, not arrows: two closures and a once() wrapper here were four
|
|
509
|
-
//
|
|
199
|
+
// shared methods, not arrows: two closures and a once() wrapper here were four allocations
|
|
200
|
+
// per request. EventEmitter calls listeners with this = the emitter.
|
|
510
201
|
//
|
|
511
202
|
// Written into the map rather than through on(). A stream arrives with its _events already
|
|
512
203
|
// 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.
|
|
204
|
+
// them is the same hidden class on() would produce and none of its work. Two calls per
|
|
205
|
+
// response, and the profile put them at 6% of a hello-world.
|
|
516
206
|
//
|
|
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
|
|
207
|
+
// The condition is the safety of it: a fresh response has no listeners, so both slots are
|
|
208
|
+
// free, and anything else falls back to on(), which finds what this wrote.
|
|
209
|
+
// cast because _events and _eventsCount are EventEmitter's own bookkeeping and have no type
|
|
522
210
|
const self = /** @type {any} */ (this);
|
|
523
211
|
const events = self._events;
|
|
524
212
|
if (
|
|
@@ -556,17 +244,15 @@ module.exports = class Response extends LazyWritable {
|
|
|
556
244
|
/**
|
|
557
245
|
* Drops the connection, which is how an application abandons a response it cannot finish: a
|
|
558
246
|
* 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())`.
|
|
247
|
+
* truncated body. node destroys the socket here and uWS's close() is the same thing; without it
|
|
248
|
+
* the client waited for bytes that never came. LibreChat's download route is written that way,
|
|
249
|
+
* `stream.on("error", () => res.destroy())`.
|
|
563
250
|
*
|
|
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.
|
|
251
|
+
* A response that is over, or one whose client is gone, only tears the stream down: touching an
|
|
252
|
+
* aborted uWS response is a use after free. writableEnded reads true after this, where node
|
|
253
|
+
* leaves it false until end() is called.
|
|
568
254
|
*
|
|
569
|
-
* @param {any} [error]
|
|
255
|
+
* @param {any} [error] whatever the caller is destroying the response with
|
|
570
256
|
* @returns {this}
|
|
571
257
|
*/
|
|
572
258
|
destroy(error) {
|
|
@@ -613,12 +299,9 @@ module.exports = class Response extends LazyWritable {
|
|
|
613
299
|
|
|
614
300
|
/**
|
|
615
301
|
* 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.
|
|
302
|
+
* own header path looks, cookie-session being the one in this project's tests, so the proxy
|
|
303
|
+
* standing in for it is built on the first look: it was a proxy, a handler object and two
|
|
304
|
+
* closures per response otherwise. A setter too, because node assigns to this slot on a reset.
|
|
622
305
|
*/
|
|
623
306
|
get [kOutHeaders]() {
|
|
624
307
|
if (!this.#outHeaders) {
|
|
@@ -712,7 +395,8 @@ module.exports = class Response extends LazyWritable {
|
|
|
712
395
|
* The booked flush. Static, so a response that never writes in pieces allocates nothing for it:
|
|
713
396
|
* nextTick forwards the receiver as an argument.
|
|
714
397
|
*
|
|
715
|
-
* @param {any} res
|
|
398
|
+
* @param {any} res the response whose queue is being flushed. Loose because naming the class
|
|
399
|
+
* inside its own body makes the checker see two unrelated `this` types
|
|
716
400
|
*/
|
|
717
401
|
static #flushOnTick(res) {
|
|
718
402
|
res.#flushBooked = false;
|
|
@@ -726,7 +410,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
726
410
|
* said how much there would be. Backpressure comes back as onWritable, which is what defers the
|
|
727
411
|
* callback rather than dropping the chunk.
|
|
728
412
|
*
|
|
729
|
-
* @param {any} chunk
|
|
413
|
+
* @param {any} chunk whatever a Writable was handed, which node does not narrow
|
|
730
414
|
* @param {BufferEncoding} encoding
|
|
731
415
|
* @param {(err?: Error|null) => void} callback
|
|
732
416
|
*/
|
|
@@ -819,16 +503,13 @@ module.exports = class Response extends LazyWritable {
|
|
|
819
503
|
|
|
820
504
|
/**
|
|
821
505
|
* 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
|
-
* Nothing is written here despite the name: the headers go out when the body does.
|
|
506
|
+
* is either the status message or the headers, since node allows both shapes. Nothing is
|
|
507
|
+
* written here despite the name: the headers go out when the body does.
|
|
825
508
|
*
|
|
826
509
|
* 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.
|
|
510
|
+
* Express's: a content-type given here keeps the value it was given, where res.set would append
|
|
511
|
+
* a charset. Every meta-framework on Express answers with writeHead, @astrojs/node and
|
|
512
|
+
* @sveltejs/adapter-node included, so the charset was added to pages nobody asked it for.
|
|
832
513
|
*
|
|
833
514
|
* @param {number} statusCode
|
|
834
515
|
* @param {string|Record<string, any>|any[]} [statusMessage] the reason phrase, or the headers
|
|
@@ -867,13 +548,12 @@ module.exports = class Response extends LazyWritable {
|
|
|
867
548
|
}
|
|
868
549
|
|
|
869
550
|
/**
|
|
870
|
-
* Writes every header set so far to uWS, which is the point of no return. Content-Length is
|
|
871
|
-
*
|
|
872
|
-
*
|
|
551
|
+
* Writes every header set so far to uWS, which is the point of no return. Content-Length is not
|
|
552
|
+
* one of them: uWS wants the length through tryEnd or endWithoutBody, so it is taken out here
|
|
553
|
+
* and kept on totalSize, where it also turns chunked framing off.
|
|
873
554
|
*
|
|
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.
|
|
555
|
+
* One writeHeader per header on purpose. Packing the whole head into a single writeStatus works
|
|
556
|
+
* on the wire and measured slower: constant header strings cross already flat, see issue #11.
|
|
877
557
|
*
|
|
878
558
|
* @param {boolean} utf8 unused, kept because node's equivalent takes it and the two callers
|
|
879
559
|
* differ on what they know about the body
|
|
@@ -890,12 +570,10 @@ module.exports = class Response extends LazyWritable {
|
|
|
890
570
|
// the value here is nearly always the 10-char "keep-alive", which paid a scan per response
|
|
891
571
|
const closing =
|
|
892
572
|
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
|
|
573
|
+
// for..in over an object some responses delete from, the shape the request side was taken
|
|
574
|
+
// off for #rawHeadersEntries. It stays here because the deletes are rare: only 204, 304,
|
|
575
|
+
// 205, the freshness branch of sendFile and removeHeader do it, and this object is built
|
|
576
|
+
// fresh per response. The request side deleted on every request
|
|
899
577
|
for (const header in headers) {
|
|
900
578
|
if (closing && header === "keep-alive") {
|
|
901
579
|
continue;
|
|
@@ -967,7 +645,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
967
645
|
}
|
|
968
646
|
|
|
969
647
|
/**
|
|
970
|
-
* @param {any} [data]
|
|
648
|
+
* @param {any} [data] the last body piece, or the callback in node's two-argument shape
|
|
971
649
|
* @param {any} [cb]
|
|
972
650
|
* @returns {this}
|
|
973
651
|
*/
|
|
@@ -1005,7 +683,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
1005
683
|
* The corked tail of end(): status, headers, body and the finish events. Split out so a
|
|
1006
684
|
* synchronous answer calls it straight, already inside uWS's own cork.
|
|
1007
685
|
*
|
|
1008
|
-
* @param {any} data
|
|
686
|
+
* @param {any} data the last body piece
|
|
1009
687
|
* @param {any} cb
|
|
1010
688
|
*/
|
|
1011
689
|
_finish(data, cb) {
|
|
@@ -1014,13 +692,12 @@ module.exports = class Response extends LazyWritable {
|
|
|
1014
692
|
// res.write(), not whether this call is about to write the head itself
|
|
1015
693
|
const headWasAlreadyOut = this.headersSent;
|
|
1016
694
|
if (!this.headersSent) {
|
|
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
|
-
// "HTTP/1.1 200 OK" head on its own, and the crossing costs more than it says
|
|
695
|
+
// freshness is not decided here. node's end() knows nothing about conditional requests,
|
|
696
|
+
// and Express answers 304 from send() and from sendFile(), each of which strips the
|
|
697
|
+
// entity headers first. Deciding it here made res.end("body") answer 304 and drop the
|
|
698
|
+
// body the caller had just written.
|
|
699
|
+
// "unknown" for a code without a message, as node's status line has it. The default 200
|
|
700
|
+
// with no phrase is not written at all: uWS emits the identical head on its own
|
|
1024
701
|
if (this.statusCode !== 200 || this.statusText !== undefined) {
|
|
1025
702
|
this._res.writeStatus(statusLine(this.statusCode, this.statusText));
|
|
1026
703
|
}
|
|
@@ -1028,15 +705,12 @@ module.exports = class Response extends LazyWritable {
|
|
|
1028
705
|
}
|
|
1029
706
|
const contentLength = this.headers["content-length"];
|
|
1030
707
|
// 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.
|
|
708
|
+
// uWS closes by itself for a bare "close" and not for a list, so "keep-alive, close" left
|
|
709
|
+
// the socket open and the bytes after that request were read as another one. See saysClose.
|
|
1035
710
|
//
|
|
1036
|
-
// Only where a length goes out with it
|
|
711
|
+
// Only where a length goes out with it: endWithoutBody takes the flag as its second
|
|
1037
712
|
// 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.
|
|
713
|
+
// without one writes "Content-Length: 9223372036854775808" onto a 204.
|
|
1040
714
|
const closeConnection = this.req._connectionClose === true;
|
|
1041
715
|
// 204 and 304 carry no body, so no Content-Length may describe one either; 1xx is the
|
|
1042
716
|
// third case, by range
|
|
@@ -1051,11 +725,9 @@ module.exports = class Response extends LazyWritable {
|
|
|
1051
725
|
// whatever is still queued goes first: end() must not overtake the body written before it
|
|
1052
726
|
this.#flushQueued(null);
|
|
1053
727
|
// 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.
|
|
728
|
+
// first res.write() both do, so this response is committed to chunked framing. node is
|
|
729
|
+
// committed the same way: after a flush, res.end("body") sends a chunk. Handing the
|
|
730
|
+
// body to uWS's end() here would append a length to a head that already said otherwise
|
|
1059
731
|
if (data) {
|
|
1060
732
|
this._res.write(data);
|
|
1061
733
|
this._sentBody = data;
|
|
@@ -1157,13 +829,10 @@ module.exports = class Response extends LazyWritable {
|
|
|
1157
829
|
}
|
|
1158
830
|
// the ETag belongs here rather than in end(): node's end() does not produce one, so
|
|
1159
831
|
// 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.
|
|
832
|
+
// req.fresh, which compares If-None-Match against it. body is defined by the time it gets
|
|
833
|
+
// here, so an empty one still earns an ETag; testing truthiness left send("") and
|
|
834
|
+
// send(null) without one. Every method by default, not only GET and HEAD: express's own
|
|
835
|
+
// suite has "should send ETag in response to <METHOD> request" per method, see issue #10
|
|
1167
836
|
const hot = this.app._hot();
|
|
1168
837
|
const etagFn = hot.etagFn;
|
|
1169
838
|
if (
|
|
@@ -1225,14 +894,11 @@ module.exports = class Response extends LazyWritable {
|
|
|
1225
894
|
options = new NullObject();
|
|
1226
895
|
}
|
|
1227
896
|
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
|
|
897
|
+
// the callback is optional: without one, errors go to next(). Express's completion handler
|
|
898
|
+
// exactly: a callback hears everything and the response is left alone, so it can still
|
|
899
|
+
// answer 200 after a 404 error. Without one, a directory falls through as a plain next().
|
|
900
|
+
// The router's next and not the route's: express reports a file it could not serve past the
|
|
901
|
+
// rest of the route, so a four argument handler inside the route never sees it
|
|
1236
902
|
const next = this.req._leaveRoute ?? this.req.next;
|
|
1237
903
|
const done = /** @type {(err?: any) => void} */ (
|
|
1238
904
|
(err) => {
|
|
@@ -1242,14 +908,11 @@ module.exports = class Response extends LazyWritable {
|
|
|
1242
908
|
}
|
|
1243
909
|
);
|
|
1244
910
|
// 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".
|
|
911
|
+
// Normalised the way send does: max-age takes a non-negative integer count of seconds, so
|
|
912
|
+
// 0.5, -1 and Infinity are all invalid, and a client that cannot read the directive may
|
|
913
|
+
// throw away the whole Cache-Control header. Number() around the lot and not only around
|
|
914
|
+
// the branch that is already a number: ms() answers undefined for a duration it cannot
|
|
915
|
+
// read, and Number.isNaN(undefined) is false, so an unreadable string reached it as NaN
|
|
1253
916
|
const maxAge = Number(
|
|
1254
917
|
typeof options.maxAge === "string" ? ms(/** @type {any} */ (options.maxAge)) : options.maxAge
|
|
1255
918
|
);
|
|
@@ -1642,14 +1305,13 @@ module.exports = class Response extends LazyWritable {
|
|
|
1642
1305
|
}
|
|
1643
1306
|
|
|
1644
1307
|
/**
|
|
1645
|
-
* Throws away any header that could not be written, so
|
|
1646
|
-
*
|
|
1308
|
+
* Throws away any header that could not be written, so flushing this response cannot fail on
|
|
1309
|
+
* one. setHeader refuses these on the way in, but `res.headers` is the live object, so an
|
|
1647
1310
|
* assignment into that still gets a value in here.
|
|
1648
1311
|
*
|
|
1649
1312
|
* 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.
|
|
1313
|
+
* page is what runs after a throw, so nobody is left to catch it. Everything writable is left
|
|
1314
|
+
* alone, since a middleware's own headers belong on the error response too.
|
|
1653
1315
|
*
|
|
1654
1316
|
* @returns {void}
|
|
1655
1317
|
*/
|
|
@@ -1663,17 +1325,15 @@ module.exports = class Response extends LazyWritable {
|
|
|
1663
1325
|
|
|
1664
1326
|
/**
|
|
1665
1327
|
* 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.
|
|
1328
|
+
* flushHeaders(). `@angular/ssr`'s writeResponseToNodeResponse calls it before streaming a page.
|
|
1669
1329
|
*
|
|
1670
1330
|
* **The head does not reach the wire here.** uWS holds it until the first body chunk, so a
|
|
1671
1331
|
* client sees nothing until then, where express answers at once. `beginWrite` is uWS's API for
|
|
1672
1332
|
* 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
|
|
1333
|
+
* rejects as HPE_INVALID_CHUNK_SIZE. Checked against v20.69.0.
|
|
1674
1334
|
*
|
|
1675
1335
|
* A second call does nothing, as node's does. Nothing is written for a response already
|
|
1676
|
-
* finished or aborted
|
|
1336
|
+
* finished or aborted.
|
|
1677
1337
|
*
|
|
1678
1338
|
* @returns {void}
|
|
1679
1339
|
*/
|
|
@@ -1695,13 +1355,11 @@ module.exports = class Response extends LazyWritable {
|
|
|
1695
1355
|
}
|
|
1696
1356
|
|
|
1697
1357
|
/**
|
|
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.
|
|
1358
|
+
* node's `writeEarlyHints`, which sends a `103` carrying the resources the page will want.
|
|
1700
1359
|
*
|
|
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.
|
|
1360
|
+
* **Nothing is sent here.** uWebSockets.js has no API for an informational response, so this
|
|
1361
|
+
* exists only so code written for Express keeps running instead of dying on
|
|
1362
|
+
* "res.writeEarlyHints is not a function". The callback is still called, as node calls it.
|
|
1705
1363
|
*
|
|
1706
1364
|
* `writeContinue` and `writeProcessing` below are the same story with `100` and `102`.
|
|
1707
1365
|
*
|
|
@@ -1781,7 +1439,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
1781
1439
|
* node's `assignSocket`, which the http server uses when a response is
|
|
1782
1440
|
* handed a raw socket. There is no such socket here.
|
|
1783
1441
|
*
|
|
1784
|
-
* @param {any} [socket]
|
|
1442
|
+
* @param {any} [socket] node takes one here and there is none to take
|
|
1785
1443
|
* @returns {void}
|
|
1786
1444
|
*/
|
|
1787
1445
|
assignSocket(socket) {}
|
|
@@ -1789,7 +1447,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
1789
1447
|
/**
|
|
1790
1448
|
* node's `detachSocket`, which the http server uses when a response is
|
|
1791
1449
|
* handed a raw socket. There is no such socket here.
|
|
1792
|
-
* @param {any} [socket]
|
|
1450
|
+
* @param {any} [socket] node takes one here and there is none to take
|
|
1793
1451
|
* @returns {void}
|
|
1794
1452
|
*/
|
|
1795
1453
|
detachSocket(socket) {}
|
|
@@ -1894,7 +1552,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
1894
1552
|
/**
|
|
1895
1553
|
* The Express name for set(), including the charset it adds to a content-type.
|
|
1896
1554
|
* @param {any} field a header name, or an object of them
|
|
1897
|
-
* @param {any} [value]
|
|
1555
|
+
* @param {any} [value] the header value, or nothing when the first argument is an object
|
|
1898
1556
|
* @returns {this}
|
|
1899
1557
|
*/
|
|
1900
1558
|
header(field, value) {
|
|
@@ -2365,10 +2023,9 @@ module.exports = class Response extends LazyWritable {
|
|
|
2365
2023
|
|
|
2366
2024
|
/**
|
|
2367
2025
|
* 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
|
|
2026
|
+
* bytes are out; here end() hands the whole response to uWS, so the two are the same moment.
|
|
2369
2027
|
* 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.
|
|
2028
|
+
* has already answered, as LibreChat's agent stream does, kept writing to a dead response.
|
|
2372
2029
|
*/
|
|
2373
2030
|
// @ts-expect-error TS2611, the accessor replacing the base property is deliberate. Expect
|
|
2374
2031
|
// rather than ignore, so it fails loudly if it ever stops applying.
|