@morojs/moro 1.7.28 → 1.7.30

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.
Files changed (41) hide show
  1. package/dist/core/config/schema.js +3 -1
  2. package/dist/core/config/schema.js.map +1 -1
  3. package/dist/core/framework.d.ts +1 -1
  4. package/dist/core/framework.js +42 -28
  5. package/dist/core/framework.js.map +1 -1
  6. package/dist/core/http/http-server.d.ts +116 -12
  7. package/dist/core/http/http-server.js +1171 -862
  8. package/dist/core/http/http-server.js.map +1 -1
  9. package/dist/core/http/uws-http-server.d.ts +72 -5
  10. package/dist/core/http/uws-http-server.js +718 -150
  11. package/dist/core/http/uws-http-server.js.map +1 -1
  12. package/dist/core/logger/logger.d.ts +2 -0
  13. package/dist/core/logger/logger.js +32 -6
  14. package/dist/core/logger/logger.js.map +1 -1
  15. package/dist/core/middleware/built-in/compression/core.js +47 -29
  16. package/dist/core/middleware/built-in/compression/core.js.map +1 -1
  17. package/dist/core/middleware/built-in/error-tracker/middleware.d.ts +5 -2
  18. package/dist/core/middleware/built-in/error-tracker/middleware.js +13 -11
  19. package/dist/core/middleware/built-in/error-tracker/middleware.js.map +1 -1
  20. package/dist/core/middleware/built-in/index.d.ts +3 -3
  21. package/dist/core/middleware/built-in/performance-monitor/middleware.d.ts +2 -1
  22. package/dist/core/middleware/built-in/performance-monitor/middleware.js +7 -6
  23. package/dist/core/middleware/built-in/performance-monitor/middleware.js.map +1 -1
  24. package/dist/core/middleware/built-in/request-logger/middleware.d.ts +2 -1
  25. package/dist/core/middleware/built-in/request-logger/middleware.js +5 -5
  26. package/dist/core/middleware/built-in/request-logger/middleware.js.map +1 -1
  27. package/dist/core/middleware/built-in/validation/core.js +61 -27
  28. package/dist/core/middleware/built-in/validation/core.js.map +1 -1
  29. package/dist/core/routing/radix-tree.d.ts +1 -0
  30. package/dist/core/routing/radix-tree.js +48 -10
  31. package/dist/core/routing/radix-tree.js.map +1 -1
  32. package/dist/core/routing/unified-router.d.ts +16 -0
  33. package/dist/core/routing/unified-router.js +39 -12
  34. package/dist/core/routing/unified-router.js.map +1 -1
  35. package/dist/core/utilities/hooks.d.ts +2 -0
  36. package/dist/core/utilities/hooks.js +12 -0
  37. package/dist/core/utilities/hooks.js.map +1 -1
  38. package/dist/moro.d.ts +1 -0
  39. package/dist/moro.js +44 -68
  40. package/dist/moro.js.map +1 -1
  41. package/package.json +1 -1
@@ -1,8 +1,268 @@
1
1
  // uWebSockets.js HTTP Server Implementation for Moro Framework
2
2
  // Provides high-performance HTTP and WebSocket server using uWebSockets.js
3
3
  import cluster from 'cluster';
4
+ import { randomUUID } from 'crypto';
4
5
  import { createFrameworkLogger } from '../logger/index.js';
5
- import { ObjectPoolManager } from '../pooling/object-pool-manager.js';
6
+ function parseUwsQueryString(queryString) {
7
+ const result = {};
8
+ if (!queryString)
9
+ return result;
10
+ const pairs = queryString.split('&');
11
+ for (let i = 0; i < pairs.length; i++) {
12
+ const pair = pairs[i];
13
+ const eqIdx = pair.indexOf('=');
14
+ if (eqIdx > 0) {
15
+ const key = pair.substring(0, eqIdx);
16
+ const value = pair.substring(eqIdx + 1);
17
+ try {
18
+ result[decodeURIComponent(key)] = decodeURIComponent(value);
19
+ }
20
+ catch {
21
+ result[key] = value;
22
+ }
23
+ }
24
+ }
25
+ return result;
26
+ }
27
+ /**
28
+ * Prototype-based request object for the uWS adapter.
29
+ *
30
+ * Headers/query/cookies/requestId are lazy: a synchronous handler that never
31
+ * reads them pays nothing (no header copy, no UUID). The underlying uWS
32
+ * request is only valid until the route callback returns to the event loop,
33
+ * so `materialize()` MUST be called before any `await` - it snapshots the
34
+ * headers and detaches the native reference.
35
+ */
36
+ export class UwsRequest {
37
+ method;
38
+ path;
39
+ url;
40
+ params = {};
41
+ body = null;
42
+ ip = '';
43
+ _server;
44
+ _uwsReq; // valid ONLY synchronously inside the uWS route callback
45
+ _queryString;
46
+ _query = undefined;
47
+ _headers = undefined;
48
+ _cookies = undefined;
49
+ _context = undefined;
50
+ _requestId = undefined;
51
+ constructor(server, uwsReq, method, url, queryString) {
52
+ this._server = server;
53
+ this._uwsReq = uwsReq;
54
+ this.method = method;
55
+ this.path = url;
56
+ this.url = url;
57
+ this._queryString = queryString || null;
58
+ }
59
+ /** Snapshot uWS-backed data and detach the native request. Idempotent.
60
+ * Must be called before returning control to the event loop if the
61
+ * request object outlives the synchronous route callback. */
62
+ materialize() {
63
+ if (this._headers === undefined) {
64
+ // Force the headers snapshot while the native request is still valid
65
+ void this.headers;
66
+ }
67
+ this._uwsReq = null;
68
+ }
69
+ get headers() {
70
+ let h = this._headers;
71
+ if (h === undefined) {
72
+ h = {};
73
+ const r = this._uwsReq;
74
+ if (r) {
75
+ try {
76
+ r.forEach((key, value) => {
77
+ h[key] = value;
78
+ });
79
+ }
80
+ catch {
81
+ // Native request no longer valid - snapshot window was missed
82
+ }
83
+ }
84
+ this._headers = h;
85
+ }
86
+ return h;
87
+ }
88
+ set headers(value) {
89
+ this._headers = value;
90
+ }
91
+ get query() {
92
+ let q = this._query;
93
+ if (q === undefined) {
94
+ const qs = this._queryString;
95
+ q = qs ? parseUwsQueryString(qs) : {};
96
+ this._query = q;
97
+ }
98
+ return q;
99
+ }
100
+ set query(value) {
101
+ this._query = value;
102
+ }
103
+ get cookies() {
104
+ let c = this._cookies;
105
+ if (c === undefined) {
106
+ c = {};
107
+ const header = this.headers.cookie;
108
+ if (header) {
109
+ const parts = header.split(';');
110
+ for (let i = 0; i < parts.length; i++) {
111
+ const part = parts[i];
112
+ const eq = part.indexOf('=');
113
+ if (eq > 0) {
114
+ const name = part.substring(0, eq).trim();
115
+ const value = part.substring(eq + 1);
116
+ if (name && value) {
117
+ try {
118
+ c[name] = decodeURIComponent(value);
119
+ }
120
+ catch {
121
+ c[name] = value;
122
+ }
123
+ }
124
+ }
125
+ }
126
+ }
127
+ this._cookies = c;
128
+ }
129
+ return c;
130
+ }
131
+ set cookies(value) {
132
+ this._cookies = value;
133
+ }
134
+ get context() {
135
+ let ctx = this._context;
136
+ if (ctx === undefined) {
137
+ ctx = {};
138
+ this._context = ctx;
139
+ }
140
+ return ctx;
141
+ }
142
+ set context(value) {
143
+ this._context = value;
144
+ }
145
+ get requestId() {
146
+ let id = this._requestId;
147
+ if (id === undefined) {
148
+ const server = this._server;
149
+ id = server && server.requestTrackingEnabled ? randomUUID() : '';
150
+ this._requestId = id;
151
+ }
152
+ return id;
153
+ }
154
+ set requestId(value) {
155
+ this._requestId = value;
156
+ }
157
+ // ==== Express-compatible request helpers (parity with the Node adapter) ====
158
+ get hostname() {
159
+ const host = this.headers.host || '';
160
+ return host ? host.split(':')[0] : '';
161
+ }
162
+ get protocol() {
163
+ const forwardedProto = this.headers['x-forwarded-proto'];
164
+ if (forwardedProto)
165
+ return forwardedProto.split(',')[0].trim();
166
+ return this._server && this._server.isSsl ? 'https' : 'http';
167
+ }
168
+ get secure() {
169
+ return this.protocol === 'https';
170
+ }
171
+ get xhr() {
172
+ const xrw = this.headers['x-requested-with'];
173
+ return !!xrw && xrw.toLowerCase() === 'xmlhttprequest';
174
+ }
175
+ get originalUrl() {
176
+ const qs = this._queryString;
177
+ return qs ? `${this.url}?${qs}` : this.url;
178
+ }
179
+ get ips() {
180
+ const forwardedFor = this.headers['x-forwarded-for'];
181
+ return forwardedFor
182
+ ? forwardedFor
183
+ .split(',')
184
+ .map(s => s.trim())
185
+ .filter(Boolean)
186
+ : [];
187
+ }
188
+ get subdomains() {
189
+ const hostnameParts = this.hostname.split('.');
190
+ return hostnameParts.length > 2 ? hostnameParts.slice(0, -2).reverse() : [];
191
+ }
192
+ get(name) {
193
+ const lower = name.toLowerCase();
194
+ if (lower === 'referer' || lower === 'referrer') {
195
+ return this.headers.referer || this.headers.referrer;
196
+ }
197
+ return this.headers[lower];
198
+ }
199
+ header(name) {
200
+ return this.get(name);
201
+ }
202
+ is(type) {
203
+ const ct = this.headers['content-type'] || '';
204
+ if (!ct)
205
+ return false;
206
+ const mime = ct.split(';')[0].trim().toLowerCase();
207
+ const t = type.toLowerCase();
208
+ if (t.indexOf('/') === -1) {
209
+ return mime.endsWith(`/${t}`) || mime.endsWith(`+${t}`);
210
+ }
211
+ if (t.endsWith('/*')) {
212
+ return mime.startsWith(t.slice(0, -1));
213
+ }
214
+ return mime === t;
215
+ }
216
+ accepts(types) {
217
+ const accept = this.headers.accept || '*/*';
218
+ if (!types)
219
+ return accept;
220
+ const wanted = Array.isArray(types) ? types : [types];
221
+ if (accept === '*/*' || accept === '')
222
+ return wanted[0] || false;
223
+ const acceptTypes = accept.split(',').map(s => s.split(';')[0].trim().toLowerCase());
224
+ for (const w of wanted) {
225
+ const wl = w.toLowerCase();
226
+ const wMime = wl.indexOf('/') === -1 ? `application/${wl}` : wl;
227
+ for (const at of acceptTypes) {
228
+ if (at === '*/*')
229
+ return w;
230
+ if (at === wMime)
231
+ return w;
232
+ if (at.endsWith('/*') && wMime.startsWith(at.slice(0, -1)))
233
+ return w;
234
+ }
235
+ }
236
+ return false;
237
+ }
238
+ acceptsLanguages(langs) {
239
+ const acceptLang = this.headers['accept-language'] || '';
240
+ if (!langs)
241
+ return acceptLang || false;
242
+ const wanted = Array.isArray(langs) ? langs : [langs];
243
+ if (!acceptLang)
244
+ return wanted[0] || false;
245
+ const accepted = acceptLang
246
+ .split(',')
247
+ .map(s => {
248
+ const parts = s.trim().split(';');
249
+ const tag = parts[0].toLowerCase();
250
+ const qPart = parts.find(p => p.trim().startsWith('q='));
251
+ const q = qPart ? parseFloat(qPart.trim().slice(2)) : 1;
252
+ return { tag, q: isNaN(q) ? 0 : q };
253
+ })
254
+ .filter(a => a.q > 0)
255
+ .sort((a, b) => b.q - a.q);
256
+ for (const { tag } of accepted) {
257
+ for (const w of wanted) {
258
+ const wl = w.toLowerCase();
259
+ if (tag === '*' || tag === wl || tag.split('-')[0] === wl.split('-')[0])
260
+ return w;
261
+ }
262
+ }
263
+ return false;
264
+ }
265
+ }
6
266
  /**
7
267
  * uWebSockets HTTP Server Adapter
8
268
  * Bridges uWebSockets.js with Moro's HTTP abstractions
@@ -15,13 +275,22 @@ export class UWebSocketsHttpServer {
15
275
  logger = createFrameworkLogger('UWSHttpServer');
16
276
  hookManager;
17
277
  requestCounter = 0;
18
- requestTrackingEnabled = true; // Generate request IDs
278
+ requestTrackingEnabled = true; // Generate request IDs (read lazily by UwsRequest)
19
279
  isListening = false;
20
280
  port;
21
281
  host;
22
282
  initPromise;
23
- // Performance optimizations - shared object pooling
24
- poolManager = ObjectPoolManager.getInstance();
283
+ /** @internal read by UwsRequest#protocol */
284
+ isSsl = false;
285
+ // Body size limits (bytes) - configured from server.bodySizeLimit/maxUploadSize
286
+ maxBodySize = 10 * 1024 * 1024;
287
+ maxUploadSize = 100 * 1024 * 1024;
288
+ // Direct router dispatch (set from Moro.listen via setRouterHandler)
289
+ routerHandler;
290
+ // Provider of fast-path routes to register on uWS's native router at listen()
291
+ nativeRouteProvider;
292
+ // Shared error handling for native fast-path routes (unified router semantics)
293
+ nativeErrorHandler;
25
294
  // String interning for common values
26
295
  static INTERNED_METHODS = new Map([
27
296
  ['get', 'GET'],
@@ -54,8 +323,25 @@ export class UWebSocketsHttpServer {
54
323
  [503, '503 Service Unavailable'],
55
324
  ]);
56
325
  constructor(options = {}) {
326
+ if (options.maxBodySize)
327
+ this.maxBodySize = options.maxBodySize;
328
+ if (options.maxUploadSize)
329
+ this.maxUploadSize = options.maxUploadSize;
57
330
  this.initPromise = this.initialize(options);
58
331
  }
332
+ // Direct router dispatch - called after global middleware without the
333
+ // per-middleware promise machinery (mirrors MoroHttpServer.setRouterHandler)
334
+ setRouterHandler(fn) {
335
+ this.routerHandler = fn;
336
+ }
337
+ // Fast-path routes get registered directly on uWS's native C++ router at
338
+ // listen() - zero middleware machinery, lazy headers, no promise for sync
339
+ // handlers. The provider is evaluated at listen time so file-based/late
340
+ // routes are included.
341
+ setNativeRouteProvider(provider, onError) {
342
+ this.nativeRouteProvider = provider;
343
+ this.nativeErrorHandler = onError;
344
+ }
59
345
  async initialize(options) {
60
346
  try {
61
347
  // Lazy load uWebSockets.js - only when explicitly configured
@@ -68,6 +354,7 @@ export class UWebSocketsHttpServer {
68
354
  cert_file_name: options.ssl.cert_file_name,
69
355
  passphrase: options.ssl.passphrase,
70
356
  });
357
+ this.isSsl = true;
71
358
  this.logger.info('uWebSockets SSL/TLS HTTP server created', 'Init');
72
359
  }
73
360
  else {
@@ -89,54 +376,224 @@ export class UWebSocketsHttpServer {
89
376
  }
90
377
  }
91
378
  setupRouteHandlers() {
92
- // Handle all HTTP methods through catchall
93
- // All requests go through middleware chain (includes UnifiedRouter)
379
+ // Catchall fallback for everything that isn't a native fast-path route:
380
+ // middleware chains, hooks, non-fast-path routes, 404s. Native
381
+ // method-specific routes registered later take priority in uWS's router
382
+ // (static > parameter > wildcard).
94
383
  this.app.any('/*', (res, req) => {
95
384
  this.handleRequest(req, res);
96
385
  });
97
386
  }
387
+ // Register fast-path routes (no middleware/auth/validation) directly on
388
+ // uWS's native router. A sync GET handler completes with zero header copies,
389
+ // zero promises and a single corked write. Each wrapper carries a cheap
390
+ // dynamic guard: the moment global middleware or request hooks exist, it
391
+ // falls back to the full pipeline so no feature is ever bypassed.
392
+ registerNativeFastPathRoutes() {
393
+ const provider = this.nativeRouteProvider;
394
+ if (!provider || !this.app)
395
+ return;
396
+ let count = 0;
397
+ for (const route of provider()) {
398
+ const methodFn = route.method.toLowerCase();
399
+ if (typeof this.app[methodFn] !== 'function')
400
+ continue;
401
+ // Only plain paths - uWS shares the :param syntax; anything exotic
402
+ // (wildcards etc.) stays on the catchall/unified-router path
403
+ if (!/^[A-Za-z0-9/_\-.:]*$/.test(route.path) || route.path.indexOf('*') !== -1)
404
+ continue;
405
+ const handler = route.handler;
406
+ const paramNames = route.paramNames;
407
+ const methodInterned = route.method;
408
+ const needsBody = methodInterned === 'POST' || methodInterned === 'PUT' || methodInterned === 'PATCH';
409
+ this.app[methodFn](route.path, (res, req) => {
410
+ // Dynamic feature guard - anything registered at runtime sends the
411
+ // request through the full pipeline instead
412
+ if (this.globalMiddleware.length !== 0 ||
413
+ (this.hookManager &&
414
+ (this.hookManager.hasHooks === undefined || this.hookManager.hasHooks('request')))) {
415
+ this.handleRequest(req, res);
416
+ return;
417
+ }
418
+ this.handleNativeRoute(req, res, handler, methodInterned, paramNames, needsBody);
419
+ });
420
+ count++;
421
+ }
422
+ if (count > 0) {
423
+ this.logger.info(`${count} fast-path routes registered on native uWS router`, 'NativeRoutes');
424
+ }
425
+ }
426
+ handleNativeRoute(req, res, handler, method, paramNames, needsBody) {
427
+ res.aborted = false;
428
+ const httpReq = new UwsRequest(this, req, method, req.getUrl(), req.getQuery());
429
+ if (paramNames) {
430
+ const params = {};
431
+ for (let i = 0; i < paramNames.length; i++) {
432
+ params[paramNames[i]] = req.getParameter(i);
433
+ }
434
+ httpReq.params = params;
435
+ }
436
+ const httpRes = new UWebSocketsHttpServer.ResponsePrototype().init(res, req, this.logger);
437
+ if (needsBody) {
438
+ // Body routes always go async: snapshot headers, arm the abort hook,
439
+ // read the body, then run the handler
440
+ httpReq.materialize();
441
+ res.onAborted(() => {
442
+ res.aborted = true;
443
+ const hook = res._abortHook;
444
+ if (hook)
445
+ hook();
446
+ });
447
+ const contentLength = req.getHeader('content-length');
448
+ const bodyPromise = contentLength && parseInt(contentLength) > 0
449
+ ? this.readBody(res, httpReq)
450
+ : Promise.resolve();
451
+ bodyPromise
452
+ .then(() => {
453
+ if (res.aborted)
454
+ return;
455
+ const result = handler(httpReq, httpRes);
456
+ if (result && typeof result.then === 'function') {
457
+ return result.then(r => {
458
+ if (r !== undefined && !httpRes.headersSent)
459
+ httpRes.json(r);
460
+ });
461
+ }
462
+ if (result !== undefined && !httpRes.headersSent)
463
+ httpRes.json(result);
464
+ })
465
+ .catch(err => this.handleNativeRouteError(err, httpReq, httpRes, res));
466
+ return;
467
+ }
468
+ try {
469
+ const result = handler(httpReq, httpRes);
470
+ if (result && typeof result.then === 'function') {
471
+ // Async handler: it is suspended at its first await - snapshot
472
+ // uWS-backed data and arm abort tracking before returning to the loop
473
+ httpReq.materialize();
474
+ res.onAborted(() => {
475
+ res.aborted = true;
476
+ const hook = res._abortHook;
477
+ if (hook)
478
+ hook();
479
+ });
480
+ result.then(r => {
481
+ if (r !== undefined && !httpRes.headersSent && !res.aborted)
482
+ httpRes.json(r);
483
+ }, err => this.handleNativeRouteError(err, httpReq, httpRes, res));
484
+ }
485
+ else if (result !== undefined && !httpRes.headersSent) {
486
+ // Fully synchronous: single corked write, no header copy, no promise
487
+ httpRes.json(result);
488
+ }
489
+ }
490
+ catch (err) {
491
+ this.handleNativeRouteError(err, httpReq, httpRes, res);
492
+ }
493
+ }
494
+ handleNativeRouteError(err, httpReq, httpRes, res) {
495
+ if (err?.statusCode === 413 && !res.aborted && !httpRes.headersSent) {
496
+ httpRes.statusCode = 413;
497
+ httpRes.setHeader('Content-Type', 'application/json');
498
+ httpRes.end('{"success":false,"error":"Request entity too large"}');
499
+ return;
500
+ }
501
+ const sendFallback = () => {
502
+ if (!res.aborted && !httpRes.headersSent) {
503
+ // Same shape as the unified router's fast-path error response
504
+ httpRes.status(500).json({ error: 'Internal server error' });
505
+ }
506
+ };
507
+ const errorHandler = this.nativeErrorHandler;
508
+ if (errorHandler) {
509
+ errorHandler(httpReq, httpRes, err).then(handled => {
510
+ if (!handled)
511
+ sendFallback();
512
+ }, () => sendFallback());
513
+ }
514
+ else {
515
+ this.logger.error(`Route handler error: ${err instanceof Error ? err.message : String(err)}`, 'NativeRouteError');
516
+ sendFallback();
517
+ }
518
+ }
98
519
  async handleRequest(req, res) {
99
520
  this.requestCounter++;
100
- // Declare outside try block for cleanup in finally
521
+ // Single abort hook for the whole request lifecycle. uWS requires
522
+ // onAborted for any response finished after returning to the event loop;
523
+ // readBody chains onto res._abortHook rather than replacing this handler.
524
+ res.aborted = false;
525
+ res.onAborted(() => {
526
+ res.aborted = true;
527
+ const hook = res._abortHook;
528
+ if (hook)
529
+ hook();
530
+ });
101
531
  let httpReq;
102
532
  let httpRes;
103
533
  try {
104
- // Create Moro-compatible request object
534
+ // Create Moro-compatible request/response objects
105
535
  httpReq = this.createMoroRequest(req, res);
106
536
  httpRes = this.createMoroResponse(req, res);
107
- // Parse body only if there's actually a body (check content-length)
108
- const method = req.getMethod().toUpperCase();
109
- const contentLength = req.getHeader('content-length');
537
+ const method = httpReq.method;
538
+ // Parse body only if there's actually a body (check content-length).
110
539
  // Check first char for early exit (all body methods start with 'P')
111
- const firstChar = method.charCodeAt(0);
112
- if (firstChar === 80 && // 'P' char code
113
- (method === 'POST' || method === 'PUT' || method === 'PATCH') &&
114
- contentLength &&
115
- parseInt(contentLength) > 0) {
116
- await this.readBody(res, httpReq);
117
- }
118
- // Execute hooks before request processing
119
- if (this.hookManager) {
120
- await this.hookManager.execute('request', {
540
+ if (method.charCodeAt(0) === 80 && // 'P' char code
541
+ (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
542
+ const contentLength = req.getHeader('content-length');
543
+ if (contentLength && parseInt(contentLength) > 0) {
544
+ // readBody awaits - snapshot uWS-backed data first
545
+ httpReq.materialize();
546
+ await this.readBody(res, httpReq);
547
+ }
548
+ }
549
+ // Execute hooks before request processing - skipped entirely when none registered
550
+ const hookManager = this.hookManager;
551
+ if (hookManager && (hookManager.hasHooks === undefined || hookManager.hasHooks('request'))) {
552
+ httpReq.materialize();
553
+ await hookManager.execute('request', {
121
554
  request: httpReq,
122
555
  response: httpRes,
123
556
  });
124
557
  }
125
- // Execute middleware chain (includes UnifiedRouter for routing)
126
- // The UnifiedRouter will handle route matching, params extraction, and handler execution
558
+ // Execute global middleware chain
127
559
  if (this.globalMiddleware.length > 0) {
560
+ httpReq.materialize();
128
561
  await this.executeMiddleware(this.globalMiddleware, httpReq, httpRes);
562
+ if (httpRes.headersSent)
563
+ return;
129
564
  }
130
- else {
131
- // No middleware - send 404 (router middleware should be present)
132
- if (!httpRes.headersSent) {
133
- httpRes.statusCode = 404;
134
- httpRes.setHeader('Content-Type', 'application/json');
135
- httpRes.end('{"success":false,"error":"Not found"}');
565
+ // Unified router direct dispatch
566
+ const routerHandler = this.routerHandler;
567
+ if (routerHandler) {
568
+ const handled = routerHandler(httpReq, httpRes);
569
+ if (handled) {
570
+ if (typeof handled.then === 'function') {
571
+ // Async route: the handler is suspended at its first await - snapshot
572
+ // uWS-backed data now, while the native request is still valid
573
+ httpReq.materialize();
574
+ if (await handled)
575
+ return;
576
+ }
577
+ else {
578
+ return;
579
+ }
136
580
  }
137
581
  }
582
+ // No route matched
583
+ if (!httpRes.headersSent && !res.aborted) {
584
+ httpRes.statusCode = 404;
585
+ httpRes.setHeader('Content-Type', 'application/json');
586
+ httpRes.end('{"success":false,"error":"Not found"}');
587
+ }
138
588
  }
139
589
  catch (error) {
590
+ // Payload-too-large: respond 413 rather than a generic 500
591
+ if (error?.statusCode === 413 && !res.aborted && httpRes && !httpRes.headersSent) {
592
+ httpRes.statusCode = 413;
593
+ httpRes.setHeader('Content-Type', 'application/json');
594
+ httpRes.end('{"success":false,"error":"Request entity too large"}');
595
+ return;
596
+ }
140
597
  this.logger.error(`Request handling error: ${error instanceof Error ? error.message : String(error)}`, 'RequestError');
141
598
  // Send error response if not already sent
142
599
  if (!res.aborted) {
@@ -152,39 +609,6 @@ export class UWebSocketsHttpServer {
152
609
  }
153
610
  }
154
611
  }
155
- finally {
156
- // Release pooled objects back to pool
157
- if (httpReq) {
158
- const pooledQuery = httpReq._pooledQuery;
159
- const pooledHeaders = httpReq._pooledHeaders;
160
- // Only release if object has keys (avoid pool churn for empty objects)
161
- if (pooledQuery) {
162
- let hasKeys = false;
163
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
164
- for (const _key in pooledQuery) {
165
- hasKeys = true;
166
- break;
167
- }
168
- if (hasKeys) {
169
- this.poolManager.releaseQuery(pooledQuery);
170
- }
171
- }
172
- if (pooledHeaders) {
173
- let hasKeys = false;
174
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
175
- for (const _key in pooledHeaders) {
176
- hasKeys = true;
177
- break;
178
- }
179
- if (hasKeys) {
180
- this.poolManager.releaseHeaders(pooledHeaders);
181
- }
182
- }
183
- }
184
- if (httpRes) {
185
- this.releaseResponse(httpRes);
186
- }
187
- }
188
612
  }
189
613
  createMoroRequest(req, _res) {
190
614
  const url = req.getUrl();
@@ -192,45 +616,9 @@ export class UWebSocketsHttpServer {
192
616
  const methodRaw = req.getMethod();
193
617
  // Use interned method string if available
194
618
  const method = UWebSocketsHttpServer.INTERNED_METHODS.get(methodRaw) || methodRaw.toUpperCase();
195
- // Optimized query parsing with pooled object
196
- let queryParams;
197
- if (queryString) {
198
- queryParams = this.poolManager.acquireQuery();
199
- // Query parsing without URLSearchParams overhead
200
- const pairs = queryString.split('&');
201
- for (let i = 0; i < pairs.length; i++) {
202
- const pair = pairs[i];
203
- const eqIdx = pair.indexOf('=');
204
- if (eqIdx > 0) {
205
- const key = pair.substring(0, eqIdx);
206
- const value = pair.substring(eqIdx + 1);
207
- queryParams[decodeURIComponent(key)] = decodeURIComponent(value);
208
- }
209
- }
210
- }
211
- else {
212
- queryParams = {};
213
- }
214
- // Optimized header parsing with pooled object
215
- const headers = this.poolManager.acquireHeaders();
216
- req.forEach((key, value) => {
217
- headers[key] = value;
218
- });
219
- const httpReq = {
220
- method,
221
- path: url,
222
- url: url,
223
- query: queryParams,
224
- params: {}, // Will be filled by route matching
225
- headers,
226
- body: null,
227
- ip: '', // Lazy - only compute if accessed
228
- requestId: this.requestTrackingEnabled ? this.poolManager.generateRequestId() : '', // ID generation (if enabled)
229
- };
230
- // Store pooled objects for cleanup
231
- httpReq._pooledQuery = queryParams;
232
- httpReq._pooledHeaders = headers;
233
- return httpReq;
619
+ // Headers/query/cookies/requestId are lazy on the UwsRequest prototype -
620
+ // materialize() snapshots them before the first await
621
+ return new UwsRequest(this, req, method, url, queryString);
234
622
  }
235
623
  // Optimized helper to write headers
236
624
  static writeHeaders(res, headers) {
@@ -255,10 +643,9 @@ export class UWebSocketsHttpServer {
255
643
  init(res, req, logger) {
256
644
  this.headersSent = false;
257
645
  this.statusCode = 200;
258
- // Clear headers object (reuse to avoid allocation)
259
- for (const key in this.responseHeaders) {
260
- delete this.responseHeaders[key];
261
- }
646
+ // Fresh object (a delete-loop reset would push the object into V8
647
+ // dictionary mode, slowing every later header access)
648
+ this.responseHeaders = {};
262
649
  this._res = res;
263
650
  this._req = req;
264
651
  this._logger = logger;
@@ -286,7 +673,7 @@ export class UWebSocketsHttpServer {
286
673
  }
287
674
  return this;
288
675
  }
289
- async json(data) {
676
+ json(data) {
290
677
  if (this.headersSent || this._res.aborted)
291
678
  return;
292
679
  // Fast-path JSON serialization for common API patterns
@@ -583,65 +970,238 @@ export class UWebSocketsHttpServer {
583
970
  }
584
971
  return this;
585
972
  }
973
+ clearCookie(name, options = {}) {
974
+ const clearOptions = { expires: new Date(0), maxAge: 0 };
975
+ if (options.path !== undefined)
976
+ clearOptions.path = options.path;
977
+ if (options.domain !== undefined)
978
+ clearOptions.domain = options.domain;
979
+ if (options.httpOnly !== undefined)
980
+ clearOptions.httpOnly = options.httpOnly;
981
+ if (options.secure !== undefined)
982
+ clearOptions.secure = options.secure;
983
+ if (options.sameSite !== undefined)
984
+ clearOptions.sameSite = options.sameSite;
985
+ return this.cookie(name, '', clearOptions);
986
+ }
987
+ // ==== Express-compatible response helpers (parity with the Node adapter) ====
988
+ locals = {};
989
+ set(field, value) {
990
+ if (this.headersSent)
991
+ return this;
992
+ if (typeof field === 'string') {
993
+ if (value !== undefined)
994
+ this.setHeader(field, value);
995
+ }
996
+ else {
997
+ for (const key in field) {
998
+ this.setHeader(key, field[key]);
999
+ }
1000
+ }
1001
+ return this;
1002
+ }
1003
+ get(field) {
1004
+ return this.getHeader(field);
1005
+ }
1006
+ hasHeader(name) {
1007
+ return this.getHeader(name) !== undefined;
1008
+ }
1009
+ setBulkHeaders(headers) {
1010
+ if (this.headersSent)
1011
+ return this;
1012
+ for (const key in headers) {
1013
+ this.setHeader(key, String(headers[key]));
1014
+ }
1015
+ return this;
1016
+ }
1017
+ appendHeader(name, value) {
1018
+ if (this.headersSent)
1019
+ return this;
1020
+ const lower = name.toLowerCase();
1021
+ const existing = this.responseHeaders[lower];
1022
+ if (existing) {
1023
+ const values = Array.isArray(existing) ? existing : [existing];
1024
+ const incoming = Array.isArray(value) ? value : [value];
1025
+ this.responseHeaders[lower] = values.concat(incoming);
1026
+ }
1027
+ else {
1028
+ this.responseHeaders[lower] = value;
1029
+ }
1030
+ return this;
1031
+ }
1032
+ append(field, value) {
1033
+ return this.appendHeader(field, value);
1034
+ }
1035
+ type(contentType) {
1036
+ if (this.headersSent)
1037
+ return this;
1038
+ let ct = contentType;
1039
+ if (ct.indexOf('/') === -1) {
1040
+ const shorthands = {
1041
+ json: 'application/json; charset=utf-8',
1042
+ html: 'text/html; charset=utf-8',
1043
+ text: 'text/plain; charset=utf-8',
1044
+ txt: 'text/plain; charset=utf-8',
1045
+ xml: 'application/xml',
1046
+ js: 'application/javascript',
1047
+ css: 'text/css; charset=utf-8',
1048
+ form: 'application/x-www-form-urlencoded',
1049
+ };
1050
+ ct = shorthands[ct.toLowerCase()] || `application/${ct}`;
1051
+ }
1052
+ this.setHeader('Content-Type', ct);
1053
+ return this;
1054
+ }
1055
+ sendStatus(code) {
1056
+ if (this.headersSent)
1057
+ return;
1058
+ this.statusCode = code;
1059
+ const statusString = UWebSocketsHttpServer.getStatusString(code);
1060
+ const body = statusString.slice(String(code).length + 1) || String(code);
1061
+ this.setHeader('Content-Type', 'text/plain; charset=utf-8');
1062
+ this.end(body);
1063
+ }
1064
+ location(url) {
1065
+ if (this.headersSent)
1066
+ return this;
1067
+ this.setHeader('Location', url.replace(/[\r\n]/g, ''));
1068
+ return this;
1069
+ }
1070
+ vary(field) {
1071
+ if (this.headersSent)
1072
+ return this;
1073
+ const existing = this.responseHeaders['vary'];
1074
+ const existingList = existing
1075
+ ? String(existing)
1076
+ .split(',')
1077
+ .map(s => s.trim())
1078
+ .filter(Boolean)
1079
+ : [];
1080
+ const incoming = Array.isArray(field) ? field : [field];
1081
+ for (const f of incoming) {
1082
+ if (!existingList.some(e => e.toLowerCase() === f.toLowerCase())) {
1083
+ existingList.push(f);
1084
+ }
1085
+ }
1086
+ this.responseHeaders['vary'] = existingList.join(', ');
1087
+ return this;
1088
+ }
1089
+ links(links) {
1090
+ if (this.headersSent)
1091
+ return this;
1092
+ const parts = [];
1093
+ for (const rel in links) {
1094
+ parts.push(`<${links[rel]}>; rel="${rel}"`);
1095
+ }
1096
+ if (parts.length > 0) {
1097
+ const existing = this.responseHeaders['link'];
1098
+ this.responseHeaders['link'] = existing
1099
+ ? `${existing}, ${parts.join(', ')}`
1100
+ : parts.join(', ');
1101
+ }
1102
+ return this;
1103
+ }
1104
+ canSetHeaders() {
1105
+ return !this.headersSent;
1106
+ }
1107
+ getResponseState() {
1108
+ return {
1109
+ headersSent: this.headersSent,
1110
+ statusCode: this.statusCode,
1111
+ headers: this.responseHeaders,
1112
+ finished: this.headersSent,
1113
+ writable: !this.headersSent,
1114
+ };
1115
+ }
586
1116
  };
587
- // Object pool for response objects (further optimization)
588
- responsePool = [];
589
- MAX_RESPONSE_POOL_SIZE = 100;
590
1117
  createMoroResponse(req, res) {
591
- const httpRes = this.responsePool.length > 0
592
- ? this.responsePool.pop()
593
- : new UWebSocketsHttpServer.ResponsePrototype();
1118
+ // Fresh instance per request. Pooling these was a correctness hazard: a
1119
+ // handler that responds after returning (timers, streams) would write into
1120
+ // a recycled object already serving another request.
1121
+ const httpRes = new UWebSocketsHttpServer.ResponsePrototype();
594
1122
  httpRes.init(res, req, this.logger);
595
1123
  return httpRes;
596
1124
  }
597
- releaseResponse(httpRes) {
598
- // Return response object to pool for reuse
599
- if (this.responsePool.length < this.MAX_RESPONSE_POOL_SIZE) {
600
- this.responsePool.push(httpRes);
601
- }
602
- }
603
1125
  async readBody(res, httpReq) {
604
- return new Promise(resolve => {
1126
+ const contentType = httpReq.headers['content-type'] || '';
1127
+ const isMultipart = contentType.includes('multipart/form-data');
1128
+ const maxSize = isMultipart ? this.maxUploadSize : this.maxBodySize;
1129
+ return new Promise((resolve, reject) => {
605
1130
  // Collect chunks in array, concat once at end (faster than repeated Buffer.concat)
606
- let buffer = null;
607
1131
  const chunks = [];
608
1132
  let totalLength = 0;
1133
+ let settled = false;
1134
+ const finish = (fn) => {
1135
+ if (settled)
1136
+ return;
1137
+ settled = true;
1138
+ res._abortHook = null;
1139
+ fn();
1140
+ };
609
1141
  res.onData((chunk, isLast) => {
610
- const chunkBuffer = Buffer.from(chunk);
611
- chunks.push(chunkBuffer);
612
- totalLength += chunkBuffer.length;
613
- if (isLast) {
614
- // Concat all chunks at once (much faster than incremental concat)
615
- buffer = totalLength === 0 ? Buffer.alloc(0) : Buffer.concat(chunks, totalLength);
616
- try {
617
- const contentType = httpReq.headers['content-type'] || '';
618
- if (contentType.includes('application/json')) {
619
- httpReq.body = JSON.parse(buffer.toString('utf-8'));
620
- }
621
- else if (contentType.includes('application/x-www-form-urlencoded')) {
622
- const params = new URLSearchParams(buffer.toString('utf-8'));
623
- const body = {};
624
- params.forEach((value, key) => {
625
- body[key] = value;
626
- });
627
- httpReq.body = body;
628
- }
629
- else {
630
- httpReq.body = buffer.toString('utf-8');
631
- }
632
- resolve();
1142
+ if (settled)
1143
+ return;
1144
+ // Buffer.from(ArrayBuffer) is a zero-copy VIEW of uWS-owned memory,
1145
+ // which uWS recycles after this callback returns. The final chunk is
1146
+ // consumed synchronously below, so the view is safe; earlier chunks
1147
+ // must be copied or they alias reused memory (silent body corruption).
1148
+ const view = Buffer.from(chunk);
1149
+ totalLength += view.length;
1150
+ if (totalLength > maxSize) {
1151
+ const error = new Error(isMultipart ? 'File upload too large' : 'Request body too large');
1152
+ error.statusCode = 413;
1153
+ finish(() => reject(error));
1154
+ return;
1155
+ }
1156
+ if (!isLast) {
1157
+ const copy = Buffer.allocUnsafe(view.length);
1158
+ view.copy(copy);
1159
+ chunks.push(copy);
1160
+ return;
1161
+ }
1162
+ // Final chunk: single-chunk bodies (the common case) parse straight
1163
+ // from the view with zero copies
1164
+ let buffer;
1165
+ if (chunks.length === 0) {
1166
+ buffer = view;
1167
+ }
1168
+ else {
1169
+ chunks.push(view);
1170
+ buffer = Buffer.concat(chunks, totalLength);
1171
+ }
1172
+ try {
1173
+ if (contentType.includes('application/json')) {
1174
+ httpReq.body = totalLength === 0 ? null : JSON.parse(buffer.toString('utf-8'));
1175
+ }
1176
+ else if (contentType.includes('application/x-www-form-urlencoded')) {
1177
+ const params = new URLSearchParams(buffer.toString('utf-8'));
1178
+ const body = {};
1179
+ params.forEach((value, key) => {
1180
+ body[key] = value;
1181
+ });
1182
+ httpReq.body = body;
633
1183
  }
634
- catch {
635
- this.logger.error('Failed to parse request body', 'BodyParseError');
636
- httpReq.body = null;
637
- resolve();
1184
+ else {
1185
+ httpReq.body = buffer.toString('utf-8');
638
1186
  }
1187
+ finish(resolve);
1188
+ }
1189
+ catch {
1190
+ this.logger.error('Failed to parse request body', 'BodyParseError');
1191
+ httpReq.body = null;
1192
+ finish(resolve);
639
1193
  }
640
1194
  });
641
- res.onAborted(() => {
1195
+ // Chain onto the request-lifetime abort hook (registered in handleRequest
1196
+ // or the native route wrapper) instead of replacing it
1197
+ res._abortHook = () => {
642
1198
  this.logger.debug('Request aborted', 'RequestAborted');
643
- resolve();
644
- });
1199
+ finish(resolve);
1200
+ };
1201
+ // If the client already disconnected before we attached the hook
1202
+ if (res.aborted) {
1203
+ finish(resolve);
1204
+ }
645
1205
  });
646
1206
  }
647
1207
  async executeMiddleware(middleware, req, res) {
@@ -707,6 +1267,14 @@ export class UWebSocketsHttpServer {
707
1267
  const cb = typeof hostOrCallback === 'function' ? hostOrCallback : callback;
708
1268
  this.port = port;
709
1269
  this.host = host;
1270
+ // Register fast-path routes on the native router now that all routes
1271
+ // (including file-based/auto-discovered ones) are loaded
1272
+ try {
1273
+ this.registerNativeFastPathRoutes();
1274
+ }
1275
+ catch (error) {
1276
+ this.logger.warn(`Native fast-path route registration failed, falling back to catchall: ${error instanceof Error ? error.message : String(error)}`, 'NativeRoutes');
1277
+ }
710
1278
  // Check if we're in a cluster environment
711
1279
  const isClusterWorker = cluster.isWorker;
712
1280
  // uWebSockets.js automatically enables SO_REUSEPORT when in cluster mode