@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.
- package/README.md +89 -37
- package/dist/core/config/config-sources.js +114 -2
- package/dist/core/config/config-sources.js.map +1 -1
- package/dist/core/config/config-validator.js +187 -5
- package/dist/core/config/config-validator.js.map +1 -1
- package/dist/core/config/schema.js +31 -3
- package/dist/core/config/schema.js.map +1 -1
- package/dist/core/framework.d.ts +41 -2
- package/dist/core/framework.js +338 -73
- package/dist/core/framework.js.map +1 -1
- package/dist/core/http/http-server.d.ts +21 -22
- package/dist/core/http/http-server.js +268 -226
- package/dist/core/http/http-server.js.map +1 -1
- package/dist/core/http/http2-server.d.ts +6 -16
- package/dist/core/http/http2-server.js +14 -79
- package/dist/core/http/http2-server.js.map +1 -1
- package/dist/core/http/index.d.ts +1 -0
- package/dist/core/http/index.js +3 -0
- package/dist/core/http/index.js.map +1 -1
- package/dist/core/http/moro-engine-server.d.ts +263 -0
- package/dist/core/http/moro-engine-server.js +1939 -0
- package/dist/core/http/moro-engine-server.js.map +1 -0
- package/dist/core/http/utils/compression.d.ts +47 -0
- package/dist/core/http/utils/compression.js +132 -0
- package/dist/core/http/utils/compression.js.map +1 -0
- package/dist/core/http/utils/lazy-event-emitter.d.ts +11 -0
- package/dist/core/http/utils/lazy-event-emitter.js +69 -0
- package/dist/core/http/utils/lazy-event-emitter.js.map +1 -0
- package/dist/core/http/utils/multipart-parser.d.ts +32 -0
- package/dist/core/http/utils/multipart-parser.js +211 -0
- package/dist/core/http/utils/multipart-parser.js.map +1 -0
- package/dist/core/http/utils/query-parser.d.ts +1 -0
- package/dist/core/http/utils/query-parser.js +44 -0
- package/dist/core/http/utils/query-parser.js.map +1 -0
- package/dist/core/http/utils/size.d.ts +33 -0
- package/dist/core/http/utils/size.js +37 -0
- package/dist/core/http/utils/size.js.map +1 -0
- package/dist/core/http/utils/ssl-config.d.ts +69 -0
- package/dist/core/http/utils/ssl-config.js +116 -0
- package/dist/core/http/utils/ssl-config.js.map +1 -0
- package/dist/core/http/utils/uws-worker-clustering.js +8 -2
- package/dist/core/http/utils/uws-worker-clustering.js.map +1 -1
- package/dist/core/http/uws-http-server.d.ts +24 -3
- package/dist/core/http/uws-http-server.js +386 -65
- package/dist/core/http/uws-http-server.js.map +1 -1
- package/dist/core/middleware/built-in/compression/core.d.ts +3 -0
- package/dist/core/middleware/built-in/compression/core.js +28 -34
- package/dist/core/middleware/built-in/compression/core.js.map +1 -1
- package/dist/core/networking/adapters/engine-adapter.d.ts +23 -0
- package/dist/core/networking/adapters/engine-adapter.js +365 -0
- package/dist/core/networking/adapters/engine-adapter.js.map +1 -0
- package/dist/core/networking/adapters/index.d.ts +1 -0
- package/dist/core/networking/adapters/index.js +1 -0
- package/dist/core/networking/adapters/index.js.map +1 -1
- package/dist/core/networking/adapters/uws-adapter.js +26 -15
- package/dist/core/networking/adapters/uws-adapter.js.map +1 -1
- package/dist/core/utilities/package-utils.d.ts +68 -0
- package/dist/core/utilities/package-utils.js +198 -1
- package/dist/core/utilities/package-utils.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/moro.d.ts +2 -0
- package/dist/moro.js +63 -21
- package/dist/moro.js.map +1 -1
- package/dist/types/config.d.ts +90 -5
- 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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
+
L.query = q;
|
|
123
104
|
}
|
|
124
105
|
return q;
|
|
125
106
|
}
|
|
126
107
|
set query(value) {
|
|
127
|
-
this.
|
|
108
|
+
(this._lazy ?? (this._lazy = {})).query = value;
|
|
128
109
|
}
|
|
129
110
|
get cookies() {
|
|
130
|
-
|
|
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
|
-
|
|
116
|
+
L.cookies = c;
|
|
135
117
|
}
|
|
136
118
|
return c;
|
|
137
119
|
}
|
|
138
120
|
set cookies(value) {
|
|
139
|
-
this.
|
|
121
|
+
(this._lazy ?? (this._lazy = {})).cookies = value;
|
|
140
122
|
}
|
|
141
123
|
get context() {
|
|
142
|
-
|
|
124
|
+
const L = this._lazy ?? (this._lazy = {});
|
|
125
|
+
let ctx = L.context;
|
|
143
126
|
if (ctx === undefined) {
|
|
144
127
|
ctx = {};
|
|
145
|
-
|
|
128
|
+
L.context = ctx;
|
|
146
129
|
}
|
|
147
130
|
return ctx;
|
|
148
131
|
}
|
|
149
132
|
set context(value) {
|
|
150
|
-
this.
|
|
133
|
+
(this._lazy ?? (this._lazy = {})).context = value;
|
|
151
134
|
}
|
|
152
135
|
get requestId() {
|
|
153
|
-
|
|
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
|
-
|
|
141
|
+
L.requestId = id;
|
|
158
142
|
}
|
|
159
143
|
return id;
|
|
160
144
|
}
|
|
161
145
|
set requestId(value) {
|
|
162
|
-
this.
|
|
146
|
+
(this._lazy ?? (this._lazy = {})).requestId = value;
|
|
163
147
|
}
|
|
164
148
|
get ip() {
|
|
165
|
-
|
|
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
|
-
|
|
153
|
+
L.ip = v;
|
|
169
154
|
}
|
|
170
155
|
return v;
|
|
171
156
|
}
|
|
172
157
|
set ip(value) {
|
|
173
|
-
this.
|
|
158
|
+
(this._lazy ?? (this._lazy = {})).ip = value;
|
|
174
159
|
}
|
|
175
160
|
get originalUrl() {
|
|
176
|
-
const v = this.
|
|
161
|
+
const v = this._lazy?.originalUrl;
|
|
177
162
|
return v !== undefined ? v : this.url || '';
|
|
178
163
|
}
|
|
179
164
|
set originalUrl(value) {
|
|
180
|
-
this.
|
|
165
|
+
(this._lazy ?? (this._lazy = {})).originalUrl = value;
|
|
181
166
|
}
|
|
182
167
|
get hostname() {
|
|
183
|
-
|
|
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
|
-
|
|
173
|
+
L.hostname = v;
|
|
188
174
|
}
|
|
189
175
|
return v;
|
|
190
176
|
}
|
|
191
177
|
set hostname(value) {
|
|
192
|
-
this.
|
|
178
|
+
(this._lazy ?? (this._lazy = {})).hostname = value;
|
|
193
179
|
}
|
|
194
180
|
get protocol() {
|
|
195
|
-
|
|
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
|
-
|
|
190
|
+
L.protocol = v;
|
|
204
191
|
}
|
|
205
192
|
return v;
|
|
206
193
|
}
|
|
207
194
|
set protocol(value) {
|
|
208
|
-
this.
|
|
195
|
+
(this._lazy ?? (this._lazy = {})).protocol = value;
|
|
209
196
|
}
|
|
210
197
|
get secure() {
|
|
211
|
-
const v = this.
|
|
198
|
+
const v = this._lazy?.secure;
|
|
212
199
|
return v !== undefined ? v : this.protocol === 'https';
|
|
213
200
|
}
|
|
214
201
|
set secure(value) {
|
|
215
|
-
this.
|
|
202
|
+
(this._lazy ?? (this._lazy = {})).secure = value;
|
|
216
203
|
}
|
|
217
204
|
get xhr() {
|
|
218
|
-
|
|
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
|
-
|
|
210
|
+
L.xhr = v;
|
|
223
211
|
}
|
|
224
212
|
return v;
|
|
225
213
|
}
|
|
226
214
|
set xhr(value) {
|
|
227
|
-
this.
|
|
215
|
+
(this._lazy ?? (this._lazy = {})).xhr = value;
|
|
228
216
|
}
|
|
229
217
|
get ips() {
|
|
230
|
-
|
|
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
|
-
|
|
228
|
+
L.ips = v;
|
|
240
229
|
}
|
|
241
230
|
return v;
|
|
242
231
|
}
|
|
243
232
|
set ips(value) {
|
|
244
|
-
this.
|
|
233
|
+
(this._lazy ?? (this._lazy = {})).ips = value;
|
|
245
234
|
}
|
|
246
235
|
get subdomains() {
|
|
247
|
-
|
|
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
|
-
|
|
243
|
+
L.subdomains = v;
|
|
254
244
|
}
|
|
255
245
|
return v;
|
|
256
246
|
}
|
|
257
247
|
set subdomains(value) {
|
|
258
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
497
|
-
|
|
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
|
-
|
|
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
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
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
|
|
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
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
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
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
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
|
-
|
|
1107
|
-
|
|
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
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
1311
|
+
httpRes.status(defaultStatus).json({
|
|
1222
1312
|
success: false,
|
|
1223
|
-
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 =
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
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
|
-
|
|
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 {
|