@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.
- package/dist/core/config/schema.js +3 -1
- package/dist/core/config/schema.js.map +1 -1
- package/dist/core/framework.d.ts +1 -1
- package/dist/core/framework.js +42 -28
- package/dist/core/framework.js.map +1 -1
- package/dist/core/http/http-server.d.ts +116 -12
- package/dist/core/http/http-server.js +1171 -862
- package/dist/core/http/http-server.js.map +1 -1
- package/dist/core/http/uws-http-server.d.ts +72 -5
- package/dist/core/http/uws-http-server.js +718 -150
- package/dist/core/http/uws-http-server.js.map +1 -1
- package/dist/core/logger/logger.d.ts +2 -0
- package/dist/core/logger/logger.js +32 -6
- package/dist/core/logger/logger.js.map +1 -1
- package/dist/core/middleware/built-in/compression/core.js +47 -29
- package/dist/core/middleware/built-in/compression/core.js.map +1 -1
- package/dist/core/middleware/built-in/error-tracker/middleware.d.ts +5 -2
- package/dist/core/middleware/built-in/error-tracker/middleware.js +13 -11
- package/dist/core/middleware/built-in/error-tracker/middleware.js.map +1 -1
- package/dist/core/middleware/built-in/index.d.ts +3 -3
- package/dist/core/middleware/built-in/performance-monitor/middleware.d.ts +2 -1
- package/dist/core/middleware/built-in/performance-monitor/middleware.js +7 -6
- package/dist/core/middleware/built-in/performance-monitor/middleware.js.map +1 -1
- package/dist/core/middleware/built-in/request-logger/middleware.d.ts +2 -1
- package/dist/core/middleware/built-in/request-logger/middleware.js +5 -5
- package/dist/core/middleware/built-in/request-logger/middleware.js.map +1 -1
- package/dist/core/middleware/built-in/validation/core.js +61 -27
- package/dist/core/middleware/built-in/validation/core.js.map +1 -1
- package/dist/core/routing/radix-tree.d.ts +1 -0
- package/dist/core/routing/radix-tree.js +48 -10
- package/dist/core/routing/radix-tree.js.map +1 -1
- package/dist/core/routing/unified-router.d.ts +16 -0
- package/dist/core/routing/unified-router.js +39 -12
- package/dist/core/routing/unified-router.js.map +1 -1
- package/dist/core/utilities/hooks.d.ts +2 -0
- package/dist/core/utilities/hooks.js +12 -0
- package/dist/core/utilities/hooks.js.map +1 -1
- package/dist/moro.d.ts +1 -0
- package/dist/moro.js +44 -68
- package/dist/moro.js.map +1 -1
- 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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
//
|
|
93
|
-
//
|
|
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
|
-
//
|
|
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
|
|
534
|
+
// Create Moro-compatible request/response objects
|
|
105
535
|
httpReq = this.createMoroRequest(req, res);
|
|
106
536
|
httpRes = this.createMoroResponse(req, res);
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
contentLength &&
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
//
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
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
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
-
|
|
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
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
|
|
635
|
-
|
|
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
|
-
|
|
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
|