ultimate-express 1.2.4 → 1.2.6
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 +169 -46
- 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.6",
|
|
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,51 +113,113 @@ 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
|
-
req.method !== 'POST' &&
|
|
141
|
-
req.method !== 'PUT' &&
|
|
142
|
-
req.method !== 'PATCH' &&
|
|
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
|
+
options.type = [options.type];
|
|
147
|
+
} else if(typeof options.type !== 'function' && !Array.isArray(options.type)) {
|
|
148
|
+
throw new Error('type must be a string, function or an array');
|
|
146
149
|
}
|
|
150
|
+
if(typeof options.defaultCharset === 'undefined') options.defaultCharset = 'utf-8';
|
|
151
|
+
|
|
152
|
+
return (req, res, next) => {
|
|
153
|
+
const type = req.headers['content-type'];
|
|
154
|
+
|
|
155
|
+
// skip reading body for non-json content type
|
|
156
|
+
if(!type) {
|
|
157
|
+
return next();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// skip reading body twice
|
|
161
|
+
if(req.body) {
|
|
162
|
+
return next();
|
|
163
|
+
}
|
|
147
164
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
165
|
+
const length = req.headers['content-length'];
|
|
166
|
+
// skip reading empty body
|
|
167
|
+
if(length == '0') {
|
|
168
|
+
return next();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// skip reading too large body
|
|
172
|
+
if(length && +length > options.limit) {
|
|
154
173
|
return next(new Error('Request entity too large'));
|
|
155
174
|
}
|
|
156
|
-
|
|
175
|
+
|
|
176
|
+
if(typeof options.type === 'function') {
|
|
177
|
+
if(!options.type(req)) {
|
|
178
|
+
return next();
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
if(!typeis(req, options.type)) {
|
|
182
|
+
return next();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// skip reading body for non-POST requests
|
|
187
|
+
// this makes it +10k req/sec faster
|
|
188
|
+
const additionalMethods = req.app.get('body methods');
|
|
189
|
+
if(
|
|
190
|
+
req.method !== 'POST' &&
|
|
191
|
+
req.method !== 'PUT' &&
|
|
192
|
+
req.method !== 'PATCH' &&
|
|
193
|
+
(!additionalMethods || !additionalMethods.includes(req.method))
|
|
194
|
+
) {
|
|
195
|
+
return next();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const abs = [];
|
|
199
|
+
let inflate;
|
|
200
|
+
let totalSize = 0;
|
|
201
|
+
if(options.inflate) {
|
|
202
|
+
inflate = createInflate(req.headers['content-encoding']);
|
|
203
|
+
if(inflate === false) {
|
|
204
|
+
return next(new Error('Unsupported content encoding'));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function onData(buf) {
|
|
209
|
+
if(!Buffer.isBuffer(buf)) {
|
|
210
|
+
buf = Buffer.from(buf);
|
|
211
|
+
}
|
|
212
|
+
if(inflate) {
|
|
213
|
+
buf = inflate.process(buf);
|
|
214
|
+
}
|
|
215
|
+
abs.push(buf);
|
|
216
|
+
totalSize += buf.length;
|
|
217
|
+
if(totalSize > options.limit) {
|
|
218
|
+
return next(new Error('Request entity too large'));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function onEnd() {
|
|
157
223
|
const buf = Buffer.concat(abs);
|
|
158
224
|
if(options.verify) {
|
|
159
225
|
try {
|
|
@@ -162,21 +228,78 @@ function json(options = {}) {
|
|
|
162
228
|
return next(e);
|
|
163
229
|
}
|
|
164
230
|
}
|
|
165
|
-
req
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
231
|
+
beforeReturn(req, res, next, options, buf);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// reading data directly from uWS is faster than from a stream
|
|
235
|
+
// if we are fast enough (not async), we can do it
|
|
236
|
+
// otherwise we need to use a stream since it already started streaming it
|
|
237
|
+
if(!req.receivedData) {
|
|
238
|
+
req._res.onData((ab, isLast) => {
|
|
239
|
+
onData(ab);
|
|
240
|
+
if(isLast) {
|
|
241
|
+
onEnd();
|
|
169
242
|
}
|
|
170
|
-
}
|
|
171
|
-
|
|
243
|
+
});
|
|
244
|
+
} else {
|
|
245
|
+
req.on('data', onData);
|
|
246
|
+
req.on('end', onEnd);
|
|
172
247
|
}
|
|
173
|
-
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
174
251
|
|
|
252
|
+
const json = createBodyParser('application/json', function(req, res, next, options, buf) {
|
|
253
|
+
if(options.strict) {
|
|
254
|
+
if(req.body && typeof req.body !== 'object') {
|
|
255
|
+
return next(new Error('Invalid body'));
|
|
256
|
+
}
|
|
175
257
|
}
|
|
258
|
+
req.body = JSON.parse(buf.toString(), options.reviver);
|
|
259
|
+
next();
|
|
260
|
+
});
|
|
176
261
|
|
|
177
|
-
|
|
262
|
+
const raw = createBodyParser('application/octet-stream', function(req, res, next, options, buf) {
|
|
263
|
+
req.body = buf;
|
|
264
|
+
next();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const text = createBodyParser('text/plain', function(req, res, next, options, buf) {
|
|
268
|
+
let contentType = req.headers['content-type'];
|
|
269
|
+
let charsetIndex = contentType.indexOf('charset=');
|
|
270
|
+
let encoding = options.defaultCharset;
|
|
271
|
+
if(charsetIndex !== -1) {
|
|
272
|
+
encoding = contentType.substring(charsetIndex + 8);
|
|
273
|
+
const semicolonIndex = encoding.indexOf(';');
|
|
274
|
+
if(semicolonIndex !== -1) {
|
|
275
|
+
encoding = encoding.substring(0, semicolonIndex);
|
|
276
|
+
}
|
|
277
|
+
encoding = encoding.trim().toLowerCase();
|
|
278
|
+
}
|
|
279
|
+
if(encoding !== 'utf-8' && encoding !== 'utf-16le' && encoding !== 'latin1') {
|
|
280
|
+
return next(new Error('Unsupported charset'));
|
|
281
|
+
}
|
|
282
|
+
req.body = buf.toString(encoding);
|
|
283
|
+
next();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const urlencoded = createBodyParser('application/x-www-form-urlencoded', function(req, res, next, options, buf) {
|
|
287
|
+
try {
|
|
288
|
+
if(options.extended) {
|
|
289
|
+
req.body = fastQueryParse(buf.toString(), options);
|
|
290
|
+
} else {
|
|
291
|
+
req.body = querystring.parse(buf.toString());
|
|
292
|
+
}
|
|
293
|
+
} catch(e) {
|
|
294
|
+
return next(e);
|
|
295
|
+
}
|
|
296
|
+
next();
|
|
297
|
+
});
|
|
178
298
|
|
|
179
299
|
module.exports = {
|
|
180
300
|
static,
|
|
181
|
-
json
|
|
301
|
+
json,
|
|
302
|
+
raw,
|
|
303
|
+
text,
|
|
304
|
+
urlencoded,
|
|
182
305
|
};
|
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) {
|