ultimate-express 1.2.5 → 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** | **46,139.797** | **49,010.91** | **49,197.87** | **40,210.61** |
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.5",
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,91 +113,193 @@ 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;
165
+ const length = req.headers['content-length'];
166
+ // skip reading empty body
167
+ if(length == '0') {
168
+ return next();
169
+ }
150
170
 
151
- function onData(ab) {
152
- abs.push(Buffer.from(ab));
153
- totalSize += ab.length;
154
- if(totalSize > options.limit) {
171
+ // skip reading too large body
172
+ if(length && +length > options.limit) {
155
173
  return next(new Error('Request entity too large'));
156
174
  }
157
- }
158
175
 
159
- function onEnd() {
160
- const buf = Buffer.concat(abs);
161
- if(options.verify) {
162
- try {
163
- options.verify(req, res, buf);
164
- } catch(e) {
165
- return next(e);
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();
166
183
  }
167
184
  }
168
- req.body = JSON.parse(buf, options.reviver);
169
- if(options.strict) {
170
- if(req.body && typeof req.body !== 'object') {
171
- return next(new Error('Invalid body'));
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'));
172
205
  }
173
206
  }
174
- next();
175
- }
176
207
 
177
- if(!req.receivedData) {
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() {
223
+ const buf = Buffer.concat(abs);
224
+ if(options.verify) {
225
+ try {
226
+ options.verify(req, res, buf);
227
+ } catch(e) {
228
+ return next(e);
229
+ }
230
+ }
231
+ beforeReturn(req, res, next, options, buf);
232
+ }
233
+
178
234
  // reading data directly from uWS is faster than from a stream
179
235
  // if we are fast enough (not async), we can do it
180
236
  // otherwise we need to use a stream since it already started streaming it
181
- req._res.onData((ab, isLast) => {
182
- onData(ab);
183
- if(isLast) {
184
- onEnd();
185
- }
186
- });
187
- } else {
188
- req.on('data', onData);
189
- req.on('end', onEnd);
237
+ if(!req.receivedData) {
238
+ req._res.onData((ab, isLast) => {
239
+ onData(ab);
240
+ if(isLast) {
241
+ onEnd();
242
+ }
243
+ });
244
+ } else {
245
+ req.on('data', onData);
246
+ req.on('end', onEnd);
247
+ }
190
248
  }
249
+ }
250
+ }
191
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
+ }
192
257
  }
258
+ req.body = JSON.parse(buf.toString(), options.reviver);
259
+ next();
260
+ });
193
261
 
194
- }
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
+ });
195
298
 
196
299
  module.exports = {
197
300
  static,
198
- json
301
+ json,
302
+ raw,
303
+ text,
304
+ urlencoded,
199
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) {