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 +4 -1
- package/src/application.js +11 -7
- package/src/middlewares.js +1 -0
- package/src/response.js +57 -38
- package/src/router.js +11 -6
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultimate-express",
|
|
3
|
-
"version": "1.
|
|
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"
|
package/src/application.js
CHANGED
|
@@ -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 =
|
|
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) {
|
package/src/middlewares.js
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
-
|
|
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
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
267
|
-
|
|
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;
|