@morojs/moro 1.7.29 → 1.8.0

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 (67) hide show
  1. package/README.md +89 -37
  2. package/dist/core/config/config-sources.js +114 -2
  3. package/dist/core/config/config-sources.js.map +1 -1
  4. package/dist/core/config/config-validator.js +187 -5
  5. package/dist/core/config/config-validator.js.map +1 -1
  6. package/dist/core/config/schema.js +31 -3
  7. package/dist/core/config/schema.js.map +1 -1
  8. package/dist/core/framework.d.ts +41 -2
  9. package/dist/core/framework.js +338 -73
  10. package/dist/core/framework.js.map +1 -1
  11. package/dist/core/http/http-server.d.ts +21 -22
  12. package/dist/core/http/http-server.js +268 -226
  13. package/dist/core/http/http-server.js.map +1 -1
  14. package/dist/core/http/http2-server.d.ts +6 -16
  15. package/dist/core/http/http2-server.js +14 -79
  16. package/dist/core/http/http2-server.js.map +1 -1
  17. package/dist/core/http/index.d.ts +1 -0
  18. package/dist/core/http/index.js +3 -0
  19. package/dist/core/http/index.js.map +1 -1
  20. package/dist/core/http/moro-engine-server.d.ts +263 -0
  21. package/dist/core/http/moro-engine-server.js +1939 -0
  22. package/dist/core/http/moro-engine-server.js.map +1 -0
  23. package/dist/core/http/utils/compression.d.ts +47 -0
  24. package/dist/core/http/utils/compression.js +132 -0
  25. package/dist/core/http/utils/compression.js.map +1 -0
  26. package/dist/core/http/utils/lazy-event-emitter.d.ts +11 -0
  27. package/dist/core/http/utils/lazy-event-emitter.js +69 -0
  28. package/dist/core/http/utils/lazy-event-emitter.js.map +1 -0
  29. package/dist/core/http/utils/multipart-parser.d.ts +32 -0
  30. package/dist/core/http/utils/multipart-parser.js +211 -0
  31. package/dist/core/http/utils/multipart-parser.js.map +1 -0
  32. package/dist/core/http/utils/query-parser.d.ts +1 -0
  33. package/dist/core/http/utils/query-parser.js +44 -0
  34. package/dist/core/http/utils/query-parser.js.map +1 -0
  35. package/dist/core/http/utils/size.d.ts +33 -0
  36. package/dist/core/http/utils/size.js +37 -0
  37. package/dist/core/http/utils/size.js.map +1 -0
  38. package/dist/core/http/utils/ssl-config.d.ts +69 -0
  39. package/dist/core/http/utils/ssl-config.js +116 -0
  40. package/dist/core/http/utils/ssl-config.js.map +1 -0
  41. package/dist/core/http/utils/uws-worker-clustering.js +8 -2
  42. package/dist/core/http/utils/uws-worker-clustering.js.map +1 -1
  43. package/dist/core/http/uws-http-server.d.ts +24 -3
  44. package/dist/core/http/uws-http-server.js +386 -65
  45. package/dist/core/http/uws-http-server.js.map +1 -1
  46. package/dist/core/middleware/built-in/compression/core.d.ts +3 -0
  47. package/dist/core/middleware/built-in/compression/core.js +28 -34
  48. package/dist/core/middleware/built-in/compression/core.js.map +1 -1
  49. package/dist/core/networking/adapters/engine-adapter.d.ts +23 -0
  50. package/dist/core/networking/adapters/engine-adapter.js +365 -0
  51. package/dist/core/networking/adapters/engine-adapter.js.map +1 -0
  52. package/dist/core/networking/adapters/index.d.ts +1 -0
  53. package/dist/core/networking/adapters/index.js +1 -0
  54. package/dist/core/networking/adapters/index.js.map +1 -1
  55. package/dist/core/networking/adapters/uws-adapter.js +26 -15
  56. package/dist/core/networking/adapters/uws-adapter.js.map +1 -1
  57. package/dist/core/utilities/package-utils.d.ts +68 -0
  58. package/dist/core/utilities/package-utils.js +198 -1
  59. package/dist/core/utilities/package-utils.js.map +1 -1
  60. package/dist/index.d.ts +1 -1
  61. package/dist/index.js +1 -1
  62. package/dist/index.js.map +1 -1
  63. package/dist/moro.d.ts +2 -0
  64. package/dist/moro.js +63 -21
  65. package/dist/moro.js.map +1 -1
  66. package/dist/types/config.d.ts +90 -5
  67. package/package.json +4 -1
@@ -1,9 +1,12 @@
1
1
  // src/core/http-server.ts
2
2
  import { IncomingMessage, ServerResponse, createServer } from 'http';
3
+ import { createServer as createHttpsServer } from 'https';
3
4
  import * as zlib from 'zlib';
4
5
  import { promisify } from 'util';
5
6
  import { randomUUID } from 'crypto';
6
7
  import { createFrameworkLogger } from '../logger/index.js';
8
+ import { parseMultipart as parseMultipartBuffer, } from './utils/multipart-parser.js';
9
+ import { parseRawQueryString } from './utils/query-parser.js';
7
10
  import { PathMatcher } from '../routing/path-matcher.js';
8
11
  import { ObjectPoolManager } from '../pooling/object-pool-manager.js';
9
12
  const gzip = promisify(zlib.gzip);
@@ -39,26 +42,9 @@ const TYPE_SHORTHANDS = {
39
42
  css: 'text/css; charset=utf-8',
40
43
  form: 'application/x-www-form-urlencoded',
41
44
  };
42
- function parseQueryString(queryString) {
43
- const result = {};
44
- if (!queryString)
45
- return result;
46
- const pairs = queryString.split('&');
47
- const pairsLen = pairs.length;
48
- for (let i = 0; i < pairsLen; i++) {
49
- const pair = pairs[i];
50
- const equalIndex = pair.indexOf('=');
51
- if (equalIndex === -1) {
52
- result[decodeURIComponent(pair)] = '';
53
- }
54
- else {
55
- const key = decodeURIComponent(pair.substring(0, equalIndex));
56
- const value = decodeURIComponent(pair.substring(equalIndex + 1));
57
- result[key] = value;
58
- }
59
- }
60
- return result;
61
- }
45
+ // Canonical query parsing shared with the uWS/engine transports - identical
46
+ // req.query semantics everywhere ('+' as space, malformed escapes tolerated).
47
+ const parseQueryString = parseRawQueryString;
62
48
  function parseCookieHeader(cookieHeader) {
63
49
  const cookies = {};
64
50
  if (!cookieHeader)
@@ -96,103 +82,104 @@ function parseCookieHeader(cookieHeader) {
96
82
  // compression settings) without any per-request allocation.
97
83
  // ---------------------------------------------------------------------------
98
84
  export class MoroIncomingMessage extends IncomingMessage {
99
- // Hot per-request fields (assigned in handleRequest; declared here for stable shape)
85
+ // Hot per-request fields (assigned in handleRequest; declared here for
86
+ // stable shape). Everything else lives behind the single `_lazy` slot -
87
+ // per-request construction pays 5 field initializations instead of 16.
100
88
  params = {};
101
89
  body = null;
102
90
  path = '';
103
- // Lazy backing fields - single hidden-class shape, created at construction
104
91
  _queryString = null;
105
- _query = undefined;
106
- _cookies = undefined;
107
- _context = undefined;
108
- _requestId = undefined;
109
- _ip = undefined;
110
- _originalUrl = undefined;
111
- _hostname = undefined;
112
- _protocol = undefined;
113
- _secure = undefined;
114
- _xhr = undefined;
115
- _ips = undefined;
116
- _subdomains = undefined;
92
+ // Single slot for every lazily-computed/assigned helper value (query,
93
+ // cookies, requestId, hostname, ...). Allocated on first access - a plain
94
+ // JSON GET that never touches the helpers never allocates it, and the
95
+ // request object keeps one stable hidden-class shape either way.
96
+ _lazy = undefined;
117
97
  get query() {
118
- let q = this._query;
98
+ const L = this._lazy ?? (this._lazy = {});
99
+ let q = L.query;
119
100
  if (q === undefined) {
120
101
  const qs = this._queryString;
121
102
  q = qs ? parseQueryString(qs) : {};
122
- this._query = q;
103
+ L.query = q;
123
104
  }
124
105
  return q;
125
106
  }
126
107
  set query(value) {
127
- this._query = value;
108
+ (this._lazy ?? (this._lazy = {})).query = value;
128
109
  }
129
110
  get cookies() {
130
- let c = this._cookies;
111
+ const L = this._lazy ?? (this._lazy = {});
112
+ let c = L.cookies;
131
113
  if (c === undefined) {
132
114
  const header = this.headers.cookie;
133
115
  c = header ? parseCookieHeader(header) : {};
134
- this._cookies = c;
116
+ L.cookies = c;
135
117
  }
136
118
  return c;
137
119
  }
138
120
  set cookies(value) {
139
- this._cookies = value;
121
+ (this._lazy ?? (this._lazy = {})).cookies = value;
140
122
  }
141
123
  get context() {
142
- let ctx = this._context;
124
+ const L = this._lazy ?? (this._lazy = {});
125
+ let ctx = L.context;
143
126
  if (ctx === undefined) {
144
127
  ctx = {};
145
- this._context = ctx;
128
+ L.context = ctx;
146
129
  }
147
130
  return ctx;
148
131
  }
149
132
  set context(value) {
150
- this._context = value;
133
+ (this._lazy ?? (this._lazy = {})).context = value;
151
134
  }
152
135
  get requestId() {
153
- let id = this._requestId;
136
+ const L = this._lazy ?? (this._lazy = {});
137
+ let id = L.requestId;
154
138
  if (id === undefined) {
155
139
  const server = this._server;
156
140
  id = server && server.requestTrackingEnabled ? randomUUID() : '';
157
- this._requestId = id;
141
+ L.requestId = id;
158
142
  }
159
143
  return id;
160
144
  }
161
145
  set requestId(value) {
162
- this._requestId = value;
146
+ (this._lazy ?? (this._lazy = {})).requestId = value;
163
147
  }
164
148
  get ip() {
165
- let v = this._ip;
149
+ const L = this._lazy ?? (this._lazy = {});
150
+ let v = L.ip;
166
151
  if (v === undefined) {
167
152
  v = (this.socket && this.socket.remoteAddress) || '';
168
- this._ip = v;
153
+ L.ip = v;
169
154
  }
170
155
  return v;
171
156
  }
172
157
  set ip(value) {
173
- this._ip = value;
158
+ (this._lazy ?? (this._lazy = {})).ip = value;
174
159
  }
175
160
  get originalUrl() {
176
- const v = this._originalUrl;
161
+ const v = this._lazy?.originalUrl;
177
162
  return v !== undefined ? v : this.url || '';
178
163
  }
179
164
  set originalUrl(value) {
180
- this._originalUrl = value;
165
+ (this._lazy ?? (this._lazy = {})).originalUrl = value;
181
166
  }
182
167
  get hostname() {
183
- let v = this._hostname;
168
+ const L = this._lazy ?? (this._lazy = {});
169
+ let v = L.hostname;
184
170
  if (v === undefined) {
185
171
  const host = (this.headers.host || '');
186
172
  v = host ? host.split(':')[0] : '';
187
- this._hostname = v;
173
+ L.hostname = v;
188
174
  }
189
175
  return v;
190
176
  }
191
177
  set hostname(value) {
192
- this._hostname = value;
178
+ (this._lazy ?? (this._lazy = {})).hostname = value;
193
179
  }
194
180
  get protocol() {
195
- let v = this._protocol;
181
+ const L = this._lazy ?? (this._lazy = {});
182
+ let v = L.protocol;
196
183
  if (v === undefined) {
197
184
  const forwardedProto = this.headers['x-forwarded-proto'];
198
185
  v = forwardedProto
@@ -200,34 +187,36 @@ export class MoroIncomingMessage extends IncomingMessage {
200
187
  : this.socket?.encrypted
201
188
  ? 'https'
202
189
  : 'http';
203
- this._protocol = v;
190
+ L.protocol = v;
204
191
  }
205
192
  return v;
206
193
  }
207
194
  set protocol(value) {
208
- this._protocol = value;
195
+ (this._lazy ?? (this._lazy = {})).protocol = value;
209
196
  }
210
197
  get secure() {
211
- const v = this._secure;
198
+ const v = this._lazy?.secure;
212
199
  return v !== undefined ? v : this.protocol === 'https';
213
200
  }
214
201
  set secure(value) {
215
- this._secure = value;
202
+ (this._lazy ?? (this._lazy = {})).secure = value;
216
203
  }
217
204
  get xhr() {
218
- let v = this._xhr;
205
+ const L = this._lazy ?? (this._lazy = {});
206
+ let v = L.xhr;
219
207
  if (v === undefined) {
220
208
  const xrw = this.headers['x-requested-with'];
221
209
  v = !!xrw && xrw.toLowerCase() === 'xmlhttprequest';
222
- this._xhr = v;
210
+ L.xhr = v;
223
211
  }
224
212
  return v;
225
213
  }
226
214
  set xhr(value) {
227
- this._xhr = value;
215
+ (this._lazy ?? (this._lazy = {})).xhr = value;
228
216
  }
229
217
  get ips() {
230
- let v = this._ips;
218
+ const L = this._lazy ?? (this._lazy = {});
219
+ let v = L.ips;
231
220
  if (v === undefined) {
232
221
  const forwardedFor = this.headers['x-forwarded-for'];
233
222
  v = forwardedFor
@@ -236,26 +225,27 @@ export class MoroIncomingMessage extends IncomingMessage {
236
225
  .map(s => s.trim())
237
226
  .filter(Boolean)
238
227
  : [];
239
- this._ips = v;
228
+ L.ips = v;
240
229
  }
241
230
  return v;
242
231
  }
243
232
  set ips(value) {
244
- this._ips = value;
233
+ (this._lazy ?? (this._lazy = {})).ips = value;
245
234
  }
246
235
  get subdomains() {
247
- let v = this._subdomains;
236
+ const L = this._lazy ?? (this._lazy = {});
237
+ let v = L.subdomains;
248
238
  if (v === undefined) {
249
239
  const hostnameParts = this.hostname.split('.');
250
240
  // Matches Express: subdomains are parts before the last two (domain + tld),
251
241
  // reversed (closest subdomain first).
252
242
  v = hostnameParts.length > 2 ? hostnameParts.slice(0, -2).reverse() : [];
253
- this._subdomains = v;
243
+ L.subdomains = v;
254
244
  }
255
245
  return v;
256
246
  }
257
247
  set subdomains(value) {
258
- this._subdomains = value;
248
+ (this._lazy ?? (this._lazy = {})).subdomains = value;
259
249
  }
260
250
  // Express-compatible request helpers (shared prototype methods, not closures)
261
251
  get(name) {
@@ -481,7 +471,8 @@ export class MoroServerResponse extends ServerResponse {
481
471
  }
482
472
  const cookieValue = encodeURIComponent(value);
483
473
  let cookieString = `${name}=${cookieValue}`;
484
- if (options.maxAge)
474
+ // maxAge: 0 is meaningful (immediate expiry - clearCookie relies on it)
475
+ if (options.maxAge !== undefined && options.maxAge !== null)
485
476
  cookieString += `; Max-Age=${options.maxAge}`;
486
477
  if (options.expires)
487
478
  cookieString += `; Expires=${options.expires.toUTCString()}`;
@@ -493,8 +484,9 @@ export class MoroServerResponse extends ServerResponse {
493
484
  cookieString += `; SameSite=${options.sameSite}`;
494
485
  if (options.domain)
495
486
  cookieString += `; Domain=${options.domain}`;
496
- if (options.path)
497
- cookieString += `; Path=${options.path}`;
487
+ // Path defaults to '/' (Express behavior) so clearCookie() from a nested
488
+ // route can clear a cookie originally set at the site root.
489
+ cookieString += `; Path=${options.path ?? '/'}`;
498
490
  const existingCookies = this.getHeader('Set-Cookie') || [];
499
491
  // Avoid spread operator - direct array manipulation
500
492
  const cookies = Array.isArray(existingCookies) ? existingCookies : [existingCookies];
@@ -862,6 +854,10 @@ export class MoroServerResponse extends ServerResponse {
862
854
  }
863
855
  export class MoroHttpServer {
864
856
  server;
857
+ /** Multipart limits threaded to parseMultipart at the call site. */
858
+ multipartLimits;
859
+ /** TCP backlog for listen(), when configured. */
860
+ listenBacklog;
865
861
  routes = [];
866
862
  globalMiddleware = [];
867
863
  /** @internal read by MoroServerResponse prototype methods */
@@ -919,7 +915,23 @@ export class MoroHttpServer {
919
915
  BoundResponse.prototype._server = boundServer;
920
916
  this.RequestClass = BoundRequest;
921
917
  this.ResponseClass = BoundResponse;
922
- this.server = createServer({ IncomingMessage: BoundRequest, ServerResponse: BoundResponse }, this.handleRequest.bind(this));
918
+ const limits = options?.limits;
919
+ // maxHeaderSize is a createServer option (not a mutable property), so it
920
+ // must be passed here.
921
+ const serverOpts = {
922
+ IncomingMessage: BoundRequest,
923
+ ServerResponse: BoundResponse,
924
+ ...(limits?.maxHeaderSize && { maxHeaderSize: limits.maxHeaderSize }),
925
+ };
926
+ if (options?.ssl) {
927
+ // In-process HTTPS. The https ServerOptions extends the http one, so the
928
+ // IncomingMessage/ServerResponse subclass injection carries over.
929
+ this.server = createHttpsServer({ ...serverOpts, ...options.ssl }, this.handleRequest.bind(this));
930
+ this.logger.info('HTTPS server created (in-process TLS)', 'HttpServer');
931
+ }
932
+ else {
933
+ this.server = createServer(serverOpts, this.handleRequest.bind(this));
934
+ }
923
935
  // Configure body size limits from options
924
936
  if (options?.maxBodySize) {
925
937
  this.maxBodySize = options.maxBodySize;
@@ -927,10 +939,24 @@ export class MoroHttpServer {
927
939
  if (options?.maxUploadSize) {
928
940
  this.maxUploadSize = options.maxUploadSize;
929
941
  }
930
- // Optimize server for high performance (conservative settings for compatibility)
931
- this.server.keepAliveTimeout = 5000; // 5 seconds
932
- this.server.headersTimeout = 6000; // 6 seconds
933
- this.server.timeout = 30000; // 30 seconds request timeout
942
+ if (limits?.multipart)
943
+ this.multipartLimits = limits.multipart;
944
+ // Timeouts: the previous hardcodes (keepAlive 5000, headers 6000) become
945
+ // the documented defaults; a configured value overrides them. Socket idle
946
+ // timeout stays 0 (disabled) unless set - a non-zero value arms a
947
+ // per-request timer that shows up in CPU profiles under load.
948
+ const t = limits?.timeouts ?? {};
949
+ this.server.keepAliveTimeout = t.keepAlive ?? 5000;
950
+ this.server.headersTimeout = t.headers ?? 6000;
951
+ this.server.timeout = t.idle ?? 0;
952
+ if (t.request)
953
+ this.server.requestTimeout = t.request;
954
+ if (limits?.maxConnections && limits.maxConnections > 0) {
955
+ this.server.maxConnections = limits.maxConnections;
956
+ }
957
+ if (limits?.maxHeaders)
958
+ this.server.maxHeadersCount = limits.maxHeaders;
959
+ this.listenBacklog = limits?.backlog;
934
960
  }
935
961
  // Direct router dispatch - called after global middleware, before the legacy
936
962
  // route table. Returning true (or a promise of true) means the request was handled.
@@ -1056,63 +1082,105 @@ export class MoroHttpServer {
1056
1082
  paramNames: compiled.paramNames,
1057
1083
  };
1058
1084
  }
1059
- async handleRequest(req, res) {
1085
+ // Request entry point. Deliberately NOT an async function: the common case
1086
+ // (no body, no hooks, no global middleware, sync route handler) completes
1087
+ // fully synchronously with zero promise allocations and zero microtask
1088
+ // hops. Anything that genuinely needs to await routes through
1089
+ // handleRequestSlow.
1090
+ handleRequest(req, res) {
1060
1091
  // req/res arrive as the per-server subclasses (MoroIncomingMessage /
1061
1092
  // MoroServerResponse) - all helpers are already on their prototypes.
1062
1093
  const httpReq = req;
1063
1094
  const httpRes = res;
1095
+ // URL split - query string parsing is lazy (MoroIncomingMessage#query getter)
1096
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1097
+ const urlString = req.url;
1098
+ const queryIndex = urlString.indexOf('?');
1099
+ if (queryIndex === -1) {
1100
+ httpReq.path = urlString;
1101
+ }
1102
+ else {
1103
+ httpReq.path = urlString.substring(0, queryIndex);
1104
+ httpReq._queryString = urlString.substring(queryIndex + 1);
1105
+ }
1106
+ // Intern method string for fast reference equality comparison (50-100% faster)
1107
+ switch (req.method) {
1108
+ case 'POST':
1109
+ httpReq.method = MoroHttpServer.METHOD_POST;
1110
+ break;
1111
+ case 'PUT':
1112
+ httpReq.method = MoroHttpServer.METHOD_PUT;
1113
+ break;
1114
+ case 'PATCH':
1115
+ httpReq.method = MoroHttpServer.METHOD_PATCH;
1116
+ break;
1117
+ case 'GET':
1118
+ httpReq.method = MoroHttpServer.METHOD_GET;
1119
+ break;
1120
+ case 'DELETE':
1121
+ httpReq.method = MoroHttpServer.METHOD_DELETE;
1122
+ break;
1123
+ case 'HEAD':
1124
+ httpReq.method = MoroHttpServer.METHOD_HEAD;
1125
+ break;
1126
+ case 'OPTIONS':
1127
+ httpReq.method = MoroHttpServer.METHOD_OPTIONS;
1128
+ break;
1129
+ }
1130
+ const needsBody = httpReq.method === MoroHttpServer.METHOD_POST ||
1131
+ httpReq.method === MoroHttpServer.METHOD_PUT ||
1132
+ httpReq.method === MoroHttpServer.METHOD_PATCH;
1133
+ const hookManager = this.hookManager;
1134
+ const hasHooks = !!(hookManager &&
1135
+ (hookManager.hasHooks === undefined || hookManager.hasHooks('request')));
1136
+ if (needsBody || hasHooks || this.globalMiddleware.length > 0) {
1137
+ // Slow path: real awaits ahead (body parse / hooks / middleware).
1138
+ // Errors are fully handled inside - the returned promise never rejects.
1139
+ void this.handleRequestSlow(httpReq, httpRes, req, needsBody, hasHooks);
1140
+ return;
1141
+ }
1142
+ // FAST PATH - no promises unless the route handler itself is async
1064
1143
  try {
1065
- // URL split - query string parsing is lazy (MoroIncomingMessage#query getter)
1066
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1067
- const urlString = req.url;
1068
- const queryIndex = urlString.indexOf('?');
1069
- if (queryIndex === -1) {
1070
- httpReq.path = urlString;
1071
- }
1072
- else {
1073
- httpReq.path = urlString.substring(0, queryIndex);
1074
- httpReq._queryString = urlString.substring(queryIndex + 1);
1075
- }
1076
- // Intern method string for fast reference equality comparison (50-100% faster)
1077
- switch (req.method) {
1078
- case 'POST':
1079
- httpReq.method = MoroHttpServer.METHOD_POST;
1080
- break;
1081
- case 'PUT':
1082
- httpReq.method = MoroHttpServer.METHOD_PUT;
1083
- break;
1084
- case 'PATCH':
1085
- httpReq.method = MoroHttpServer.METHOD_PATCH;
1086
- break;
1087
- case 'GET':
1088
- httpReq.method = MoroHttpServer.METHOD_GET;
1089
- break;
1090
- case 'DELETE':
1091
- httpReq.method = MoroHttpServer.METHOD_DELETE;
1092
- break;
1093
- case 'HEAD':
1094
- httpReq.method = MoroHttpServer.METHOD_HEAD;
1095
- break;
1096
- case 'OPTIONS':
1097
- httpReq.method = MoroHttpServer.METHOD_OPTIONS;
1098
- break;
1144
+ const routerHandler = this.routerHandler;
1145
+ if (routerHandler) {
1146
+ const handled = routerHandler(httpReq, httpRes);
1147
+ if (handled) {
1148
+ if (typeof handled.then === 'function') {
1149
+ handled.then(wasHandled => {
1150
+ if (!wasHandled && !httpRes.headersSent) {
1151
+ try {
1152
+ this.dispatchLegacyRoute(httpReq, httpRes, req);
1153
+ }
1154
+ catch (error) {
1155
+ void this.onRequestError(error, httpReq, httpRes, req);
1156
+ }
1157
+ }
1158
+ }, error => void this.onRequestError(error, httpReq, httpRes, req));
1159
+ }
1160
+ return;
1161
+ }
1162
+ if (httpRes.headersSent)
1163
+ return;
1099
1164
  }
1100
- // Method checking - use reference equality for interned strings
1101
- if (httpReq.method === MoroHttpServer.METHOD_POST ||
1102
- httpReq.method === MoroHttpServer.METHOD_PUT ||
1103
- httpReq.method === MoroHttpServer.METHOD_PATCH) {
1165
+ this.dispatchLegacyRoute(httpReq, httpRes, req);
1166
+ }
1167
+ catch (error) {
1168
+ void this.onRequestError(error, httpReq, httpRes, req);
1169
+ }
1170
+ }
1171
+ // Full async pipeline for requests that need it: bodied methods, hook
1172
+ // consumers, and apps with global middleware.
1173
+ async handleRequestSlow(httpReq, httpRes, req, needsBody, hasHooks) {
1174
+ try {
1175
+ if (needsBody) {
1104
1176
  httpReq.body = await this.parseBody(req);
1105
1177
  }
1106
- // Execute hooks before request processing - skipped entirely (no context
1107
- // object, no promise) when no 'request' hooks are registered
1108
- const hookManager = this.hookManager;
1109
- if (hookManager && (hookManager.hasHooks === undefined || hookManager.hasHooks('request'))) {
1110
- await hookManager.execute('request', {
1178
+ if (hasHooks) {
1179
+ await this.hookManager.execute('request', {
1111
1180
  request: httpReq,
1112
1181
  response: httpRes,
1113
1182
  });
1114
1183
  }
1115
- // Execute global middleware first - EARLY EXIT if none registered.
1116
1184
  // executeMiddleware returns undefined when the whole chain completed
1117
1185
  // synchronously, so a sync chain costs zero promise allocations.
1118
1186
  if (this.globalMiddleware.length > 0) {
@@ -1140,41 +1208,58 @@ export class MoroHttpServer {
1140
1208
  if (httpRes.headersSent)
1141
1209
  return;
1142
1210
  }
1143
- // Find matching route (legacy direct-route table)
1144
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1145
- const route = this.findRoute(req.method, httpReq.path);
1146
- if (!route) {
1147
- // 404 response with pre-compiled buffer
1148
- httpRes.statusCode = 404;
1149
- httpRes.setHeader('Content-Type', 'application/json; charset=utf-8');
1150
- httpRes.setHeader('Content-Length', MoroHttpServer.RESPONSE_TEMPLATES.notFound.length);
1151
- httpRes.end(MoroHttpServer.RESPONSE_TEMPLATES.notFound);
1152
- return;
1153
- }
1154
- // Extract path parameters
1155
- const matches = httpReq.path.match(route.pattern);
1156
- if (matches) {
1157
- const params = {};
1158
- const paramNames = route.paramNames;
1159
- const paramNamesLen = paramNames.length;
1160
- for (let i = 0; i < paramNamesLen; i++) {
1161
- params[paramNames[i]] = matches[i + 1];
1162
- }
1163
- httpReq.params = params;
1164
- }
1165
- // Execute middleware chain - EARLY EXIT if no route middleware
1166
- if (route.middleware.length > 0) {
1167
- const mwResult = this.executeMiddleware(route.middleware, httpReq, httpRes);
1168
- if (mwResult)
1169
- await mwResult;
1211
+ this.dispatchLegacyRoute(httpReq, httpRes, req);
1212
+ }
1213
+ catch (error) {
1214
+ await this.onRequestError(error, httpReq, httpRes, req);
1215
+ }
1216
+ }
1217
+ // Legacy direct-route table dispatch + 404 fallback. Synchronous; async
1218
+ // route middleware/handlers are continued via promise callbacks wired to
1219
+ // onRequestError so the sync fast path stays promise-free.
1220
+ dispatchLegacyRoute(httpReq, httpRes, req) {
1221
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1222
+ const route = this.findRoute(req.method, httpReq.path);
1223
+ if (!route) {
1224
+ // 404 response with pre-compiled buffer
1225
+ httpRes.statusCode = 404;
1226
+ httpRes.setHeader('Content-Type', 'application/json; charset=utf-8');
1227
+ httpRes.setHeader('Content-Length', MoroHttpServer.RESPONSE_TEMPLATES.notFound.length);
1228
+ httpRes.end(MoroHttpServer.RESPONSE_TEMPLATES.notFound);
1229
+ return;
1230
+ }
1231
+ // Extract path parameters
1232
+ const matches = httpReq.path.match(route.pattern);
1233
+ if (matches) {
1234
+ const params = {};
1235
+ const paramNames = route.paramNames;
1236
+ const paramNamesLen = paramNames.length;
1237
+ for (let i = 0; i < paramNamesLen; i++) {
1238
+ params[paramNames[i]] = matches[i + 1];
1170
1239
  }
1240
+ httpReq.params = params;
1241
+ }
1242
+ const runHandler = () => {
1171
1243
  // Execute handler - Don't await sync handlers
1172
1244
  const handlerResult = route.handler(httpReq, httpRes);
1173
1245
  if (handlerResult && typeof handlerResult.then === 'function') {
1174
- await handlerResult;
1246
+ handlerResult.catch(error => void this.onRequestError(error, httpReq, httpRes, req));
1247
+ }
1248
+ };
1249
+ // Execute middleware chain - EARLY EXIT if no route middleware
1250
+ if (route.middleware.length > 0) {
1251
+ const mwResult = this.executeMiddleware(route.middleware, httpReq, httpRes);
1252
+ if (mwResult) {
1253
+ mwResult.then(runHandler, error => void this.onRequestError(error, httpReq, httpRes, req));
1254
+ return;
1175
1255
  }
1176
1256
  }
1177
- catch (error) {
1257
+ runHandler();
1258
+ }
1259
+ // Shared error handling for all request paths (sync throw, slow-path catch,
1260
+ // and promise rejections from async handlers/middleware). Never throws.
1261
+ async onRequestError(error, httpReq, httpRes, req) {
1262
+ try {
1178
1263
  // Debug: Log the actual error and where it came from
1179
1264
  this.logger.debug('Request error details', 'RequestHandler', {
1180
1265
  errorType: typeof error,
@@ -1216,18 +1301,23 @@ export class MoroHttpServer {
1216
1301
  }
1217
1302
  }
1218
1303
  if (!httpRes.headersSent) {
1304
+ // A malformed body is a client error: default to 400 (parity with the
1305
+ // engine/uWS transports), not the generic 500.
1306
+ const isBadRequest = error?.statusCode === 400;
1307
+ const defaultStatus = isBadRequest ? 400 : 500;
1308
+ const defaultError = isBadRequest ? 'Invalid request body' : 'Internal server error';
1219
1309
  // Ensure response is properly enhanced before using custom methods
1220
1310
  if (typeof httpRes.status === 'function' && typeof httpRes.json === 'function') {
1221
- httpRes.status(500).json({
1311
+ httpRes.status(defaultStatus).json({
1222
1312
  success: false,
1223
- error: 'Internal server error',
1313
+ error: defaultError,
1224
1314
  requestId: httpReq.requestId,
1225
1315
  });
1226
1316
  }
1227
1317
  else {
1228
1318
  // Defensive fallback - check each method individually
1229
1319
  if (typeof httpRes.setHeader === 'function') {
1230
- httpRes.statusCode = 500;
1320
+ httpRes.statusCode = defaultStatus;
1231
1321
  httpRes.setHeader('Content-Type', 'application/json');
1232
1322
  }
1233
1323
  else {
@@ -1240,7 +1330,7 @@ export class MoroHttpServer {
1240
1330
  if (typeof httpRes.end === 'function') {
1241
1331
  httpRes.end(JSON.stringify({
1242
1332
  success: false,
1243
- error: 'Internal server error',
1333
+ error: defaultError,
1244
1334
  requestId: httpReq.requestId,
1245
1335
  }));
1246
1336
  }
@@ -1250,6 +1340,13 @@ export class MoroHttpServer {
1250
1340
  }
1251
1341
  }
1252
1342
  }
1343
+ catch (fatalError) {
1344
+ // Error handling itself failed (e.g. the connection died mid-response) -
1345
+ // never let this escape as an unhandled rejection
1346
+ this.logger.error('Failed while handling request error', 'RequestHandler', {
1347
+ error: fatalError instanceof Error ? fatalError.message : String(fatalError),
1348
+ });
1349
+ }
1253
1350
  }
1254
1351
  // Force cleanup of all pooled objects
1255
1352
  forceCleanupPools() {
@@ -1401,7 +1498,9 @@ export class MoroHttpServer {
1401
1498
  // otherwise concat with the known total length to avoid a re-scan
1402
1499
  const body = chunks.length === 1 ? chunks[0] : Buffer.concat(chunks, totalLength);
1403
1500
  if (contentType.includes('application/json')) {
1404
- resolve(JSON.parse(body.toString()));
1501
+ // Empty JSON body -> null (parity with the engine/uWS servers, and
1502
+ // avoids JSON.parse('') throwing and 500ing an empty POST).
1503
+ resolve(totalLength === 0 ? null : JSON.parse(body.toString()));
1405
1504
  }
1406
1505
  else if (contentType.includes('application/x-www-form-urlencoded')) {
1407
1506
  resolve(this.parseUrlEncoded(body.toString()));
@@ -1414,6 +1513,12 @@ export class MoroHttpServer {
1414
1513
  }
1415
1514
  }
1416
1515
  catch (error) {
1516
+ // A malformed body is a client error (400), not a server error (500).
1517
+ // Tag it so the request-error path answers 400, matching the engine
1518
+ // and uWS transports.
1519
+ if (error instanceof Error && error.statusCode === undefined) {
1520
+ error.statusCode = 400;
1521
+ }
1417
1522
  reject(error);
1418
1523
  }
1419
1524
  });
@@ -1547,76 +1652,9 @@ export class MoroHttpServer {
1547
1652
  });
1548
1653
  }
1549
1654
  parseMultipart(buffer, contentType) {
1550
- const boundary = contentType.split('boundary=')[1];
1551
- if (!boundary) {
1552
- throw new Error('Invalid multipart boundary');
1553
- }
1554
- // Work with Buffer directly - don't convert to string (corrupts binary data)
1555
- const boundaryBuffer = Buffer.from(`--${boundary}`);
1556
- const parts = this.splitBuffer(buffer, boundaryBuffer);
1557
- const fields = {};
1558
- const files = {};
1559
- // Skip first (empty) and last (closing boundary) parts
1560
- const partsLen = parts.length - 1;
1561
- for (let i = 1; i < partsLen; i++) {
1562
- const part = parts[i];
1563
- // Find the double CRLF that separates headers from content
1564
- const headerEndPos = this.findBufferSequence(part, Buffer.from('\r\n\r\n'));
1565
- if (headerEndPos === -1)
1566
- continue;
1567
- // Headers are always text - safe to convert to string
1568
- const headers = part.slice(0, headerEndPos).toString('utf8');
1569
- // Content stays as Buffer to preserve binary data
1570
- const content = part.slice(headerEndPos + 4); // Skip the \r\n\r\n
1571
- const nameMatch = headers.match(/name="([^"]+)"/);
1572
- const filenameMatch = headers.match(/filename="([^"]+)"/);
1573
- const contentTypeMatch = headers.match(/Content-Type: ([^\r\n]+)/);
1574
- if (nameMatch) {
1575
- const name = nameMatch[1];
1576
- if (filenameMatch) {
1577
- // This is a file - keep as Buffer to preserve binary data
1578
- const filename = filenameMatch[1];
1579
- const mimeType = contentTypeMatch ? contentTypeMatch[1] : 'application/octet-stream';
1580
- // Remove trailing \r\n (2 bytes) from the end
1581
- const fileData = content.slice(0, content.length - 2);
1582
- files[name] = {
1583
- filename,
1584
- mimetype: mimeType,
1585
- data: fileData, // Already a Buffer - preserves binary integrity
1586
- size: fileData.length,
1587
- };
1588
- }
1589
- else {
1590
- // This is a regular field - convert to string
1591
- const fieldContent = content.slice(0, content.length - 2); // Remove trailing \r\n
1592
- fields[name] = fieldContent.toString('utf8');
1593
- }
1594
- }
1595
- }
1596
- return { fields, files };
1597
- }
1598
- /**
1599
- * Split a Buffer by a delimiter Buffer
1600
- * Used for multipart boundary splitting without corrupting binary data
1601
- */
1602
- splitBuffer(buffer, delimiter) {
1603
- const parts = [];
1604
- let start = 0;
1605
- let pos = 0;
1606
- while ((pos = buffer.indexOf(delimiter, start)) !== -1) {
1607
- parts.push(buffer.slice(start, pos));
1608
- start = pos + delimiter.length;
1609
- }
1610
- // Add the remaining part
1611
- parts.push(buffer.slice(start));
1612
- return parts;
1613
- }
1614
- /**
1615
- * Find the position of a sequence within a Buffer
1616
- * Returns -1 if not found
1617
- */
1618
- findBufferSequence(buffer, sequence) {
1619
- return buffer.indexOf(sequence);
1655
+ // Shared implementation - the native engine server parses uploads with
1656
+ // the exact same code so both transports agree on body/files shape
1657
+ return parseMultipartBuffer(buffer, contentType, this.multipartLimits);
1620
1658
  }
1621
1659
  parseUrlEncoded(body) {
1622
1660
  const params = new URLSearchParams(body);
@@ -1789,7 +1827,11 @@ export class MoroHttpServer {
1789
1827
  callback = host;
1790
1828
  host = undefined;
1791
1829
  }
1792
- if (host) {
1830
+ const backlog = this.listenBacklog;
1831
+ if (host && backlog) {
1832
+ this.server.listen(port, host, backlog, callback);
1833
+ }
1834
+ else if (host) {
1793
1835
  this.server.listen(port, host, callback);
1794
1836
  }
1795
1837
  else {