ultimate-express 1.4.5 → 1.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/package.json +7 -2
- package/src/application.js +3 -0
- package/src/middlewares.js +1 -1
- package/src/request.js +19 -10
- package/src/response.js +23 -4
- package/src/router.js +31 -28
- package/src/utils.js +3 -2
package/README.md
CHANGED
|
@@ -308,7 +308,7 @@ Almost all middlewares that are compatible with Express are compatible with µEx
|
|
|
308
308
|
- ✅ [body-parser](https://npmjs.com/package/body-parser) (use `express.text()` etc instead for better performance)
|
|
309
309
|
- ✅ [cookie-parser](https://npmjs.com/package/cookie-parser)
|
|
310
310
|
- ✅ [cookie-session](https://npmjs.com/package/cookie-session)
|
|
311
|
-
-
|
|
311
|
+
- ✅ [compression](https://npmjs.com/package/compression)
|
|
312
312
|
- ✅ [serve-static](https://npmjs.com/package/serve-static) (use `express.static()` instead for better performance)
|
|
313
313
|
- ✅ [serve-index](https://npmjs.com/package/serve-index)
|
|
314
314
|
- ✅ [cors](https://npmjs.com/package/cors)
|
|
@@ -323,6 +323,10 @@ Almost all middlewares that are compatible with Express are compatible with µEx
|
|
|
323
323
|
- ✅ [vhost](https://npmjs.com/package/vhost)
|
|
324
324
|
- ✅ [tsoa](https://github.com/lukeautry/tsoa)
|
|
325
325
|
- ✅ [express-mongo-sanitize](https://www.npmjs.com/package/express-mongo-sanitize)
|
|
326
|
+
- ✅ [helmet](https://www.npmjs.com/package/helmet)
|
|
327
|
+
- ✅ [passport](https://www.npmjs.com/package/passport)
|
|
328
|
+
- ✅ [morgan](https://www.npmjs.com/package/morgan)
|
|
329
|
+
- ✅ [swagger-ui-express](https://www.npmjs.com/package/swagger-ui-express)
|
|
326
330
|
|
|
327
331
|
Middlewares and modules that are confirmed to not work:
|
|
328
332
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultimate-express",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.7",
|
|
4
4
|
"description": "The Ultimate Express. Fastest http server with full Express compatibility, based on uWebSockets.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
},
|
|
44
44
|
"homepage": "https://github.com/dimdenGD/ultimate-express#readme",
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@types/express": "^4.
|
|
46
|
+
"@types/express": "^4.17.21",
|
|
47
47
|
"accepts": "^1.3.8",
|
|
48
48
|
"acorn": "^8.14.1",
|
|
49
49
|
"bytes": "^3.1.2",
|
|
@@ -86,16 +86,21 @@
|
|
|
86
86
|
"express-session": "^1.18.0",
|
|
87
87
|
"express-subdomain": "^1.0.6",
|
|
88
88
|
"formdata-node": "^6.0.3",
|
|
89
|
+
"helmet": "^8.1.0",
|
|
89
90
|
"method-override": "^3.0.0",
|
|
91
|
+
"morgan": "^1.10.0",
|
|
90
92
|
"multer": "^1.4.5-lts.1",
|
|
91
93
|
"mustache-express": "^1.3.2",
|
|
92
94
|
"nyc": "^17.1.0",
|
|
93
95
|
"pako": "^2.1.0",
|
|
96
|
+
"passport": "^0.7.0",
|
|
97
|
+
"passport-local": "^1.0.0",
|
|
94
98
|
"pkg-pr-new": "^0.0.29",
|
|
95
99
|
"pug": "^3.0.3",
|
|
96
100
|
"response-time": "^2.3.3",
|
|
97
101
|
"serve-index": "^1.9.1",
|
|
98
102
|
"serve-static": "^1.16.2",
|
|
103
|
+
"swagger-ui-express": "^5.0.1",
|
|
99
104
|
"swig": "^1.4.2",
|
|
100
105
|
"vhost": "^3.0.2"
|
|
101
106
|
}
|
package/src/application.js
CHANGED
|
@@ -188,6 +188,9 @@ class Application extends Router {
|
|
|
188
188
|
|
|
189
189
|
const matchedRoute = await this._routeRequest(request, response);
|
|
190
190
|
if(!matchedRoute && !response.headersSent && !response.aborted) {
|
|
191
|
+
if(request._error) {
|
|
192
|
+
return this._handleError(request._error, null, request, response);
|
|
193
|
+
}
|
|
191
194
|
response.status(404);
|
|
192
195
|
this._sendErrorPage(request, response, `Cannot ${request.method} ${request.path}`, false);
|
|
193
196
|
}
|
package/src/middlewares.js
CHANGED
|
@@ -206,7 +206,7 @@ function createBodyParser(defaultType, beforeReturn) {
|
|
|
206
206
|
|
|
207
207
|
// skip reading body for non-POST requests
|
|
208
208
|
// this makes it +10k req/sec faster
|
|
209
|
-
if(
|
|
209
|
+
if(additionalMethods === undefined) additionalMethods = req.app.get('body methods') ?? null;
|
|
210
210
|
if(
|
|
211
211
|
req.method !== 'POST' &&
|
|
212
212
|
req.method !== 'PUT' &&
|
package/src/request.js
CHANGED
|
@@ -37,6 +37,9 @@ module.exports = class Request extends Readable {
|
|
|
37
37
|
#cachedDistinctHeaders = null;
|
|
38
38
|
#rawHeadersEntries = [];
|
|
39
39
|
#cachedParsedIp = null;
|
|
40
|
+
#needsData = false;
|
|
41
|
+
#doneReadingData = false;
|
|
42
|
+
#bufferedData = null;
|
|
40
43
|
constructor(req, res, app) {
|
|
41
44
|
super();
|
|
42
45
|
this._res = res;
|
|
@@ -72,7 +75,6 @@ module.exports = class Request extends Readable {
|
|
|
72
75
|
this._stack = [];
|
|
73
76
|
this._paramStack = [];
|
|
74
77
|
this.receivedData = false;
|
|
75
|
-
this.doneReadingData = false;
|
|
76
78
|
// reading ip is very slow in UWS, so its better to not do it unless truly needed
|
|
77
79
|
if(this.app.needsIpAfterResponse || this.key < 100) {
|
|
78
80
|
// if app needs ip after response, read it now because after response its not accessible
|
|
@@ -89,18 +91,22 @@ module.exports = class Request extends Readable {
|
|
|
89
91
|
this.method === 'PATCH' ||
|
|
90
92
|
(additionalMethods && additionalMethods.includes(this.method))
|
|
91
93
|
) {
|
|
92
|
-
this
|
|
94
|
+
this.#bufferedData = Buffer.allocUnsafe(0);
|
|
93
95
|
this._res.onData((ab, isLast) => {
|
|
94
96
|
// make stream actually readable
|
|
95
97
|
this.receivedData = true;
|
|
96
98
|
if(isLast) {
|
|
97
|
-
this
|
|
99
|
+
this.#doneReadingData = true;
|
|
98
100
|
}
|
|
99
101
|
// instead of pushing data immediately, buffer it
|
|
100
102
|
// because writable streams cant handle the amount of data uWS gives (usually 512kb+)
|
|
101
103
|
const chunk = Buffer.from(ab);
|
|
102
|
-
this
|
|
103
|
-
|
|
104
|
+
this.#bufferedData = Buffer.concat([this.#bufferedData, chunk]);
|
|
105
|
+
|
|
106
|
+
if(this.#needsData) {
|
|
107
|
+
this.#needsData = false;
|
|
108
|
+
this._read();
|
|
109
|
+
}
|
|
104
110
|
});
|
|
105
111
|
} else {
|
|
106
112
|
this.receivedData = true;
|
|
@@ -108,16 +114,19 @@ module.exports = class Request extends Readable {
|
|
|
108
114
|
}
|
|
109
115
|
|
|
110
116
|
_read() {
|
|
111
|
-
if(!this.receivedData || !this
|
|
117
|
+
if(!this.receivedData || !this.#bufferedData) {
|
|
118
|
+
this.#needsData = true;
|
|
112
119
|
return;
|
|
113
120
|
}
|
|
114
|
-
if(this
|
|
121
|
+
if(this.#bufferedData.length > 0) {
|
|
115
122
|
// push 128kb chunks
|
|
116
|
-
const chunk = this
|
|
117
|
-
this
|
|
123
|
+
const chunk = this.#bufferedData.subarray(0, 1024 * 128);
|
|
124
|
+
this.#bufferedData = this.#bufferedData.subarray(1024 * 128);
|
|
118
125
|
this.push(chunk);
|
|
119
|
-
} else if(this
|
|
126
|
+
} else if(this.#doneReadingData) {
|
|
120
127
|
this.push(null);
|
|
128
|
+
} else {
|
|
129
|
+
this.#needsData = true;
|
|
121
130
|
}
|
|
122
131
|
}
|
|
123
132
|
|
package/src/response.js
CHANGED
|
@@ -38,6 +38,7 @@ const etag = require("etag");
|
|
|
38
38
|
const outgoingMessage = new http.OutgoingMessage();
|
|
39
39
|
const symbols = Object.getOwnPropertySymbols(outgoingMessage);
|
|
40
40
|
const kOutHeaders = symbols.find(s => s.toString() === 'Symbol(kOutHeaders)');
|
|
41
|
+
const HIGH_WATERMARK = 256 * 1024;
|
|
41
42
|
|
|
42
43
|
class Socket extends EventEmitter {
|
|
43
44
|
constructor(response) {
|
|
@@ -68,6 +69,8 @@ class Socket extends EventEmitter {
|
|
|
68
69
|
|
|
69
70
|
module.exports = class Response extends Writable {
|
|
70
71
|
#socket = null;
|
|
72
|
+
#pendingChunks = [];
|
|
73
|
+
#lastWriteChunkTime = 0;
|
|
71
74
|
constructor(res, req, app) {
|
|
72
75
|
super();
|
|
73
76
|
this._req = req;
|
|
@@ -147,7 +150,17 @@ module.exports = class Response extends Writable {
|
|
|
147
150
|
}
|
|
148
151
|
|
|
149
152
|
if (this.chunkedTransfer) {
|
|
150
|
-
this.
|
|
153
|
+
this.#pendingChunks.push(chunk);
|
|
154
|
+
const size = this.#pendingChunks.reduce((acc, chunk) => acc + chunk.byteLength, 0);
|
|
155
|
+
const now = Date.now();
|
|
156
|
+
// the first chunk is set immediately (!this.#lastWriteChunkTime)
|
|
157
|
+
// the other chunks are sent when watermark is reached (size >= HIGH_WATERMARK)
|
|
158
|
+
// or if elapsed 100ms of last send (now - this.#lastWriteChunkTime > 100)
|
|
159
|
+
if (!this.#lastWriteChunkTime || size >= HIGH_WATERMARK || now - this.#lastWriteChunkTime > 100) {
|
|
160
|
+
this._res.write(Buffer.concat(this.#pendingChunks, size));
|
|
161
|
+
this.#pendingChunks = [];
|
|
162
|
+
this.#lastWriteChunkTime = now;
|
|
163
|
+
}
|
|
151
164
|
this.writingChunk = false;
|
|
152
165
|
callback(null);
|
|
153
166
|
} else {
|
|
@@ -261,6 +274,11 @@ module.exports = class Response extends Writable {
|
|
|
261
274
|
if(!data && contentLength) {
|
|
262
275
|
this._res.endWithoutBody(contentLength.toString());
|
|
263
276
|
} else {
|
|
277
|
+
if(this.#pendingChunks.length) {
|
|
278
|
+
this._res.write(Buffer.concat(this.#pendingChunks));
|
|
279
|
+
this.#pendingChunks = [];
|
|
280
|
+
this.lastWriteChunkTime = 0;
|
|
281
|
+
}
|
|
264
282
|
if(data instanceof Buffer) {
|
|
265
283
|
data = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
266
284
|
}
|
|
@@ -271,11 +289,12 @@ module.exports = class Response extends Writable {
|
|
|
271
289
|
this._res.end(data);
|
|
272
290
|
}
|
|
273
291
|
}
|
|
292
|
+
|
|
274
293
|
this.finished = true;
|
|
275
294
|
if(this.socketExists) this.socket.emit('close');
|
|
295
|
+
this.emit('finish');
|
|
296
|
+
this.emit('close');
|
|
276
297
|
});
|
|
277
|
-
|
|
278
|
-
this.emit('finish')
|
|
279
298
|
return this;
|
|
280
299
|
}
|
|
281
300
|
|
|
@@ -498,7 +517,7 @@ module.exports = class Response extends Writable {
|
|
|
498
517
|
} else {
|
|
499
518
|
// larger files or range requests are piped over response
|
|
500
519
|
let opts = {
|
|
501
|
-
highWaterMark:
|
|
520
|
+
highWaterMark: HIGH_WATERMARK
|
|
502
521
|
};
|
|
503
522
|
if(ranged) {
|
|
504
523
|
opts.start = offset;
|
package/src/router.js
CHANGED
|
@@ -46,7 +46,6 @@ module.exports = class Router extends EventEmitter {
|
|
|
46
46
|
this._paramCallbacks = new Map();
|
|
47
47
|
this._mountpathCache = new Map();
|
|
48
48
|
this._routes = [];
|
|
49
|
-
this.errorRoute = undefined;
|
|
50
49
|
this.mountpath = '/';
|
|
51
50
|
this.settings = settings;
|
|
52
51
|
this._request = Request;
|
|
@@ -293,6 +292,9 @@ module.exports = class Router extends EventEmitter {
|
|
|
293
292
|
}
|
|
294
293
|
const matchedRoute = await this._routeRequest(request, response, 0, optimizedPath, true, route);
|
|
295
294
|
if(!matchedRoute && !response.headersSent && !response.aborted) {
|
|
295
|
+
if(request._error) {
|
|
296
|
+
return this._handleError(request._error, null, request, response);
|
|
297
|
+
}
|
|
296
298
|
response.status(404);
|
|
297
299
|
request.noEtag = true;
|
|
298
300
|
this._sendErrorPage(request, response, `Cannot ${request.method} ${request._originalPath}`, false);
|
|
@@ -332,20 +334,12 @@ module.exports = class Router extends EventEmitter {
|
|
|
332
334
|
}
|
|
333
335
|
}
|
|
334
336
|
|
|
335
|
-
_handleError(err, request, response) {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
if(errorRoute) {
|
|
342
|
-
return errorRoute(err, request, response, () => {
|
|
343
|
-
if(!response.headersSent) {
|
|
344
|
-
if(response.statusCode === 200) {
|
|
345
|
-
response.statusCode = 500;
|
|
346
|
-
}
|
|
347
|
-
this._sendErrorPage(request, response, err, true);
|
|
348
|
-
}
|
|
337
|
+
_handleError(err, handler, request, response) {
|
|
338
|
+
if(handler) {
|
|
339
|
+
return handler(err, request, response, () => {
|
|
340
|
+
delete request._error;
|
|
341
|
+
delete request._errorKey;
|
|
342
|
+
return request.next();
|
|
349
343
|
});
|
|
350
344
|
}
|
|
351
345
|
console.error(err);
|
|
@@ -405,8 +399,8 @@ module.exports = class Router extends EventEmitter {
|
|
|
405
399
|
if(thingamabob === 'route') {
|
|
406
400
|
return resolve('route');
|
|
407
401
|
} else {
|
|
408
|
-
|
|
409
|
-
|
|
402
|
+
req._error = thingamabob;
|
|
403
|
+
req._errorKey = route.routeKey;
|
|
410
404
|
}
|
|
411
405
|
}
|
|
412
406
|
return resolveRoute();
|
|
@@ -460,7 +454,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
460
454
|
let callbackindex = 0;
|
|
461
455
|
|
|
462
456
|
// avoid calling _preprocessRequest as async function as its slower
|
|
463
|
-
// but it seems like calling it as async has unintended consequence of resetting max call stack size
|
|
457
|
+
// but it seems like calling it as async has unintended, but useful consequence of resetting max call stack size
|
|
464
458
|
// so call it as async when the request has been through every 300 routes to reset it
|
|
465
459
|
const continueRoute = this._paramCallbacks.size === 0 && req.routeCount % 300 !== 0 ?
|
|
466
460
|
this._preprocessRequest(req, res, route) : await this._preprocessRequest(req, res, route);
|
|
@@ -513,8 +507,8 @@ module.exports = class Router extends EventEmitter {
|
|
|
513
507
|
req.routeCount++;
|
|
514
508
|
return resolve(this._routeRequest(req, res, routeIndex + 1, routes, skipCheck, skipUntil));
|
|
515
509
|
} else {
|
|
516
|
-
|
|
517
|
-
|
|
510
|
+
req._error = thingamabob;
|
|
511
|
+
req._errorKey = route.routeKey;
|
|
518
512
|
}
|
|
519
513
|
}
|
|
520
514
|
const callback = route.callbacks[callbackindex++];
|
|
@@ -535,6 +529,15 @@ module.exports = class Router extends EventEmitter {
|
|
|
535
529
|
if(routed) return resolve(true);
|
|
536
530
|
next();
|
|
537
531
|
} else {
|
|
532
|
+
// handle errors and error handlers
|
|
533
|
+
if(req._error || callback.length === 4) {
|
|
534
|
+
if(req._error && callback.length === 4 && route.routeKey >= req._errorKey) {
|
|
535
|
+
return this._handleError(req._error, callback, req, res);
|
|
536
|
+
} else {
|
|
537
|
+
return next();
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
538
541
|
try {
|
|
539
542
|
// skipping routes we already went through via optimized path
|
|
540
543
|
if(!skipCheck && skipUntil && skipUntil.routeKey >= route.routeKey) {
|
|
@@ -544,16 +547,18 @@ module.exports = class Router extends EventEmitter {
|
|
|
544
547
|
if(out instanceof Promise) {
|
|
545
548
|
out.catch(err => {
|
|
546
549
|
if(this.get("catch async errors")) {
|
|
547
|
-
|
|
548
|
-
|
|
550
|
+
req._error = err;
|
|
551
|
+
req._errorKey = route.routeKey;
|
|
552
|
+
return next();
|
|
549
553
|
} else {
|
|
550
554
|
throw err;
|
|
551
555
|
}
|
|
552
556
|
});
|
|
553
557
|
}
|
|
554
558
|
} catch(err) {
|
|
555
|
-
|
|
556
|
-
|
|
559
|
+
req._error = err;
|
|
560
|
+
req._errorKey = route.routeKey;
|
|
561
|
+
return next();
|
|
557
562
|
}
|
|
558
563
|
}
|
|
559
564
|
}
|
|
@@ -570,16 +575,14 @@ module.exports = class Router extends EventEmitter {
|
|
|
570
575
|
|
|
571
576
|
use(path, ...callbacks) {
|
|
572
577
|
if(typeof path === 'function' || path instanceof Router || (Array.isArray(path) && path.every(p => typeof p === 'function' || p instanceof Router))) {
|
|
573
|
-
if(callbacks.length === 0 && typeof path === 'function' && path.length === 4) {
|
|
574
|
-
this.errorRoute = path;
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
578
|
callbacks.unshift(path);
|
|
578
579
|
path = '';
|
|
579
580
|
}
|
|
580
581
|
if(path === '/') {
|
|
581
582
|
path = '';
|
|
582
583
|
}
|
|
584
|
+
callbacks = callbacks.flat();
|
|
585
|
+
|
|
583
586
|
for(let callback of callbacks) {
|
|
584
587
|
if(callback instanceof Router) {
|
|
585
588
|
callback.mountpath = path;
|
package/src/utils.js
CHANGED
|
@@ -55,8 +55,9 @@ function patternToRegex(pattern, isPrefix = false) {
|
|
|
55
55
|
.replaceAll('.', '\\.')
|
|
56
56
|
.replaceAll('-', '\\-')
|
|
57
57
|
.replaceAll('*', '(.*)') // Convert * to .*
|
|
58
|
-
.replace(
|
|
59
|
-
|
|
58
|
+
.replace(/\/:(\w+)(\(.+?\))?\??/g, (match, param, regex) => {
|
|
59
|
+
const optional = match.endsWith('?');
|
|
60
|
+
return `\\/${optional ? '?' : ''}?(?<${param}>${regex ? regex + '($|\\/)' : '[^/]+'})${optional ? '?' : ''}`;
|
|
60
61
|
}); // Convert :param to capture group
|
|
61
62
|
|
|
62
63
|
return new RegExp(`^${regexPattern}${isPrefix ? '(?=$|\/)' : '$'}`);
|