ultimate-express 1.2.5 → 1.2.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 +17 -24
- package/package.json +3 -2
- package/src/index.js +6 -7
- package/src/middlewares.js +179 -62
- package/src/utils.js +2 -2
package/README.md
CHANGED
|
@@ -22,10 +22,10 @@ Similar projects based on uWebSockets:
|
|
|
22
22
|
|
|
23
23
|
## Performance
|
|
24
24
|
|
|
25
|
-
Tested using [wrk](https://github.com/wg/wrk) (`-d 60 -t 1 -c 200`). Etag was disabled in both Express and µExpress. Tested on Ubuntu 22.04, Node.js 20.17.0, AMD Ryzen 5 3600, 64GB RAM.
|
|
26
|
-
|
|
27
25
|
### Test results
|
|
28
26
|
|
|
27
|
+
Tested using [wrk](https://github.com/wg/wrk) (`-d 60 -t 1 -c 200`). Etag was disabled in both Express and µExpress. Tested on Ubuntu 22.04, Node.js 20.17.0, AMD Ryzen 5 3600, 64GB RAM.
|
|
28
|
+
|
|
29
29
|
| Test | Express req/sec | µExpress req/sec | Express throughput | µExpress throughput | µExpress speedup |
|
|
30
30
|
| --------------------------------------------- | --------------- | ---------------- | ------------------ | ------------------- | ---------------- |
|
|
31
31
|
| routing/simple-routes (/) | 10.90k | 70.10k | 2.04 MB/sec | 11.57 MB/sec | **6.43X** |
|
|
@@ -38,25 +38,19 @@ Tested using [wrk](https://github.com/wg/wrk) (`-d 60 -t 1 -c 200`). Etag was di
|
|
|
38
38
|
|
|
39
39
|
### Performance against other frameworks
|
|
40
40
|
|
|
41
|
-
Tested using [bun-http-framework-benchmark](https://github.com/dimdenGD/bun-http-framework-benchmark)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
|
45
|
-
|
|
|
46
|
-
|
|
|
47
|
-
|
|
|
48
|
-
|
|
|
49
|
-
|
|
|
50
|
-
|
|
|
51
|
-
|
|
|
52
|
-
|
|
|
53
|
-
|
|
|
54
|
-
| fastify | node | 33,094.62 | 40,147.67 | 40,076.35 | 19,059.84 |
|
|
55
|
-
| oak | bun | 32,705.36 | 35,856.59 | 32,116.4 | 30,143.09 |
|
|
56
|
-
| hono | node | 26,576.02 | 36,215.35 | 34,656.12 | 8,856.59 |
|
|
57
|
-
| acorn | deno | 24,476.67 | 29,690.42 | 22,254.82 | 21,484.77 |
|
|
58
|
-
| koa | node | 24,045.08 | 28,202.12 | 24,590.84 | 19,342.28 |
|
|
59
|
-
| express | node | 10,411.313 | 11,245.57 | 10,598.74 | 9,389.63 |
|
|
41
|
+
Tested using [bun-http-framework-benchmark](https://github.com/dimdenGD/bun-http-framework-benchmark). This table only includes Node.js results.
|
|
42
|
+
For full table with other runtimes, check [here](https://github.com/dimdenGD/bun-http-framework-benchmark?tab=readme-ov-file#results).
|
|
43
|
+
|
|
44
|
+
| Framework | Average | Ping | Query | Body |
|
|
45
|
+
| ---------------- | ------- | ---------- | ---------- | ---------- |
|
|
46
|
+
| uws | 94,296.49 | 108,551.92 | 104,756.22 | 69,581.33 |
|
|
47
|
+
| hyper-express | 66,356.707 | 80,002.53 | 69,953.76 | 49,113.83 |
|
|
48
|
+
| **ultimate-express** | **46,826.31** | **50,764.93** | **49,117.76** | **40,596.24** |
|
|
49
|
+
| h3 | 35,423.263 | 41,243.68 | 34,429.26 | 30,596.85 |
|
|
50
|
+
| fastify | 33,094.62 | 40,147.67 | 40,076.35 | 19,059.84 |
|
|
51
|
+
| hono | 26,576.02 | 36,215.35 | 34,656.12 | 8,856.59 |
|
|
52
|
+
| koa | 24,045.08 | 28,202.12 | 24,590.84 | 19,342.28 |
|
|
53
|
+
| express | 10,411.313 | 11,245.57 | 10,598.74 | 9,389.63 |
|
|
60
54
|
|
|
61
55
|
### Performance on real-world application
|
|
62
56
|
|
|
@@ -106,7 +100,7 @@ app.listen(3000, () => {
|
|
|
106
100
|
|
|
107
101
|
1. µExpress tries to optimize routing as much as possible, but it's only possible if:
|
|
108
102
|
- `case sensitive routing` is enabled (it is by default, unlike in normal Express).
|
|
109
|
-
- only string paths without regex characters like *, +, (), {},
|
|
103
|
+
- only string paths without regex characters like *, +, (), {}, etc. can be optimized.
|
|
110
104
|
- only 1-level deep routers can be optimized.
|
|
111
105
|
|
|
112
106
|
Optimized routes can be up to 10 times faster than normal routes, as they're using native uWS router and have pre-calculated path.
|
|
@@ -136,8 +130,7 @@ In general, basically all features and options are supported. Use [Express 4.x d
|
|
|
136
130
|
|
|
137
131
|
- ✅ express()
|
|
138
132
|
- ✅ express.Router()
|
|
139
|
-
-
|
|
140
|
-
- - ❌ options.inflate
|
|
133
|
+
- ✅ express.json()
|
|
141
134
|
- ✅ express.urlencoded()
|
|
142
135
|
- ✅ express.static()
|
|
143
136
|
- - Additionally you can pass `options.ifModifiedSince` to support If-Modified-Since header (this header is not supported in normal Express, but is supported in µExpress)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultimate-express",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.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": {
|
|
@@ -39,12 +39,12 @@
|
|
|
39
39
|
"homepage": "https://github.com/dimdenGD/ultimate-express#readme",
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"accepts": "^1.3.8",
|
|
42
|
-
"body-parser": "^1.20.3",
|
|
43
42
|
"cookie": "^0.6.0",
|
|
44
43
|
"cookie-signature": "^1.2.1",
|
|
45
44
|
"encodeurl": "^2.0.0",
|
|
46
45
|
"etag": "^1.8.1",
|
|
47
46
|
"fast-querystring": "^1.1.2",
|
|
47
|
+
"fast-zlib": "^2.0.1",
|
|
48
48
|
"fresh": "^0.5.2",
|
|
49
49
|
"mime-types": "^2.1.35",
|
|
50
50
|
"ms": "^2.1.3",
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"vary": "^1.1.2"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
+
"body-parser": "^1.20.3",
|
|
61
62
|
"cookie-parser": "^1.4.6",
|
|
62
63
|
"cookie-session": "^2.1.0",
|
|
63
64
|
"cors": "^2.8.5",
|
package/src/index.js
CHANGED
|
@@ -16,8 +16,7 @@ limitations under the License.
|
|
|
16
16
|
|
|
17
17
|
const Application = require("./application.js");
|
|
18
18
|
const Router = require("./router.js");
|
|
19
|
-
const
|
|
20
|
-
const { static, json } = require("./middlewares.js");
|
|
19
|
+
const middlewares = require("./middlewares.js");
|
|
21
20
|
const Request = require("./request.js");
|
|
22
21
|
const Response = require("./response.js");
|
|
23
22
|
|
|
@@ -28,11 +27,11 @@ Application.Router = function(options) {
|
|
|
28
27
|
Application.request = Request.prototype;
|
|
29
28
|
Application.response = Response.prototype;
|
|
30
29
|
|
|
31
|
-
Application.static = static;
|
|
30
|
+
Application.static = middlewares.static;
|
|
32
31
|
|
|
33
|
-
Application.json = json;
|
|
34
|
-
Application.urlencoded =
|
|
35
|
-
Application.text =
|
|
36
|
-
Application.raw =
|
|
32
|
+
Application.json = middlewares.json;
|
|
33
|
+
Application.urlencoded = middlewares.urlencoded;
|
|
34
|
+
Application.text = middlewares.text;
|
|
35
|
+
Application.raw = middlewares.raw;
|
|
37
36
|
|
|
38
37
|
module.exports = Application;
|
package/src/middlewares.js
CHANGED
|
@@ -17,6 +17,10 @@ limitations under the License.
|
|
|
17
17
|
const fs = require('fs');
|
|
18
18
|
const path = require('path');
|
|
19
19
|
const bytes = require('bytes');
|
|
20
|
+
const zlib = require('fast-zlib');
|
|
21
|
+
const typeis = require('type-is');
|
|
22
|
+
const querystring = require('fast-querystring');
|
|
23
|
+
const { fastQueryParse } = require('./utils.js');
|
|
20
24
|
|
|
21
25
|
function static(root, options) {
|
|
22
26
|
if(!options) options = {};
|
|
@@ -109,91 +113,204 @@ function static(root, options) {
|
|
|
109
113
|
}
|
|
110
114
|
}
|
|
111
115
|
|
|
112
|
-
function
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
116
|
+
function createInflate(contentEncoding) {
|
|
117
|
+
const encoding = (contentEncoding || 'identity').toLowerCase();
|
|
118
|
+
switch(encoding) {
|
|
119
|
+
case 'identity':
|
|
120
|
+
return;
|
|
121
|
+
case 'deflate':
|
|
122
|
+
return new zlib.Inflate();
|
|
123
|
+
case 'gzip':
|
|
124
|
+
return new zlib.Gunzip();
|
|
125
|
+
case 'br':
|
|
126
|
+
return new zlib.BrotliDecompress();
|
|
127
|
+
default:
|
|
128
|
+
return false;
|
|
122
129
|
}
|
|
130
|
+
}
|
|
123
131
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if(!type || contentType !== options.type) {
|
|
129
|
-
return next();
|
|
132
|
+
function createBodyParser(defaultType, beforeReturn) {
|
|
133
|
+
return function(options) {
|
|
134
|
+
if(typeof options !== 'object') {
|
|
135
|
+
options = {};
|
|
130
136
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
137
|
+
if(typeof options.limit === 'undefined') options.limit = bytes('100kb');
|
|
138
|
+
else options.limit = bytes(options.limit);
|
|
139
|
+
|
|
140
|
+
if(typeof options.type === 'undefined') options.type = defaultType;
|
|
141
|
+
else if(typeof options.type !== 'string') {
|
|
142
|
+
throw new Error('type must be a string');
|
|
134
143
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
(!additionalMethods || !additionalMethods.includes(req.method))
|
|
144
|
-
) {
|
|
145
|
-
return next();
|
|
144
|
+
if(typeof options.inflate === 'undefined') options.inflate = true;
|
|
145
|
+
if(typeof options.type === 'string') {
|
|
146
|
+
if(!options.type.includes("*")) {
|
|
147
|
+
options.simpleType = options.type;
|
|
148
|
+
}
|
|
149
|
+
options.type = [options.type];
|
|
150
|
+
} else if(typeof options.type !== 'function' && !Array.isArray(options.type)) {
|
|
151
|
+
throw new Error('type must be a string, function or an array');
|
|
146
152
|
}
|
|
153
|
+
if(typeof options.defaultCharset === 'undefined') options.defaultCharset = 'utf-8';
|
|
154
|
+
|
|
155
|
+
return (req, res, next) => {
|
|
156
|
+
const type = req.headers['content-type'];
|
|
157
|
+
|
|
158
|
+
// skip reading body for non-json content type
|
|
159
|
+
if(!type) {
|
|
160
|
+
return next();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// skip reading body twice
|
|
164
|
+
if(req.body) {
|
|
165
|
+
return next();
|
|
166
|
+
}
|
|
147
167
|
|
|
148
|
-
|
|
149
|
-
|
|
168
|
+
const length = req.headers['content-length'];
|
|
169
|
+
// skip reading empty body
|
|
170
|
+
if(length == '0') {
|
|
171
|
+
return next();
|
|
172
|
+
}
|
|
150
173
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
totalSize += ab.length;
|
|
154
|
-
if(totalSize > options.limit) {
|
|
174
|
+
// skip reading too large body
|
|
175
|
+
if(length && +length > options.limit) {
|
|
155
176
|
return next(new Error('Request entity too large'));
|
|
156
177
|
}
|
|
157
|
-
}
|
|
158
178
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
} catch(e) {
|
|
165
|
-
return next(e);
|
|
179
|
+
if(options.simpleType) {
|
|
180
|
+
const semicolonIndex = type.indexOf(';');
|
|
181
|
+
const clearType = semicolonIndex !== -1 ? type.substring(0, semicolonIndex) : type;
|
|
182
|
+
if(clearType !== options.simpleType) {
|
|
183
|
+
return next();
|
|
166
184
|
}
|
|
185
|
+
} else {
|
|
186
|
+
if(typeof options.type === 'function') {
|
|
187
|
+
if(!options.type(req)) {
|
|
188
|
+
return next();
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
if(!typeis(req, options.type)) {
|
|
192
|
+
return next();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// skip reading body for non-POST requests
|
|
198
|
+
// this makes it +10k req/sec faster
|
|
199
|
+
const additionalMethods = req.app.get('body methods');
|
|
200
|
+
if(
|
|
201
|
+
req.method !== 'POST' &&
|
|
202
|
+
req.method !== 'PUT' &&
|
|
203
|
+
req.method !== 'PATCH' &&
|
|
204
|
+
(!additionalMethods || !additionalMethods.includes(req.method))
|
|
205
|
+
) {
|
|
206
|
+
return next();
|
|
167
207
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
208
|
+
|
|
209
|
+
const abs = [];
|
|
210
|
+
let inflate;
|
|
211
|
+
let totalSize = 0;
|
|
212
|
+
if(options.inflate) {
|
|
213
|
+
inflate = createInflate(req.headers['content-encoding']);
|
|
214
|
+
if(inflate === false) {
|
|
215
|
+
return next(new Error('Unsupported content encoding'));
|
|
172
216
|
}
|
|
173
217
|
}
|
|
174
|
-
next();
|
|
175
|
-
}
|
|
176
218
|
|
|
177
|
-
|
|
219
|
+
function onData(buf) {
|
|
220
|
+
if(!Buffer.isBuffer(buf)) {
|
|
221
|
+
buf = Buffer.from(buf);
|
|
222
|
+
}
|
|
223
|
+
if(inflate) {
|
|
224
|
+
buf = inflate.process(buf);
|
|
225
|
+
}
|
|
226
|
+
abs.push(buf);
|
|
227
|
+
totalSize += buf.length;
|
|
228
|
+
if(totalSize > options.limit) {
|
|
229
|
+
return next(new Error('Request entity too large'));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function onEnd() {
|
|
234
|
+
const buf = Buffer.concat(abs);
|
|
235
|
+
if(options.verify) {
|
|
236
|
+
try {
|
|
237
|
+
options.verify(req, res, buf);
|
|
238
|
+
} catch(e) {
|
|
239
|
+
return next(e);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
beforeReturn(req, res, next, options, buf);
|
|
243
|
+
}
|
|
244
|
+
|
|
178
245
|
// reading data directly from uWS is faster than from a stream
|
|
179
246
|
// if we are fast enough (not async), we can do it
|
|
180
247
|
// otherwise we need to use a stream since it already started streaming it
|
|
181
|
-
req.
|
|
182
|
-
onData(ab)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
248
|
+
if(!req.receivedData) {
|
|
249
|
+
req._res.onData((ab, isLast) => {
|
|
250
|
+
onData(ab);
|
|
251
|
+
if(isLast) {
|
|
252
|
+
onEnd();
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
} else {
|
|
256
|
+
req.on('data', onData);
|
|
257
|
+
req.on('end', onEnd);
|
|
258
|
+
}
|
|
190
259
|
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
191
262
|
|
|
263
|
+
const json = createBodyParser('application/json', function(req, res, next, options, buf) {
|
|
264
|
+
if(options.strict) {
|
|
265
|
+
if(req.body && typeof req.body !== 'object') {
|
|
266
|
+
return next(new Error('Invalid body'));
|
|
267
|
+
}
|
|
192
268
|
}
|
|
269
|
+
req.body = JSON.parse(buf.toString(), options.reviver);
|
|
270
|
+
next();
|
|
271
|
+
});
|
|
193
272
|
|
|
194
|
-
|
|
273
|
+
const raw = createBodyParser('application/octet-stream', function(req, res, next, options, buf) {
|
|
274
|
+
req.body = buf;
|
|
275
|
+
next();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const text = createBodyParser('text/plain', function(req, res, next, options, buf) {
|
|
279
|
+
let contentType = req.headers['content-type'];
|
|
280
|
+
let charsetIndex = contentType.indexOf('charset=');
|
|
281
|
+
let encoding = options.defaultCharset;
|
|
282
|
+
if(charsetIndex !== -1) {
|
|
283
|
+
encoding = contentType.substring(charsetIndex + 8);
|
|
284
|
+
const semicolonIndex = encoding.indexOf(';');
|
|
285
|
+
if(semicolonIndex !== -1) {
|
|
286
|
+
encoding = encoding.substring(0, semicolonIndex);
|
|
287
|
+
}
|
|
288
|
+
encoding = encoding.trim().toLowerCase();
|
|
289
|
+
}
|
|
290
|
+
if(encoding !== 'utf-8' && encoding !== 'utf-16le' && encoding !== 'latin1') {
|
|
291
|
+
return next(new Error('Unsupported charset'));
|
|
292
|
+
}
|
|
293
|
+
req.body = buf.toString(encoding);
|
|
294
|
+
next();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const urlencoded = createBodyParser('application/x-www-form-urlencoded', function(req, res, next, options, buf) {
|
|
298
|
+
try {
|
|
299
|
+
if(options.extended) {
|
|
300
|
+
req.body = fastQueryParse(buf.toString(), options);
|
|
301
|
+
} else {
|
|
302
|
+
req.body = querystring.parse(buf.toString());
|
|
303
|
+
}
|
|
304
|
+
} catch(e) {
|
|
305
|
+
return next(e);
|
|
306
|
+
}
|
|
307
|
+
next();
|
|
308
|
+
});
|
|
195
309
|
|
|
196
310
|
module.exports = {
|
|
197
311
|
static,
|
|
198
|
-
json
|
|
312
|
+
json,
|
|
313
|
+
raw,
|
|
314
|
+
text,
|
|
315
|
+
urlencoded,
|
|
199
316
|
};
|
package/src/utils.js
CHANGED
|
@@ -22,13 +22,13 @@ const querystring = require("fast-querystring");
|
|
|
22
22
|
const etag = require("etag");
|
|
23
23
|
const { Stats } = require("fs");
|
|
24
24
|
|
|
25
|
-
function fastQueryParse(query) {
|
|
25
|
+
function fastQueryParse(query, options) {
|
|
26
26
|
if(query.length <= 128) {
|
|
27
27
|
if(!query.includes('[') && !query.includes('%5B') && !query.includes('.') && !query.includes('%2E')) {
|
|
28
28
|
return querystring.parse(query);
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
-
return qs.parse(query);
|
|
31
|
+
return qs.parse(query, options);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
function removeDuplicateSlashes(path) {
|