ultimate-express 2.0.17 → 2.1.0
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 +4 -19
- package/package.json +20 -15
- package/src/application.js +17 -6
- package/src/declarative.js +18 -5
- package/src/request.js +92 -34
- package/src/response.js +38 -49
- package/src/router.js +25 -16
- package/src/types.d.ts +9 -5
- package/src/utils.js +47 -0
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ Similar projects based on uWebSockets:
|
|
|
24
24
|
|
|
25
25
|
## Performance
|
|
26
26
|
|
|
27
|
-
###
|
|
27
|
+
### Performance against Express
|
|
28
28
|
|
|
29
29
|
Tested using [wrk](https://github.com/wg/wrk) (`-d 60 -t 1 -c 200`). Tested on Ubuntu 22.04, Node.js 20.17.0, AMD Ryzen 5 3600, 64GB RAM.
|
|
30
30
|
|
|
@@ -40,25 +40,9 @@ Tested using [wrk](https://github.com/wg/wrk) (`-d 60 -t 1 -c 200`). Tested on U
|
|
|
40
40
|
| middlewares/compression-file (/small-file) | 4.81k | 14.92k | 386 MB/sec | 1.17 GB/sec | **3.10X** |
|
|
41
41
|
|
|
42
42
|
### Performance against other frameworks
|
|
43
|
-
|
|
44
|
-
Tested using [bun-http-framework-benchmark](https://github.com/dimdenGD/bun-http-framework-benchmark). This table only includes Node.js results.
|
|
45
|
-
For full table with other runtimes, check [here](https://github.com/dimdenGD/bun-http-framework-benchmark?tab=readme-ov-file#results).
|
|
46
|
-
|
|
47
|
-
| Framework | Average | Ping | Query | Body |
|
|
48
|
-
| -------------------- | -------------- | ------------- | ------------- | ------------- |
|
|
49
|
-
| uws | 95,531.277 | 109,960.35 | 105,601.47 | 71,032.01 |
|
|
50
|
-
| **ultimate-express (declarative)** | **86,794.997** | **108,546.44** | **105,869.75** | **45,968.8** |
|
|
51
|
-
| hyper-express | 68,959.92 | 82,547.21 | 71,685.51 | 52,647.04 |
|
|
52
|
-
| **ultimate-express** | **60,839.75** | **68,938.53** | **66,173.86** | **47,406.86** |
|
|
53
|
-
| h3 | 35,423.263 | 41,243.68 | 34,429.26 | 30,596.85 |
|
|
54
|
-
| fastify | 33,094.62 | 40,147.67 | 40,076.35 | 19,059.84 |
|
|
55
|
-
| hono | 26,576.02 | 36,215.35 | 34,656.12 | 8,856.59 |
|
|
56
|
-
| koa | 24,045.08 | 28,202.12 | 24,590.84 | 19,342.28 |
|
|
57
|
-
| express | 10,411.313 | 11,245.57 | 10,598.74 | 9,389.63 |
|
|
58
|
-
|
|
59
|
-
Other benchmarks:
|
|
60
43
|
- [TechEmpower / FrameworkBenchmarks](https://www.techempower.com/benchmarks/#section=data-r23&test=plaintext&l=zik0sf-pa7)
|
|
61
44
|
- [the-benchmarker / web-frameworks](https://web-frameworks-benchmark.netlify.app/result?l=javascript)
|
|
45
|
+
- [HttpArena](https://www.http-arena.com/leaderboard/)
|
|
62
46
|
|
|
63
47
|
### Performance on real-world application
|
|
64
48
|
|
|
@@ -237,7 +221,7 @@ In general, basically all features and options are supported. Use [Express 4.x d
|
|
|
237
221
|
- ✅ req.subdomains
|
|
238
222
|
- ✅ req.xhr
|
|
239
223
|
- 🚧 req.route (route implementation is different from Express)
|
|
240
|
-
- 🚧 req.connection, req.socket (only `end()`, `encrypted`, `remoteAddress` and `localPort` are supported)
|
|
224
|
+
- 🚧 req.connection, req.socket (only `end()`, `encrypted`, `remoteAddress`, `remotePort` and `localPort` are supported)
|
|
241
225
|
- ✅ req.accepts()
|
|
242
226
|
- ✅ req.acceptsCharsets()
|
|
243
227
|
- ✅ req.acceptsEncodings()
|
|
@@ -333,6 +317,7 @@ Almost all middlewares that are compatible with Express are compatible with µEx
|
|
|
333
317
|
- ✅ [swagger-ui-express](https://www.npmjs.com/package/swagger-ui-express)
|
|
334
318
|
- ✅ [graphql-http](https://www.npmjs.com/package/graphql-http)
|
|
335
319
|
- ✅ [better-sse](https://www.npmjs.com/package/better-sse)
|
|
320
|
+
- ✅ [supertest](https://www.npmjs.com/package/supertest)
|
|
336
321
|
|
|
337
322
|
Middlewares and modules that are confirmed to not work:
|
|
338
323
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultimate-express",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
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
|
+
"test:types": "tsd --files tests/types/*.test-d.ts",
|
|
9
|
+
"benchmark:compare": "node benchmark/run.js",
|
|
9
10
|
"cover": "npm run cover:unit && npm run cover:report",
|
|
10
11
|
"cover:unit": "nyc --silent npm run test",
|
|
11
12
|
"cover:report": "nyc report --reporter=html"
|
|
@@ -43,10 +44,11 @@
|
|
|
43
44
|
},
|
|
44
45
|
"homepage": "https://github.com/dimdenGD/ultimate-express#readme",
|
|
45
46
|
"dependencies": {
|
|
46
|
-
"@types/express": "^4.17.
|
|
47
|
+
"@types/express": "^4.17.25",
|
|
47
48
|
"accepts": "^1.3.8",
|
|
48
|
-
"acorn": "^8.
|
|
49
|
+
"acorn": "^8.16.0",
|
|
49
50
|
"bytes": "^3.1.2",
|
|
51
|
+
"content-disposition": "^1.1.0",
|
|
50
52
|
"cookie": "^1.1.1",
|
|
51
53
|
"cookie-signature": "^1.2.2",
|
|
52
54
|
"encodeurl": "^2.0.0",
|
|
@@ -57,22 +59,23 @@
|
|
|
57
59
|
"mime-types": "^2.1.35",
|
|
58
60
|
"ms": "^2.1.3",
|
|
59
61
|
"proxy-addr": "^2.0.7",
|
|
60
|
-
"qs": "^6.
|
|
62
|
+
"qs": "^6.15.1",
|
|
61
63
|
"range-parser": "^1.2.1",
|
|
62
64
|
"statuses": "^2.0.2",
|
|
63
65
|
"tseep": "^1.3.1",
|
|
64
66
|
"type-is": "^2.0.1",
|
|
65
|
-
"uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.
|
|
67
|
+
"uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.64.0",
|
|
66
68
|
"vary": "^1.1.2"
|
|
67
69
|
},
|
|
68
70
|
"devDependencies": {
|
|
69
71
|
"@codechecks/client": "^0.1.12",
|
|
72
|
+
"@types/node": "^25.5.2",
|
|
70
73
|
"better-sse": "^0.16.1",
|
|
71
|
-
"body-parser": "^2.2.
|
|
74
|
+
"body-parser": "^2.2.2",
|
|
72
75
|
"compression": "^1.8.1",
|
|
73
76
|
"cookie-parser": "^1.4.7",
|
|
74
77
|
"cookie-session": "^2.1.1",
|
|
75
|
-
"cors": "^2.8.
|
|
78
|
+
"cors": "^2.8.6",
|
|
76
79
|
"ejs": "^3.1.10",
|
|
77
80
|
"errorhandler": "^1.5.2",
|
|
78
81
|
"eventsource": "^4.1.0",
|
|
@@ -82,30 +85,32 @@
|
|
|
82
85
|
"express-async-errors": "^3.1.1",
|
|
83
86
|
"express-dot-engine": "^1.0.8",
|
|
84
87
|
"express-fileupload": "^1.5.2",
|
|
85
|
-
"express-handlebars": "^8.0.
|
|
88
|
+
"express-handlebars": "^8.0.7",
|
|
86
89
|
"express-http-proxy": "^2.1.2",
|
|
87
90
|
"express-mongo-sanitize": "^2.2.0",
|
|
88
|
-
"express-rate-limit": "^8.2
|
|
89
|
-
"express-session": "^1.
|
|
91
|
+
"express-rate-limit": "^8.3.2",
|
|
92
|
+
"express-session": "^1.19.0",
|
|
90
93
|
"express-subdomain": "^1.0.6",
|
|
91
94
|
"graphql-http": "^1.22.4",
|
|
92
95
|
"helmet": "^8.1.0",
|
|
93
96
|
"http-proxy-middleware": "^3.0.5",
|
|
94
97
|
"method-override": "^3.0.0",
|
|
95
98
|
"morgan": "^1.10.1",
|
|
96
|
-
"multer": "^2.
|
|
99
|
+
"multer": "^2.1.1",
|
|
97
100
|
"mustache-express": "^1.3.2",
|
|
98
101
|
"nyc": "^17.1.0",
|
|
99
102
|
"pako": "^2.1.0",
|
|
100
103
|
"passport": "^0.7.0",
|
|
101
104
|
"passport-local": "^1.0.0",
|
|
102
|
-
"pkg-pr-new": "^0.0.
|
|
103
|
-
"pug": "^3.0.
|
|
105
|
+
"pkg-pr-new": "^0.0.66",
|
|
106
|
+
"pug": "^3.0.4",
|
|
104
107
|
"response-time": "^2.3.4",
|
|
105
|
-
"serve-index": "^1.9.
|
|
108
|
+
"serve-index": "^1.9.2",
|
|
106
109
|
"serve-static": "^2.2.1",
|
|
110
|
+
"supertest": "^7.2.2",
|
|
107
111
|
"swagger-ui-express": "^5.0.1",
|
|
108
112
|
"swig": "^1.4.2",
|
|
113
|
+
"tsd": "^0.33.0",
|
|
109
114
|
"vhost": "^3.0.2"
|
|
110
115
|
}
|
|
111
116
|
}
|
package/src/application.js
CHANGED
|
@@ -22,6 +22,7 @@ const ViewClass = require("./view.js");
|
|
|
22
22
|
const path = require("path");
|
|
23
23
|
const os = require("os");
|
|
24
24
|
const { Worker } = require("worker_threads");
|
|
25
|
+
const cluster = require('cluster');
|
|
25
26
|
|
|
26
27
|
const cpuCount = os.cpus().length;
|
|
27
28
|
|
|
@@ -222,18 +223,21 @@ class Application extends Router {
|
|
|
222
223
|
}
|
|
223
224
|
const onListen = socket => {
|
|
224
225
|
if(!socket) {
|
|
225
|
-
let err = new Error('
|
|
226
|
+
let err = new Error('listen EADDRINUSE: address already in use :::' + port);
|
|
227
|
+
err.code = 'EADDRINUSE';
|
|
226
228
|
throw err;
|
|
227
229
|
}
|
|
228
230
|
this.port = uWS.us_socket_local_port(socket);
|
|
229
|
-
if(callback) callback(
|
|
231
|
+
if(callback) callback();
|
|
230
232
|
};
|
|
231
233
|
let fn = 'listen';
|
|
232
234
|
let args = [];
|
|
235
|
+
// 1 = exclusive port, 0 = shared port
|
|
236
|
+
let uwsOptions = cluster.isPrimary ? 1 : 0;
|
|
233
237
|
if(typeof port !== 'number') {
|
|
234
238
|
if(!isNaN(Number(port))) {
|
|
235
239
|
port = Number(port);
|
|
236
|
-
args.push(port, onListen);
|
|
240
|
+
args.push(port, uwsOptions, onListen);
|
|
237
241
|
if(host) {
|
|
238
242
|
args.unshift(host);
|
|
239
243
|
}
|
|
@@ -242,18 +246,18 @@ class Application extends Router {
|
|
|
242
246
|
args.push(onListen, port);
|
|
243
247
|
}
|
|
244
248
|
} else {
|
|
245
|
-
args.push(port, onListen);
|
|
249
|
+
args.push(port, uwsOptions, onListen);
|
|
246
250
|
if(host) {
|
|
247
251
|
args.unshift(host);
|
|
248
252
|
}
|
|
249
253
|
}
|
|
250
254
|
this.listenCalled = true;
|
|
251
255
|
this.uwsApp[fn](...args);
|
|
252
|
-
return this
|
|
256
|
+
return this;
|
|
253
257
|
}
|
|
254
258
|
|
|
255
259
|
address() {
|
|
256
|
-
return { port: this.port };
|
|
260
|
+
return this.port ? { port: this.port } : null;
|
|
257
261
|
}
|
|
258
262
|
|
|
259
263
|
path() {
|
|
@@ -335,6 +339,13 @@ class Application extends Router {
|
|
|
335
339
|
callback(err);
|
|
336
340
|
}
|
|
337
341
|
}
|
|
342
|
+
|
|
343
|
+
close(callback) {
|
|
344
|
+
if(this.listenCalled) {
|
|
345
|
+
this.uwsApp.close();
|
|
346
|
+
}
|
|
347
|
+
if(callback) callback();
|
|
348
|
+
}
|
|
338
349
|
}
|
|
339
350
|
|
|
340
351
|
module.exports = function(options) {
|
package/src/declarative.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
const acorn = require("acorn");
|
|
2
2
|
const { stringify } = require("./utils.js");
|
|
3
3
|
const uWS = require("uWebSockets.js");
|
|
4
|
+
const statuses = require("statuses");
|
|
4
5
|
|
|
5
6
|
const parser = acorn.Parser;
|
|
6
7
|
|
|
7
|
-
const allowedResMethods = ['set', 'header', 'setHeader', 'status', 'send', 'end', 'append'];
|
|
8
|
+
const allowedResMethods = ['set', 'header', 'setHeader', 'sendStatus', 'status', 'send', 'end', 'append'];
|
|
8
9
|
const allowedIdentifiers = ['query', 'params', ...allowedResMethods];
|
|
9
10
|
const objKeyRegex = /[\s{\n]([A-Za-z-0-9_]+)(\s|\n)*?:/g;
|
|
10
11
|
|
|
@@ -203,6 +204,11 @@ module.exports = function compileDeclarative(cb, app) {
|
|
|
203
204
|
return false;
|
|
204
205
|
}
|
|
205
206
|
headers.push([call.arguments[0].value, call.arguments[1].value]);
|
|
207
|
+
} else if(call.obj.propertyName === 'sendStatus'){
|
|
208
|
+
if(call.arguments[0].type !== 'Literal') {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
statusCode = call.arguments[0].value;
|
|
206
212
|
}
|
|
207
213
|
}
|
|
208
214
|
|
|
@@ -354,13 +360,16 @@ module.exports = function compileDeclarative(cb, app) {
|
|
|
354
360
|
}
|
|
355
361
|
}
|
|
356
362
|
|
|
357
|
-
|
|
363
|
+
let decRes = new uWS.DeclarativeResponse();
|
|
364
|
+
|
|
358
365
|
if(statusCode != 200) {
|
|
359
|
-
|
|
366
|
+
const statusMessage = statuses.message[statusCode] ?? '';
|
|
367
|
+
decRes = decRes.writeStatus(`${statusCode} ${statusMessage}`.trim());
|
|
368
|
+
if(!headers.some(header => header[0].toLowerCase() === 'content-type')) {
|
|
369
|
+
decRes = decRes.writeHeader('content-type','text/plain; charset=utf-8');
|
|
370
|
+
}
|
|
360
371
|
}
|
|
361
372
|
|
|
362
|
-
let decRes = new uWS.DeclarativeResponse();
|
|
363
|
-
|
|
364
373
|
for(let header of headers) {
|
|
365
374
|
if(header[0].toLowerCase() === 'content-length') {
|
|
366
375
|
return false;
|
|
@@ -390,6 +399,10 @@ module.exports = function compileDeclarative(cb, app) {
|
|
|
390
399
|
}
|
|
391
400
|
}
|
|
392
401
|
|
|
402
|
+
if(!body.length) {
|
|
403
|
+
decRes = decRes.write(statuses.message[statusCode] || String(statusCode));
|
|
404
|
+
}
|
|
405
|
+
|
|
393
406
|
return decRes.end();
|
|
394
407
|
} catch(e) {
|
|
395
408
|
return false;
|
package/src/request.js
CHANGED
|
@@ -19,15 +19,16 @@ const accepts = require("accepts");
|
|
|
19
19
|
const typeis = require("type-is");
|
|
20
20
|
const parseRange = require("range-parser");
|
|
21
21
|
const proxyaddr = require("proxy-addr");
|
|
22
|
+
const { isIP } = require("node:net");
|
|
22
23
|
const fresh = require("fresh");
|
|
23
24
|
const { Readable } = require("stream");
|
|
24
25
|
|
|
25
|
-
const discardedDuplicates = [
|
|
26
|
+
const discardedDuplicates = new Set([
|
|
26
27
|
"age", "authorization", "content-length", "content-type", "etag", "expires",
|
|
27
28
|
"from", "host", "if-modified-since", "if-unmodified-since", "last-modified",
|
|
28
29
|
"location", "max-forwards", "proxy-authorization", "referer", "retry-after",
|
|
29
30
|
"server", "user-agent"
|
|
30
|
-
];
|
|
31
|
+
]);
|
|
31
32
|
|
|
32
33
|
let key = 0;
|
|
33
34
|
|
|
@@ -153,19 +154,28 @@ module.exports = class Request extends Readable {
|
|
|
153
154
|
|
|
154
155
|
get #host() {
|
|
155
156
|
const trust = this.app.get('trust proxy fn');
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
157
|
+
const isTrusted = !!(trust && trust(this.connection.remoteAddress, 0));
|
|
158
|
+
const rawHeader = (isTrusted && this.headers['x-forwarded-host']) || this.headers['host'];
|
|
159
|
+
let host = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader;
|
|
160
|
+
|
|
161
|
+
if (typeof host !== 'string' || !host) return;
|
|
162
|
+
host = host.trim();
|
|
163
|
+
|
|
164
|
+
if (isTrusted) {
|
|
165
|
+
const commaIndex = host.indexOf(',');
|
|
166
|
+
if (commaIndex !== -1) {
|
|
167
|
+
// Note: X-Forwarded-Host is normally only ever a
|
|
168
|
+
// single value, but this is to be safe.
|
|
169
|
+
host = host.substring(0, commaIndex).trimEnd();
|
|
170
|
+
}
|
|
166
171
|
}
|
|
167
|
-
|
|
168
|
-
|
|
172
|
+
|
|
173
|
+
if (!host) return;
|
|
174
|
+
|
|
175
|
+
const offset = host[0] === '[' ? host.indexOf(']') + 1 : 0;
|
|
176
|
+
const portIndex = host.indexOf(':', offset);
|
|
177
|
+
|
|
178
|
+
return portIndex !== -1 ? host.substring(0, portIndex) : host;
|
|
169
179
|
}
|
|
170
180
|
|
|
171
181
|
get host() {
|
|
@@ -174,11 +184,7 @@ module.exports = class Request extends Readable {
|
|
|
174
184
|
}
|
|
175
185
|
|
|
176
186
|
get hostname() {
|
|
177
|
-
|
|
178
|
-
if(!host) return this.headers['host'].split(':')[0];
|
|
179
|
-
const offset = host[0] === '[' ? host.indexOf(']') + 1 : 0;
|
|
180
|
-
const index = host.indexOf(':', offset);
|
|
181
|
-
return index !== -1 ? host.slice(0, index) : host;
|
|
187
|
+
return this.#host;
|
|
182
188
|
}
|
|
183
189
|
|
|
184
190
|
get httpVersion() {
|
|
@@ -246,18 +252,28 @@ module.exports = class Request extends Readable {
|
|
|
246
252
|
return this.protocol === 'https';
|
|
247
253
|
}
|
|
248
254
|
|
|
255
|
+
#cachedSubdomains = null;
|
|
256
|
+
|
|
249
257
|
get subdomains() {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
258
|
+
if(this.#cachedSubdomains !== null) {
|
|
259
|
+
return this.#cachedSubdomains;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const hostname = this.hostname;
|
|
263
|
+
if(!hostname || isIP(hostname)) {
|
|
264
|
+
return this.#cachedSubdomains = [];
|
|
255
265
|
}
|
|
256
|
-
|
|
266
|
+
|
|
267
|
+
const offset = this.app.get('subdomain offset');
|
|
268
|
+
const parts = hostname.split('.');
|
|
269
|
+
const subdomains = parts.reverse().slice(offset);
|
|
270
|
+
|
|
271
|
+
return this.#cachedSubdomains = subdomains;
|
|
257
272
|
}
|
|
258
273
|
|
|
259
274
|
get xhr() {
|
|
260
|
-
|
|
275
|
+
const val = this.headers?.['x-requested-with'];
|
|
276
|
+
return typeof val === 'string' && val.toLowerCase() === 'xmlhttprequest';
|
|
261
277
|
}
|
|
262
278
|
|
|
263
279
|
get parsedIp() {
|
|
@@ -299,6 +315,7 @@ module.exports = class Request extends Readable {
|
|
|
299
315
|
get connection() {
|
|
300
316
|
return {
|
|
301
317
|
remoteAddress: this.parsedIp,
|
|
318
|
+
remotePort: this._res.getRemotePort(),
|
|
302
319
|
localPort: this.app.port,
|
|
303
320
|
encrypted: this.app.ssl,
|
|
304
321
|
end: (body) => this.res.end(body)
|
|
@@ -327,6 +344,12 @@ module.exports = class Request extends Readable {
|
|
|
327
344
|
}
|
|
328
345
|
|
|
329
346
|
get(field) {
|
|
347
|
+
if(!field) {
|
|
348
|
+
throw new TypeError('name argument is required to req.get');
|
|
349
|
+
}
|
|
350
|
+
if(typeof field !== 'string') {
|
|
351
|
+
throw new TypeError('name must be a string to req.get');
|
|
352
|
+
}
|
|
330
353
|
field = field.toLowerCase();
|
|
331
354
|
if(field === 'referrer' || field === 'referer') {
|
|
332
355
|
const res = this.headers['referrer'];
|
|
@@ -355,19 +378,54 @@ module.exports = class Request extends Readable {
|
|
|
355
378
|
return accepts(this).languages(...languages);
|
|
356
379
|
}
|
|
357
380
|
|
|
358
|
-
|
|
359
|
-
|
|
381
|
+
acceptsEncoding(...args) {
|
|
382
|
+
deprecated('req.acceptsEncoding', 'req.acceptsEncodings');
|
|
383
|
+
return this.acceptsEncodings(...args);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
acceptsCharset(...args) {
|
|
387
|
+
deprecated('req.acceptsCharset', 'req.acceptsCharsets');
|
|
388
|
+
return this.acceptsCharsets(...args);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
acceptsLanguage(...args) {
|
|
392
|
+
deprecated('req.acceptsLanguage', 'req.acceptsLanguages');
|
|
393
|
+
return this.acceptsLanguages(...args);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
is(types) {
|
|
397
|
+
if(Array.isArray(types)) {
|
|
398
|
+
return typeis(this, types);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if(arguments.length === 1) {
|
|
402
|
+
return typeis(this, [types]);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return typeis(this, [...arguments]);
|
|
360
406
|
}
|
|
361
407
|
|
|
362
408
|
param(name, defaultValue) {
|
|
363
409
|
deprecated('req.param(name)', 'req.params, req.body, or req.query');
|
|
364
|
-
|
|
365
|
-
|
|
410
|
+
|
|
411
|
+
if(name == null) return defaultValue;
|
|
412
|
+
|
|
413
|
+
if(this.params && Object.prototype.hasOwnProperty.call(this.params, name)) {
|
|
414
|
+
const value = this.params[name];
|
|
415
|
+
if(value != null) return value;
|
|
366
416
|
}
|
|
367
|
-
|
|
368
|
-
|
|
417
|
+
|
|
418
|
+
if(this.body && Object.prototype.hasOwnProperty.call(this.body, name)) {
|
|
419
|
+
const value = this.body[name];
|
|
420
|
+
if(value != null) return value;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if(this.query && Object.prototype.hasOwnProperty.call(this.query, name)) {
|
|
424
|
+
const value = this.query[name];
|
|
425
|
+
if(value != null) return value;
|
|
369
426
|
}
|
|
370
|
-
|
|
427
|
+
|
|
428
|
+
return defaultValue;
|
|
371
429
|
}
|
|
372
430
|
|
|
373
431
|
range(size, options) {
|
|
@@ -389,7 +447,7 @@ module.exports = class Request extends Readable {
|
|
|
389
447
|
let [key, value] = this.#rawHeadersEntries[index];
|
|
390
448
|
key = key.toLowerCase();
|
|
391
449
|
if(this.#cachedHeaders[key]) {
|
|
392
|
-
if(discardedDuplicates.
|
|
450
|
+
if(discardedDuplicates.has(key)) {
|
|
393
451
|
continue;
|
|
394
452
|
}
|
|
395
453
|
if(key === 'cookie') {
|
package/src/response.js
CHANGED
|
@@ -18,9 +18,10 @@ const cookie = require("cookie");
|
|
|
18
18
|
const mime = require("mime-types");
|
|
19
19
|
const vary = require("vary");
|
|
20
20
|
const encodeUrl = require("encodeurl");
|
|
21
|
-
const
|
|
21
|
+
const contentDisposition = require("content-disposition");
|
|
22
|
+
const {
|
|
22
23
|
normalizeType, stringify, deprecated, UP_PATH_REGEXP, decode,
|
|
23
|
-
containsDotFile, isPreconditionFailure, isRangeFresh, NullObject
|
|
24
|
+
containsDotFile, isPreconditionFailure, isRangeFresh, escapeHtml, NullObject
|
|
24
25
|
} = require("./utils.js");
|
|
25
26
|
const { Writable } = require("stream");
|
|
26
27
|
const { isAbsolute } = require("path");
|
|
@@ -70,9 +71,7 @@ class Socket extends EventEmitter {
|
|
|
70
71
|
module.exports = class Response extends Writable {
|
|
71
72
|
#socket = null;
|
|
72
73
|
#ended = false;
|
|
73
|
-
#
|
|
74
|
-
#lastWriteChunkTime = 0;
|
|
75
|
-
#writeTimeout = null;
|
|
74
|
+
#pendingCallback = null;
|
|
76
75
|
req;
|
|
77
76
|
constructor(res, req, app) {
|
|
78
77
|
super();
|
|
@@ -156,36 +155,21 @@ module.exports = class Response extends Writable {
|
|
|
156
155
|
}
|
|
157
156
|
|
|
158
157
|
if (this.chunkedTransfer) {
|
|
159
|
-
this
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
} else if(!this.#writeTimeout) {
|
|
174
|
-
this.#writeTimeout = setTimeout(() => {
|
|
175
|
-
this.#writeTimeout = null;
|
|
176
|
-
if(!this.finished && !this.aborted) this._res.cork(() => {
|
|
177
|
-
if(this.#pendingChunks.length) {
|
|
178
|
-
const size = this.#pendingChunks.reduce((acc, chunk) => acc + chunk.byteLength, 0);
|
|
179
|
-
this._res.write(Buffer.concat(this.#pendingChunks, size));
|
|
180
|
-
this.#pendingChunks = [];
|
|
181
|
-
this.#lastWriteChunkTime = performance.now();
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
}, 50);
|
|
185
|
-
this.#writeTimeout.unref();
|
|
158
|
+
const ok = this._res.write(chunk);
|
|
159
|
+
if (ok) {
|
|
160
|
+
this.writingChunk = false;
|
|
161
|
+
callback(null);
|
|
162
|
+
} else {
|
|
163
|
+
this.#pendingCallback = callback;
|
|
164
|
+
this._res.onWritable(() => {
|
|
165
|
+
if (this.aborted || this.finished) return true;
|
|
166
|
+
const cb = this.#pendingCallback;
|
|
167
|
+
this.#pendingCallback = null;
|
|
168
|
+
this.writingChunk = false;
|
|
169
|
+
if (cb) cb(null);
|
|
170
|
+
return true;
|
|
171
|
+
});
|
|
186
172
|
}
|
|
187
|
-
this.writingChunk = false;
|
|
188
|
-
callback(null);
|
|
189
173
|
} else {
|
|
190
174
|
const lastOffset = this._res.getWriteOffset();
|
|
191
175
|
const [ok, done] = this._res.tryEnd(chunk, this.totalSize);
|
|
@@ -263,7 +247,7 @@ module.exports = class Response extends Writable {
|
|
|
263
247
|
return this;
|
|
264
248
|
}
|
|
265
249
|
sendStatus(code) {
|
|
266
|
-
return this.status(code).send(statuses.message[
|
|
250
|
+
return this.status(code).type('txt').send(statuses.message[code] || String(code));
|
|
267
251
|
}
|
|
268
252
|
end(data, cb) {
|
|
269
253
|
if(typeof data === 'function') {
|
|
@@ -311,11 +295,6 @@ module.exports = class Response extends Writable {
|
|
|
311
295
|
if(!data && contentLength) {
|
|
312
296
|
this._res.endWithoutBody(contentLength.toString());
|
|
313
297
|
} else {
|
|
314
|
-
if(this.#pendingChunks.length) {
|
|
315
|
-
this._res.write(Buffer.concat(this.#pendingChunks));
|
|
316
|
-
this.#pendingChunks = [];
|
|
317
|
-
this.lastWriteChunkTime = 0;
|
|
318
|
-
}
|
|
319
298
|
if(data instanceof Buffer) {
|
|
320
299
|
data = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
321
300
|
}
|
|
@@ -724,8 +703,10 @@ module.exports = class Response extends Writable {
|
|
|
724
703
|
return this.cookie(name, '', opts);
|
|
725
704
|
}
|
|
726
705
|
attachment(filename) {
|
|
727
|
-
|
|
728
|
-
|
|
706
|
+
if(filename) {
|
|
707
|
+
this.type(Path.extname(filename));
|
|
708
|
+
}
|
|
709
|
+
this.set('Content-Disposition', contentDisposition(filename));
|
|
729
710
|
return this;
|
|
730
711
|
}
|
|
731
712
|
format(object) {
|
|
@@ -740,7 +721,7 @@ module.exports = class Response extends Writable {
|
|
|
740
721
|
} else if(object.default) {
|
|
741
722
|
object.default(this.req, this, this.req.next);
|
|
742
723
|
} else {
|
|
743
|
-
this.status(406).send(this.app._generateErrorPage('Not Acceptable'));
|
|
724
|
+
this.status(406).send(this.app._generateErrorPage('Not Acceptable', this.statusCode, false));
|
|
744
725
|
}
|
|
745
726
|
|
|
746
727
|
return this;
|
|
@@ -787,8 +768,13 @@ module.exports = class Response extends Writable {
|
|
|
787
768
|
return this.send(body);
|
|
788
769
|
}
|
|
789
770
|
links(links) {
|
|
790
|
-
this.headers['link'] = Object.entries(links).map(([rel, url]) => `<${url}>; rel="${rel}"`).join(', ');
|
|
791
|
-
return this;
|
|
771
|
+
// this.headers['link'] = Object.entries(links).map(([rel, url]) => `<${url}>; rel="${rel}"`).join(', ');
|
|
772
|
+
// return this;
|
|
773
|
+
let link = this.get('Link') || '';
|
|
774
|
+
if(link) link += ', ';
|
|
775
|
+
return this.set('Link', link + Object.keys(links).map(function(rel){
|
|
776
|
+
return '<' + links[rel] + '>; rel="' + rel + '"';
|
|
777
|
+
}).join(', '));
|
|
792
778
|
}
|
|
793
779
|
location(path) {
|
|
794
780
|
if(path === 'back') {
|
|
@@ -796,7 +782,8 @@ module.exports = class Response extends Writable {
|
|
|
796
782
|
if(!path) path = this.req.get('Referer');
|
|
797
783
|
if(!path) path = '/';
|
|
798
784
|
}
|
|
799
|
-
|
|
785
|
+
this.headers['location'] = encodeUrl(path);
|
|
786
|
+
return this;
|
|
800
787
|
}
|
|
801
788
|
redirect(status, url, forceHtml = false) {
|
|
802
789
|
if(typeof status !== 'number' && !url) {
|
|
@@ -805,6 +792,8 @@ module.exports = class Response extends Writable {
|
|
|
805
792
|
}
|
|
806
793
|
this.location(url);
|
|
807
794
|
this.status(status);
|
|
795
|
+
|
|
796
|
+
const address = this.get('Location');
|
|
808
797
|
let body;
|
|
809
798
|
// Support text/{plain,html} by default
|
|
810
799
|
if(forceHtml) {
|
|
@@ -817,18 +806,18 @@ module.exports = class Response extends Writable {
|
|
|
817
806
|
'<title>Redirecting</title>\n' +
|
|
818
807
|
'</head>\n' +
|
|
819
808
|
'<body>\n' +
|
|
820
|
-
`<pre>Redirecting to ${
|
|
809
|
+
`<pre>Redirecting to ${escapeHtml(address)}</pre>\n` +
|
|
821
810
|
'</body>\n' +
|
|
822
811
|
'</html>\n';
|
|
823
812
|
} else {
|
|
824
813
|
this.format({
|
|
825
814
|
text: () => {
|
|
826
815
|
this.set('Content-Type', 'text/plain; charset=UTF-8');
|
|
827
|
-
body = statuses.message[status]
|
|
816
|
+
body = `${statuses.message[status]}. Redirecting to ${address}`;
|
|
828
817
|
},
|
|
829
818
|
html: () => {
|
|
830
819
|
this.set('Content-Type', 'text/html; charset=UTF-8');
|
|
831
|
-
body = `<p>${statuses.message[status]}. Redirecting to ${
|
|
820
|
+
body = `<p>${statuses.message[status]}. Redirecting to ${escapeHtml(address)}</p>`;
|
|
832
821
|
},
|
|
833
822
|
default: () => {
|
|
834
823
|
this.set('Content-Type', 'text/plain; charset=UTF-8');
|
package/src/router.js
CHANGED
|
@@ -35,10 +35,23 @@ const methods = [
|
|
|
35
35
|
'search', 'subscribe', 'unsubscribe', 'report', 'mkactivity', 'mkcalendar',
|
|
36
36
|
'checkout', 'merge', 'm-search', 'notify', 'subscribe', 'unsubscribe', 'search'
|
|
37
37
|
];
|
|
38
|
-
const supportedUwsMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD', 'CONNECT', 'TRACE'];
|
|
38
|
+
const supportedUwsMethods = new Set(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD', 'CONNECT', 'TRACE']);
|
|
39
39
|
|
|
40
40
|
const regExParam = /:(\w+)/g;
|
|
41
41
|
|
|
42
|
+
function generateErrorPageHtml(err) {
|
|
43
|
+
return `<!DOCTYPE html>\n` +
|
|
44
|
+
`<html lang="en">\n` +
|
|
45
|
+
`<head>\n` +
|
|
46
|
+
`<meta charset="utf-8">\n` +
|
|
47
|
+
`<title>Error</title>\n` +
|
|
48
|
+
`</head>\n` +
|
|
49
|
+
`<body>\n` +
|
|
50
|
+
`<pre>${err?.stack ?? err}</pre>\n` +
|
|
51
|
+
`</body>\n` +
|
|
52
|
+
`</html>\n`;
|
|
53
|
+
}
|
|
54
|
+
|
|
42
55
|
module.exports = class Router extends EventEmitter {
|
|
43
56
|
parent;
|
|
44
57
|
listenCalled;
|
|
@@ -162,7 +175,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
162
175
|
routes.push(route);
|
|
163
176
|
// normal routes optimization
|
|
164
177
|
if(canBeOptimized(route.path) && route.pattern !== '/*' && !this.parent && this.get('case sensitive routing') && this.uwsApp) {
|
|
165
|
-
if(supportedUwsMethods.
|
|
178
|
+
if(supportedUwsMethods.has(method)) {
|
|
166
179
|
const optimizedPath = this._optimizeRoute(route, this._routes);
|
|
167
180
|
if(optimizedPath) {
|
|
168
181
|
this._registerUwsRoute(route, optimizedPath);
|
|
@@ -192,7 +205,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
192
205
|
return; // can only optimize router whos parent is listening
|
|
193
206
|
}
|
|
194
207
|
for(let cbroute of callback._routes) {
|
|
195
|
-
if(!needsConversionToRegex(cbroute.path) && cbroute.path !== '/*' && supportedUwsMethods.
|
|
208
|
+
if(!needsConversionToRegex(cbroute.path) && cbroute.path !== '/*' && supportedUwsMethods.has(cbroute.method)) {
|
|
196
209
|
let optimizedRouterPath = this._optimizeRoute(cbroute, callback._routes);
|
|
197
210
|
if(optimizedRouterPath) {
|
|
198
211
|
optimizedRouterPath = optimizedRouterPath.slice(0, -1);
|
|
@@ -363,6 +376,13 @@ module.exports = class Router extends EventEmitter {
|
|
|
363
376
|
this._sendErrorPage(request, response, err, true);
|
|
364
377
|
}
|
|
365
378
|
|
|
379
|
+
_generateErrorPage(err, statusCode, checkEnv = false) {
|
|
380
|
+
if(checkEnv && this.get('env') === 'production') {
|
|
381
|
+
err = statusCode >= 400 ? (statuses.message[statusCode] ?? 'Internal Server Error') : 'Internal Server Error';
|
|
382
|
+
}
|
|
383
|
+
return generateErrorPageHtml(err);
|
|
384
|
+
}
|
|
385
|
+
|
|
366
386
|
_extractParams(pattern, path) {
|
|
367
387
|
if(path.endsWith('/')) {
|
|
368
388
|
path = path.slice(0, -1);
|
|
@@ -647,22 +667,11 @@ module.exports = class Router extends EventEmitter {
|
|
|
647
667
|
}
|
|
648
668
|
|
|
649
669
|
_sendErrorPage(request, response, err, checkEnv = false) {
|
|
650
|
-
|
|
651
|
-
err = response.statusCode >= 400 ? (statuses.message[response.statusCode] ?? 'Internal Server Error') : 'Internal Server Error';
|
|
652
|
-
}
|
|
670
|
+
err = this._generateErrorPage(err, response.statusCode, checkEnv);
|
|
653
671
|
request.noEtag = true;
|
|
654
672
|
response.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
655
673
|
response.setHeader('X-Content-Type-Options', 'nosniff');
|
|
656
674
|
response.setHeader('Content-Security-Policy', "default-src 'none'");
|
|
657
|
-
response.send(
|
|
658
|
-
`<html lang="en">\n` +
|
|
659
|
-
`<head>\n` +
|
|
660
|
-
`<meta charset="utf-8">\n` +
|
|
661
|
-
`<title>Error</title>\n` +
|
|
662
|
-
`</head>\n` +
|
|
663
|
-
`<body>\n` +
|
|
664
|
-
`<pre>${err?.stack ?? err}</pre>\n` +
|
|
665
|
-
`</body>\n` +
|
|
666
|
-
`</html>\n`);
|
|
675
|
+
response.send(err);
|
|
667
676
|
}
|
|
668
677
|
}
|
package/src/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
declare module "ultimate-express" {
|
|
2
|
-
import e from "
|
|
2
|
+
import e from "express";
|
|
3
3
|
import uWS from "uWebSockets.js";
|
|
4
4
|
|
|
5
5
|
type Settings = {
|
|
@@ -19,7 +19,7 @@ declare module "ultimate-express" {
|
|
|
19
19
|
export import response = e.response;
|
|
20
20
|
|
|
21
21
|
export import static = e.static;
|
|
22
|
-
// export import query = e.query;
|
|
22
|
+
// export import query = e.query;
|
|
23
23
|
|
|
24
24
|
export import urlencoded = e.urlencoded;
|
|
25
25
|
|
|
@@ -45,11 +45,15 @@ declare module "ultimate-express" {
|
|
|
45
45
|
export import Send = e.Send;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
type UltimateExpressListen = ReturnType<e.Express['listen']> & {
|
|
49
|
+
uwsApp: uWS.TemplatedApp;
|
|
50
|
+
};
|
|
51
|
+
|
|
48
52
|
type UltimateExpress = Omit<e.Express, 'listen'> & {
|
|
49
53
|
readonly uwsApp: uWS.TemplatedApp;
|
|
50
|
-
listen(port: number, callback?: (token: any) => void):
|
|
51
|
-
listen(port: number, host: string, callback?: (token: any) => void):
|
|
52
|
-
listen(callback: (token: any) => void):
|
|
54
|
+
listen(port: number, callback?: (token: any) => void): UltimateExpressListen;
|
|
55
|
+
listen(port: number, host: string, callback?: (token: any) => void): UltimateExpressListen;
|
|
56
|
+
listen(callback: (token: any) => void): UltimateExpressListen;
|
|
53
57
|
};
|
|
54
58
|
|
|
55
59
|
function express(settings?: Settings): UltimateExpress;
|
package/src/utils.js
CHANGED
|
@@ -340,6 +340,52 @@ function isRangeFresh(req, res) {
|
|
|
340
340
|
return parseHttpDate(lastModified) <= parseHttpDate(ifRange);
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
+
function escapeHtml(str) {
|
|
344
|
+
const s = String(str);
|
|
345
|
+
const len = s.length;
|
|
346
|
+
let i = 0;
|
|
347
|
+
|
|
348
|
+
// Fast scan: find first char that needs escaping
|
|
349
|
+
for(; i < len; i++) {
|
|
350
|
+
const ch = s.charCodeAt(i);
|
|
351
|
+
if(ch === 0x26 || ch === 0x3C || ch === 0x3E || ch === 0x22 || ch === 0x27) {
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// No escaping needed
|
|
357
|
+
if(i === len) return s;
|
|
358
|
+
|
|
359
|
+
// Build escaped string from the first match onward
|
|
360
|
+
let escaped = s.substring(0, i);
|
|
361
|
+
|
|
362
|
+
for(; i < len; i++) {
|
|
363
|
+
const ch = s.charCodeAt(i);
|
|
364
|
+
switch(ch) {
|
|
365
|
+
case 0x26: // &
|
|
366
|
+
escaped += '&';
|
|
367
|
+
break;
|
|
368
|
+
case 0x3C: // <
|
|
369
|
+
escaped += '<';
|
|
370
|
+
break;
|
|
371
|
+
case 0x3E: // >
|
|
372
|
+
escaped += '>';
|
|
373
|
+
break;
|
|
374
|
+
case 0x22: // "
|
|
375
|
+
escaped += '"';
|
|
376
|
+
break;
|
|
377
|
+
case 0x27: // '
|
|
378
|
+
escaped += ''';
|
|
379
|
+
break;
|
|
380
|
+
default:
|
|
381
|
+
escaped += s.charAt(i);
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return escaped;
|
|
387
|
+
}
|
|
388
|
+
|
|
343
389
|
// fast null object
|
|
344
390
|
const NullObject = function() {};
|
|
345
391
|
NullObject.prototype = Object.create(null);
|
|
@@ -366,5 +412,6 @@ module.exports = {
|
|
|
366
412
|
findIndexStartingFrom,
|
|
367
413
|
fastQueryParse,
|
|
368
414
|
canBeOptimized,
|
|
415
|
+
escapeHtml,
|
|
369
416
|
EMPTY_REGEX
|
|
370
417
|
};
|