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 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
- - 🚧 [compression](https://npmjs.com/package/compression) - in some cases may send uncompressed files
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.5",
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.0.0",
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
  }
@@ -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
  }
@@ -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( additionalMethods === undefined ) additionalMethods = req.app.get('body methods') ?? null;
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.bufferedData = Buffer.allocUnsafe(0);
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.doneReadingData = true;
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.bufferedData = Buffer.concat([this.bufferedData, chunk]);
103
- this._read();
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.bufferedData) {
117
+ if(!this.receivedData || !this.#bufferedData) {
118
+ this.#needsData = true;
112
119
  return;
113
120
  }
114
- if(this.bufferedData.length > 0) {
121
+ if(this.#bufferedData.length > 0) {
115
122
  // push 128kb chunks
116
- const chunk = this.bufferedData.subarray(0, 1024 * 128);
117
- this.bufferedData = this.bufferedData.subarray(1024 * 128);
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.doneReadingData) {
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._res.write(chunk);
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: 256 * 1024
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
- let errorRoute = this.errorRoute, parent = this.parent;
337
- while(!errorRoute && parent) {
338
- errorRoute = parent.errorRoute;
339
- parent = parent.parent;
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
- this._handleError(thingamabob, req, res);
409
- return resolve(false);
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
- this._handleError(thingamabob, req, res);
517
- return resolve(true);
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
- this._handleError(err, req, res);
548
- return resolve(true);
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
- this._handleError(err, req, res);
556
- return resolve(true);
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(/:(\w+)(\(.+?\))?/g, (match, param, regex) => {
59
- return `(?<${param}>${regex ? regex + '($|\\/)' : '[^/]+'})`;
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 ? '(?=$|\/)' : '$'}`);