ultimate-express 1.0.9 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "ultimate-express",
3
- "version": "1.0.9",
3
+ "version": "1.1.1",
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": {
7
7
  "test": "node tests/index.js"
8
8
  },
9
+ "engines": {
10
+ "node": ">=16"
11
+ },
9
12
  "files": [
10
13
  "src",
11
14
  "EXPRESS_LICENSE"
@@ -65,7 +65,7 @@ class Application extends Router {
65
65
  for(const key in defaultSettings) {
66
66
  if(typeof this.settings[key] === 'undefined') {
67
67
  if(typeof defaultSettings[key] === 'function') {
68
- this.settings[key] = defaultSettings[key]();
68
+ this.settings[key] = defaultSettings[key](this);
69
69
  } else {
70
70
  this.settings[key] = defaultSettings[key];
71
71
  }
@@ -75,19 +75,23 @@ class Application extends Router {
75
75
  this.set('views', path.resolve('views'));
76
76
  }
77
77
 
78
+ createWorkerTask(resolve, reject) {
79
+ const key = taskKey++;
80
+ workerTasks[key] = { resolve, reject };
81
+ if(key > 1000000) {
82
+ taskKey = 0;
83
+ }
84
+ return key;
85
+ }
86
+
78
87
  readFileWithWorker(path) {
79
88
  return new Promise((resolve, reject) => {
80
89
  const worker = this.workers[Math.floor(Math.random() * this.workers.length)];
81
- const key = taskKey++;
90
+ const key = this.createWorkerTask(resolve, reject);
82
91
  worker.postMessage({ key, type: 'readFile', path });
83
- workerTasks[key] = { resolve, reject };
84
- if(key > 1000000) {
85
- taskKey = 0;
86
- }
87
92
  });
88
93
  }
89
94
 
90
-
91
95
  set(key, value) {
92
96
  if(key === 'trust proxy') {
93
97
  if(!value) {
@@ -17,6 +17,7 @@ function static(root, options) {
17
17
  options.extensions = options.extensions.map(ext => ext.startsWith('.') ? ext.slice(1) : ext);
18
18
  }
19
19
  options.root = root;
20
+ options.skipEncodePath = true;
20
21
 
21
22
  return (req, res, next) => {
22
23
  const iq = req.url.indexOf('?');
package/src/response.js CHANGED
@@ -96,7 +96,7 @@ module.exports = class Response extends Writable {
96
96
  this._res.writeHeader(header, this.headers[header]);
97
97
  }
98
98
  if(!this.headers['content-type']) {
99
- this._res.writeHeader('content-type', 'text/html');
99
+ this._res.writeHeader('content-type', 'text/html' + (typeof chunk === 'string' ? `; charset=utf-8` : ''));
100
100
  }
101
101
  this.headersSent = true;
102
102
  }
@@ -139,7 +139,7 @@ module.exports = class Response extends Writable {
139
139
  if(!this.headersSent) {
140
140
  const etagFn = this.app.get('etag fn');
141
141
  if(data && !this.headers['etag'] && etagFn && !this.req.noEtag) {
142
- this.set('etag', etagFn(data, this.req));
142
+ this.set('etag', etagFn(data));
143
143
  }
144
144
  if(this.req.fresh) {
145
145
  if(!this.headersSent) {
@@ -157,13 +157,16 @@ module.exports = class Response extends Writable {
157
157
  this._res.writeHeader(header, this.headers[header]);
158
158
  }
159
159
  if(!this.headers['content-type']) {
160
- this._res.writeHeader('content-type', 'text/html');
160
+ this._res.writeHeader('content-type', 'text/html' + (typeof data === 'string' ? `; charset=utf-8` : ''));
161
161
  }
162
162
  this.headersSent = true;
163
163
  }
164
164
  if(!data && this.headers['content-length']) {
165
165
  this._res.endWithoutBody(this.headers['content-length'].toString());
166
166
  } else {
167
+ if(data instanceof Buffer) {
168
+ data = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
169
+ }
167
170
  if(this.req.method === 'HEAD') {
168
171
  const length = Buffer.byteLength(data ?? '');
169
172
  this._res.endWithoutBody(length.toString());
@@ -180,14 +183,10 @@ module.exports = class Response extends Writable {
180
183
  if(this.headersSent) {
181
184
  throw new Error('Can\'t write body: Response was already sent');
182
185
  }
183
- if(Buffer.isBuffer(body)) {
184
- body = body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength);
185
- } else if(body === null || body === undefined) {
186
+ if(body === null || body === undefined) {
186
187
  body = '';
187
- } else if(typeof body === 'object') {
188
- if(!(body instanceof ArrayBuffer)) {
189
- return this.json(body);
190
- }
188
+ } else if(typeof body === 'object' && !Buffer.isBuffer(body)) {
189
+ return this.json(body);
191
190
  } else if(typeof body === 'number') {
192
191
  if(arguments[1]) {
193
192
  deprecated('res.send(status, body)', 'res.status(status).send(body)');
@@ -199,6 +198,12 @@ module.exports = class Response extends Writable {
199
198
  } else {
200
199
  body = String(body);
201
200
  }
201
+ if(typeof body === 'string') {
202
+ const contentType = this.headers['content-type'];
203
+ if(contentType && !contentType.includes(';')) {
204
+ this.headers['content-type'] += '; charset=utf-8';
205
+ }
206
+ }
202
207
  this.writeHead(this.statusCode);
203
208
  return this.end(body);
204
209
  }
@@ -244,6 +249,9 @@ module.exports = class Response extends Writable {
244
249
  this.status(500);
245
250
  return done(new Error('path must be absolute or specify root to res.sendFile'));
246
251
  }
252
+ if(!options.skipEncodePath) {
253
+ path = encodeURI(path);
254
+ }
247
255
  path = decode(path);
248
256
  if(path === -1) {
249
257
  this.status(400);
@@ -349,27 +357,30 @@ module.exports = class Response extends Writable {
349
357
 
350
358
  // range requests
351
359
  let offset = 0, len = stat.size, ranged = false;
352
- if(options.acceptRanges && this.req.headers.range) {
353
- let ranges = this.req.range(stat.size, {
354
- combine: true
355
- });
356
-
357
- // if-range
358
- if(!isRangeFresh(this.req, this)) {
359
- ranges = -2;
360
- }
361
-
362
- if(ranges === -1) {
363
- this.status(416);
364
- this.set('Content-Range', `bytes */${stat.size}`);
365
- return done(new Error('Range Not Satisfiable'));
366
- }
367
- if(ranges !== -2 && ranges.length === 1) {
368
- this.status(206);
369
- this.set('Content-Range', `bytes ${ranges[0].start}-${ranges[0].end}/${stat.size}`);
370
- offset = ranges[0].start;
371
- len = ranges[0].end - ranges[0].start + 1;
372
- ranged = true;
360
+ if(options.acceptRanges) {
361
+ this.set('accept-ranges', 'bytes');
362
+ if(this.req.headers.range) {
363
+ let ranges = this.req.range(stat.size, {
364
+ combine: true
365
+ });
366
+
367
+ // if-range
368
+ if(!isRangeFresh(this.req, this)) {
369
+ ranges = -2;
370
+ }
371
+
372
+ if(ranges === -1) {
373
+ this.status(416);
374
+ this.set('Content-Range', `bytes */${stat.size}`);
375
+ return done(new Error('Range Not Satisfiable'));
376
+ }
377
+ if(ranges !== -2 && ranges.length === 1) {
378
+ this.status(206);
379
+ this.set('Content-Range', `bytes ${ranges[0].start}-${ranges[0].end}/${stat.size}`);
380
+ offset = ranges[0].start;
381
+ len = ranges[0].end - ranges[0].start + 1;
382
+ ranged = true;
383
+ }
373
384
  }
374
385
  }
375
386
 
@@ -384,7 +395,7 @@ module.exports = class Response extends Writable {
384
395
  if(this._res.aborted) {
385
396
  return;
386
397
  }
387
- this.send(data);
398
+ this.end(data);
388
399
  if(callback) callback();
389
400
  }).catch((err) => {
390
401
  if(callback) callback(err);
@@ -440,10 +451,15 @@ module.exports = class Response extends Writable {
440
451
  this.set(header, field[header]);
441
452
  }
442
453
  } else {
454
+ field = field.toLowerCase();
443
455
  if(field === 'set-cookie' && Array.isArray(value)) {
444
456
  value = value.join('; ');
457
+ } else if(field === 'content-type') {
458
+ if(value.startsWith('text/') || value === 'application/json' || value === 'application/javascript') {
459
+ value += '; charset=utf-8';
460
+ }
445
461
  }
446
- this.headers[field.toLowerCase()] = String(value);
462
+ this.headers[field] = String(value);
447
463
  }
448
464
  return this;
449
465
  }
@@ -551,7 +567,7 @@ module.exports = class Response extends Writable {
551
567
  }
552
568
  json(body) {
553
569
  if(!this.get('Content-Type')) {
554
- this.set('Content-Type', 'application/json');
570
+ this.set('Content-Type', 'application/json; charset=utf-8');
555
571
  }
556
572
  const escape = this.app.get('json escape');
557
573
  const replacer = this.app.get('json replacer');
@@ -563,7 +579,7 @@ module.exports = class Response extends Writable {
563
579
  let body = stringify(object, this.app.get('json replacer'), this.app.get('json spaces'), this.app.get('json escape'));
564
580
 
565
581
  if(!this.get('Content-Type')) {
566
- this.set('Content-Type', 'application/javascript');
582
+ this.set('Content-Type', 'application/javascript; charset=utf-8');
567
583
  this.set('X-Content-Type-Options', 'nosniff');
568
584
  }
569
585
 
@@ -572,7 +588,7 @@ module.exports = class Response extends Writable {
572
588
  }
573
589
 
574
590
  if(typeof callback === 'string' && callback.length !== 0) {
575
- this.set('Content-Type', 'application/javascript');
591
+ this.set('Content-Type', 'application/javascript; charset=utf-8');
576
592
  this.set('X-Content-Type-Options', 'nosniff');
577
593
  callback = callback.replace(/[^\[\]\w$.]/g, '');
578
594
 
@@ -608,14 +624,17 @@ module.exports = class Response extends Writable {
608
624
  }
609
625
  this.location(url);
610
626
  this.status(status);
611
- this.set('Content-Type', 'text/plain');
627
+ this.set('Content-Type', 'text/plain; charset=utf-8');
612
628
  return this.send(`${statuses.message[status] ?? status}. Redirecting to ${url}`);
613
629
  }
614
630
 
615
631
  type(type) {
616
- const ct = type.indexOf('/') === -1
632
+ let ct = type.indexOf('/') === -1
617
633
  ? (mime.contentType(type) || 'application/octet-stream')
618
634
  : type;
635
+ if(ct.startsWith('text/') || ct === 'application/json' || ct === 'application/javascript') {
636
+ ct += '; charset=UTF-8';
637
+ }
619
638
  return this.set('Content-Type', ct);
620
639
  }
621
640
  contentType(type) {
package/src/router.js CHANGED
@@ -12,6 +12,7 @@ const methods = [
12
12
  'search', 'subscribe', 'unsubscribe', 'report', 'mkactivity', 'mkcalendar',
13
13
  'checkout', 'merge', 'm-search', 'notify', 'subscribe', 'unsubscribe', 'search'
14
14
  ];
15
+ const supportedUwsMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD', 'CONNECT', 'TRACE'];
15
16
 
16
17
  module.exports = class Router extends EventEmitter {
17
18
  #paramCallbacks = new Map();
@@ -122,8 +123,7 @@ module.exports = class Router extends EventEmitter {
122
123
  routes.push(route);
123
124
  // normal routes optimization
124
125
  if(typeof route.pattern === 'string' && route.pattern !== '/*' && !this.parent && this.get('case sensitive routing') && this.uwsApp) {
125
- // the only methods that uWS supports natively
126
- if(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD', 'CONNECT', 'TRACE'].includes(method)) {
126
+ if(supportedUwsMethods.includes(method)) {
127
127
  const optimizedPath = this.#optimizeRoute(route, this._routes);
128
128
  if(optimizedPath) {
129
129
  this.#registerUwsRoute(route, optimizedPath);
@@ -153,13 +153,13 @@ module.exports = class Router extends EventEmitter {
153
153
  return; // can only optimize router whos parent is listening
154
154
  }
155
155
  for(let cbroute of callback._routes) {
156
- if(!needsConversionToRegex(cbroute.path) && cbroute.path !== '/*') {
156
+ if(!needsConversionToRegex(cbroute.path) && cbroute.path !== '/*' && supportedUwsMethods.includes(cbroute.method)) {
157
157
  let optimizedRouterPath = this.#optimizeRoute(cbroute, callback._routes);
158
158
  if(optimizedRouterPath) {
159
159
  optimizedRouterPath = optimizedRouterPath.slice(0, -1);
160
160
  const optimizedPath = [...optimizedPathToRouter, {
161
- ...route,
162
161
  // fake route to update req._opPath and req.url
162
+ ...route,
163
163
  callbacks: [
164
164
  (req, res, next) => {
165
165
  next('skipPop');
@@ -263,8 +263,13 @@ module.exports = class Router extends EventEmitter {
263
263
  }
264
264
 
265
265
  #handleError(err, request, response) {
266
- if(this.errorRoute) {
267
- return this.errorRoute(err, request, response, () => {
266
+ let errorRoute = this.errorRoute, parent = this.parent;
267
+ while(!errorRoute && parent) {
268
+ errorRoute = parent.errorRoute;
269
+ parent = parent.parent;
270
+ }
271
+ if(errorRoute) {
272
+ return errorRoute(err, request, response, () => {
268
273
  if(!response.headersSent) {
269
274
  if(response.statusCode === 200) {
270
275
  response.statusCode = 500;