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 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
- | Framework | Runtime | Average | Ping | Query | Body |
44
- | ---------------- | ------- | ------- | ---------- | ---------- | ---------- |
45
- | uws | node | 94,296.49 | 108,551.92 | 104,756.22 | 69,581.33 |
46
- | bun | bun | 74,824.52 | 85,839.42 | 74,668.88 | 63,965.26 |
47
- | elysia | bun | 72,112.447 | 82,589.71 | 69,356.08 | 64,391.55 |
48
- | hyper-express | node | 66,356.707 | 80,002.53 | 69,953.76 | 49,113.83 |
49
- | hono | bun | 63,944.627 | 74,550.47 | 62,810.28 | 54,473.13 |
50
- | **ultimate-express** | **node** | **44,081.737** | **51,753.24** | **48,389.84** | **32,102.13** |
51
- | oak | deno | 40,878.467 | 68,429.24 | 28,541.99 | 25,664.17 |
52
- | express | bun | 35,937.977 | 41,329.97 | 34,339.79 | 32,144.17 |
53
- | h3 | node | 35,423.263 | 41,243.68 | 34,429.26 | 30,596.85 |
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 *, +, (), {}, :param, etc. can be optimized.
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
- - 🚧 express.json()
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.4",
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 bodyParser = require("body-parser");
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 = bodyParser.urlencoded;
35
- Application.text = bodyParser.text;
36
- Application.raw = bodyParser.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;
@@ -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 json(options = {}) {
113
- if(typeof options !== 'object') {
114
- options = {};
115
- }
116
- if(typeof options.limit === 'undefined') options.limit = bytes('100kb');
117
- else options.limit = bytes(options.limit);
118
-
119
- if(typeof options.type === 'undefined') options.type = 'application/json';
120
- else if(typeof options.type !== 'string') {
121
- throw new Error('type must be a string');
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
- return (req, res, next) => {
125
- const type = req.headers['content-type'];
126
- const semiColonIndex = type.indexOf(';');
127
- const contentType = semiColonIndex !== -1 ? type.substring(0, semiColonIndex) : type;
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
- // skip reading body twice
132
- if(req.body) {
133
- return next();
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
- // skip reading body for non-POST requests
137
- // this makes it +10k req/sec faster
138
- const additionalMethods = req.app.get('body methods');
139
- if(
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
- const abs = []
149
- let totalSize = 0;
150
- req._res.onData((ab, isLast) => {
151
- abs.push(Buffer.from(ab));
152
- totalSize += ab.length;
153
- if(totalSize > options.limit) {
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
- if(isLast) {
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.body = JSON.parse(buf, options.reviver);
166
- if(options.strict) {
167
- if(req.body && typeof req.body !== 'object') {
168
- return next(new Error('Invalid body'));
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
- next();
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) {