@linebridge/engine-neo 1.0.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/LICENSE +202 -0
- package/README.md +26 -0
- package/lib/SSEventStream.d.ts +29 -0
- package/lib/SSEventStream.js +85 -0
- package/lib/close.d.ts +11 -0
- package/lib/close.js +23 -0
- package/lib/engine.d.ts +105 -0
- package/lib/engine.js +221 -0
- package/lib/index.d.ts +17 -0
- package/lib/index.js +1 -0
- package/lib/listen.d.ts +13 -0
- package/lib/listen.js +44 -0
- package/lib/on_request.d.ts +21 -0
- package/lib/on_request.js +97 -0
- package/lib/register.d.ts +17 -0
- package/lib/register.js +5 -0
- package/lib/register_middleware.d.ts +14 -0
- package/lib/register_middleware.js +42 -0
- package/lib/request.d.ts +168 -0
- package/lib/request.js +400 -0
- package/lib/request_iterator.d.ts +22 -0
- package/lib/request_iterator.js +61 -0
- package/lib/response.d.ts +306 -0
- package/lib/response.js +742 -0
- package/lib/route_register.d.ts +10 -0
- package/lib/route_register.js +67 -0
- package/lib/utils.d.ts +14 -0
- package/lib/utils.js +30 -0
- package/lib/ws/Websocket.d.ts +173 -0
- package/lib/ws/Websocket.js +381 -0
- package/package.json +32 -0
- package/uws-wrapper.node +0 -0
package/lib/request.js
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Full HTTP request wrapper for the Neo engine.
|
|
3
|
+
*
|
|
4
|
+
* Wraps a raw uWS HttpRequest and provides high-level accessors for headers,
|
|
5
|
+
* body parsing (JSON, text, urlencoded, buffer), cookies, IP resolution,
|
|
6
|
+
* and path/query parameters.
|
|
7
|
+
*/
|
|
8
|
+
import * as cookie from "cookie";
|
|
9
|
+
import querystring from "fast-querystring";
|
|
10
|
+
import signature from "cookie-signature";
|
|
11
|
+
/**
|
|
12
|
+
* HTTP Request class wrapping a uWS HttpRequest.
|
|
13
|
+
*
|
|
14
|
+
* Provides parsed access to the request path, query string, headers, cookies,
|
|
15
|
+
* and body. Body parsing is lazy and supports JSON, text, urlencoded, and buffer.
|
|
16
|
+
*
|
|
17
|
+
* @typeParam TServer - The server type this request belongs to.
|
|
18
|
+
*/
|
|
19
|
+
export default class Request {
|
|
20
|
+
constructor() { }
|
|
21
|
+
/** Per-request local storage for middleware communication. */
|
|
22
|
+
_locals;
|
|
23
|
+
/** Whether the response stream is paused. */
|
|
24
|
+
_paused;
|
|
25
|
+
/** HTTP method string, normalized (e.g. "GET", "DELETE"). */
|
|
26
|
+
_method;
|
|
27
|
+
/** Full URL including query string. */
|
|
28
|
+
_url;
|
|
29
|
+
/** URL path portion without query string. */
|
|
30
|
+
_path;
|
|
31
|
+
/** Raw query string (without leading `?`). */
|
|
32
|
+
_query_str;
|
|
33
|
+
/** Resolved remote IP address. */
|
|
34
|
+
_remote_ip;
|
|
35
|
+
/** Resolved proxy IP address. */
|
|
36
|
+
_remote_proxy_ip;
|
|
37
|
+
/** Parsed cookies (lazy). */
|
|
38
|
+
_cookies;
|
|
39
|
+
/** URL path parameters extracted by uWS (e.g. `/user/:id`). */
|
|
40
|
+
_path_parameters;
|
|
41
|
+
/** Parsed query string parameters (lazy). */
|
|
42
|
+
_query_parameters;
|
|
43
|
+
/** The raw uWS HttpRequest object. */
|
|
44
|
+
_raw_request;
|
|
45
|
+
/** The raw uWS HttpResponse object (needed for `onData` and `getRemoteAddress`). */
|
|
46
|
+
_raw_response;
|
|
47
|
+
/** Request headers extracted synchronously. */
|
|
48
|
+
_headers;
|
|
49
|
+
/** Whether all body chunks have been received. */
|
|
50
|
+
_received;
|
|
51
|
+
/** Total bytes received so far for the body. */
|
|
52
|
+
_body_received_bytes;
|
|
53
|
+
/** Has the onData listener been registered for body parsing? */
|
|
54
|
+
_body_parser_on_data_registered;
|
|
55
|
+
/** Parsed body (populated lazily by `.json()`, `.text()`, etc.). */
|
|
56
|
+
_body;
|
|
57
|
+
/** The type that `.parseBody()` resolved to. */
|
|
58
|
+
_body_type;
|
|
59
|
+
/** Raw body buffer, populated after body is fully received. */
|
|
60
|
+
_body_raw;
|
|
61
|
+
/** Promise that resolves once all body data is received and parsed. */
|
|
62
|
+
_body_promise;
|
|
63
|
+
/** Callback invoked when body data is fully received. */
|
|
64
|
+
_onDone;
|
|
65
|
+
/** Context object for sharing data between middlewares and the handler. */
|
|
66
|
+
ctx;
|
|
67
|
+
/** The matched route for this request. */
|
|
68
|
+
route;
|
|
69
|
+
/**
|
|
70
|
+
* Creates a Request instance from raw uWS objects.
|
|
71
|
+
*/
|
|
72
|
+
static create(route, raw_request, raw_response) {
|
|
73
|
+
const req = new Request();
|
|
74
|
+
req._raw_request = raw_request;
|
|
75
|
+
req._raw_response = raw_response;
|
|
76
|
+
req.route = route;
|
|
77
|
+
req.ctx = route.ctx || Object.create(null);
|
|
78
|
+
req._query_str = raw_request.getQuery();
|
|
79
|
+
req._path = raw_request.getUrl();
|
|
80
|
+
req._received = true;
|
|
81
|
+
// normalize method - uWS uses "del" internally, we want "DELETE"
|
|
82
|
+
const rawMethod = raw_request.getMethod();
|
|
83
|
+
req._method = rawMethod === "del" ? "DELETE" : rawMethod.toUpperCase();
|
|
84
|
+
// Capture ALL headers synchronously. uWS HttpRequest is invalid after the first await.
|
|
85
|
+
req._headers = {};
|
|
86
|
+
raw_request.forEach((key, value) => {
|
|
87
|
+
req._headers[key] = value;
|
|
88
|
+
});
|
|
89
|
+
// extract path parameters (e.g. `/user/:id` → `{ id: "42" }`)
|
|
90
|
+
const keys = route.pathParametersKey;
|
|
91
|
+
if (keys.length > 0) {
|
|
92
|
+
const params = Object.create(null);
|
|
93
|
+
for (let i = 0; i < keys.length; i++) {
|
|
94
|
+
params[keys[i][0]] = raw_request.getParameter(keys[i][1]);
|
|
95
|
+
}
|
|
96
|
+
req._path_parameters = params;
|
|
97
|
+
}
|
|
98
|
+
return req;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The engine that owns this request.
|
|
102
|
+
*/
|
|
103
|
+
get engine() {
|
|
104
|
+
return this.route?.engine || null;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The raw uWS HttpRequest.
|
|
108
|
+
*/
|
|
109
|
+
get raw() {
|
|
110
|
+
return this._raw_request;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Lazily-parsed headers as a plain object.
|
|
114
|
+
* Header names are lowercased by uWS.
|
|
115
|
+
*/
|
|
116
|
+
get headers() {
|
|
117
|
+
return this._headers;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Pauses the underlying response stream.
|
|
121
|
+
* Used during body parsing to apply backpressure.
|
|
122
|
+
*/
|
|
123
|
+
pause() {
|
|
124
|
+
if (!this._paused) {
|
|
125
|
+
this._paused = true;
|
|
126
|
+
this._raw_response.pause();
|
|
127
|
+
}
|
|
128
|
+
return this;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Resumes the underlying response stream.
|
|
132
|
+
*/
|
|
133
|
+
resume() {
|
|
134
|
+
if (this._paused) {
|
|
135
|
+
this._paused = false;
|
|
136
|
+
this._raw_response.resume();
|
|
137
|
+
}
|
|
138
|
+
return this;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Signs a string with a secret using the same algorithm as `cookie-signature`.
|
|
142
|
+
*/
|
|
143
|
+
sign(string, secret) {
|
|
144
|
+
return signature.sign(string, secret);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Unsigns a signed value. Returns the original string or `undefined` on failure.
|
|
148
|
+
*/
|
|
149
|
+
unsign(signed_value, secret) {
|
|
150
|
+
const unsigned_value = signature.unsign(signed_value, secret);
|
|
151
|
+
return unsigned_value !== false ? unsigned_value : undefined;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Starts the body parser by registering a uWS `onData` callback.
|
|
155
|
+
*
|
|
156
|
+
* This is called before any body-consuming method (`.json()`, `.text()`, etc.)
|
|
157
|
+
* to begin buffering incoming body chunks.
|
|
158
|
+
*/
|
|
159
|
+
_body_parser_run(response, limit_bytes) {
|
|
160
|
+
if (this._body_parser_on_data_registered)
|
|
161
|
+
return true;
|
|
162
|
+
this._received = false;
|
|
163
|
+
this._body_received_bytes = 0;
|
|
164
|
+
this._body_parser_on_data_registered = true;
|
|
165
|
+
const chunks = [];
|
|
166
|
+
const max_buffer = this.engine?.options.max_body_buffer ?? 1024 * 1024;
|
|
167
|
+
this._raw_response.onData((chunk, is_last) => {
|
|
168
|
+
if (response.completed)
|
|
169
|
+
return;
|
|
170
|
+
if (chunk.byteLength > 0) {
|
|
171
|
+
this._body_received_bytes += chunk.byteLength;
|
|
172
|
+
if (this._body_received_bytes > limit_bytes) {
|
|
173
|
+
response.status(413).send("Payload Too Large");
|
|
174
|
+
this._received = true;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const chunkCopy = Buffer.allocUnsafe(chunk.byteLength);
|
|
178
|
+
chunkCopy.set(new Uint8Array(chunk));
|
|
179
|
+
chunks.push(chunkCopy);
|
|
180
|
+
if (this._body_received_bytes > max_buffer &&
|
|
181
|
+
!this._body_promise) {
|
|
182
|
+
this.pause();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (is_last) {
|
|
186
|
+
this._received = true;
|
|
187
|
+
if (chunks.length === 0) {
|
|
188
|
+
this._body_raw = Buffer.allocUnsafe(0);
|
|
189
|
+
}
|
|
190
|
+
else if (chunks.length === 1) {
|
|
191
|
+
this._body_raw = chunks[0];
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
this._body_raw = Buffer.concat(chunks, this._body_received_bytes);
|
|
195
|
+
}
|
|
196
|
+
if (this._onDone) {
|
|
197
|
+
this._onDone();
|
|
198
|
+
this._onDone = null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Returns the entire request body as a Buffer.
|
|
206
|
+
*/
|
|
207
|
+
async buffer() {
|
|
208
|
+
if (this._body_type === "buffer") {
|
|
209
|
+
return this._body;
|
|
210
|
+
}
|
|
211
|
+
await this.parseBody();
|
|
212
|
+
return this._body_raw || Buffer.allocUnsafe(0);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Returns the entire request body as a string.
|
|
216
|
+
*/
|
|
217
|
+
async text() {
|
|
218
|
+
if (this._body_type === "text") {
|
|
219
|
+
return this._body;
|
|
220
|
+
}
|
|
221
|
+
await this.parseBody();
|
|
222
|
+
return typeof this._body === "string" ? this._body : "";
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Parses the request body as JSON.
|
|
226
|
+
*/
|
|
227
|
+
async json(default_value = {}) {
|
|
228
|
+
if (this._body_type === "json") {
|
|
229
|
+
return this._body;
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
await this.parseBody();
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
if (default_value !== undefined) {
|
|
236
|
+
return default_value;
|
|
237
|
+
}
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
return this._body;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Parses the request body as URL-encoded form data.
|
|
244
|
+
*/
|
|
245
|
+
async urlencoded() {
|
|
246
|
+
if (this._body_type === "urlencoded") {
|
|
247
|
+
return this._body;
|
|
248
|
+
}
|
|
249
|
+
await this.parseBody();
|
|
250
|
+
return this._body;
|
|
251
|
+
}
|
|
252
|
+
get locals() {
|
|
253
|
+
if (!this._locals) {
|
|
254
|
+
this._locals = Object.create(null);
|
|
255
|
+
}
|
|
256
|
+
return this._locals;
|
|
257
|
+
}
|
|
258
|
+
/** Whether the request stream is paused. */
|
|
259
|
+
get paused() {
|
|
260
|
+
return this._paused;
|
|
261
|
+
}
|
|
262
|
+
/** Normalized HTTP method (uppercase). */
|
|
263
|
+
get method() {
|
|
264
|
+
return this._method;
|
|
265
|
+
}
|
|
266
|
+
/** Full URL (path + query string). */
|
|
267
|
+
get url() {
|
|
268
|
+
if (this._url) {
|
|
269
|
+
return this._url;
|
|
270
|
+
}
|
|
271
|
+
this._url = this._path + (this._query_str ? "?" + this._query_str : "");
|
|
272
|
+
return this._url;
|
|
273
|
+
}
|
|
274
|
+
/** URL path (no query string). */
|
|
275
|
+
get path() {
|
|
276
|
+
return this._path;
|
|
277
|
+
}
|
|
278
|
+
/** Parsed query string parameters. */
|
|
279
|
+
get query() {
|
|
280
|
+
if (this._query_parameters) {
|
|
281
|
+
return this._query_parameters;
|
|
282
|
+
}
|
|
283
|
+
this._query_parameters = querystring.parse(this._query_str);
|
|
284
|
+
return this._query_parameters;
|
|
285
|
+
}
|
|
286
|
+
/** URL path parameters extracted (e.g. `/user/:id` → `{ id: "42" }`). */
|
|
287
|
+
get params() {
|
|
288
|
+
return this._path_parameters;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Parsed cookies from the `Cookie` header.
|
|
292
|
+
* Parsed lazily on first access.
|
|
293
|
+
*/
|
|
294
|
+
get cookies() {
|
|
295
|
+
if (this._cookies) {
|
|
296
|
+
return this._cookies;
|
|
297
|
+
}
|
|
298
|
+
const header = this.headers["cookie"];
|
|
299
|
+
this._cookies = header
|
|
300
|
+
? cookie.parseCookie(header)
|
|
301
|
+
: Object.create(null);
|
|
302
|
+
return this._cookies;
|
|
303
|
+
}
|
|
304
|
+
/** The parsed body. Populated after calling `.parseBody()`, `.json()`, `.text()`, etc. */
|
|
305
|
+
get body() {
|
|
306
|
+
return this._body;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Automatically parses the request body based on `Content-Type`.
|
|
310
|
+
*/
|
|
311
|
+
async parseBody() {
|
|
312
|
+
if (this._body_promise)
|
|
313
|
+
return this._body_promise;
|
|
314
|
+
this._body_promise = new Promise((resolve) => {
|
|
315
|
+
if (this._received) {
|
|
316
|
+
return resolve(this._finalize_body());
|
|
317
|
+
}
|
|
318
|
+
this._onDone = () => {
|
|
319
|
+
resolve(this._finalize_body());
|
|
320
|
+
};
|
|
321
|
+
});
|
|
322
|
+
return this._body_promise;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Decodes the raw body buffer based on Content-Type.
|
|
326
|
+
*/
|
|
327
|
+
_finalize_body() {
|
|
328
|
+
if (this._body !== undefined)
|
|
329
|
+
return this._body;
|
|
330
|
+
const raw = this._body_raw || Buffer.allocUnsafe(0);
|
|
331
|
+
const contentType = this._headers["content-type"] || "";
|
|
332
|
+
if (contentType.includes("application/json")) {
|
|
333
|
+
try {
|
|
334
|
+
this._body = JSON.parse(raw.toString());
|
|
335
|
+
this._body_type = "json";
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
this._body = {};
|
|
339
|
+
this._body_type = "json";
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
else if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
343
|
+
this._body = querystring.parse(raw.toString());
|
|
344
|
+
this._body_type = "urlencoded";
|
|
345
|
+
}
|
|
346
|
+
else if (contentType.includes("text/") ||
|
|
347
|
+
contentType.includes("application/xml") ||
|
|
348
|
+
contentType.includes("application/javascript") ||
|
|
349
|
+
!contentType) {
|
|
350
|
+
this._body = raw.toString();
|
|
351
|
+
this._body_type = "text";
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
this._body = raw;
|
|
355
|
+
this._body_type = "buffer";
|
|
356
|
+
}
|
|
357
|
+
return this._body;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Resolved remote IP address.
|
|
361
|
+
*
|
|
362
|
+
* When `trust_proxy` is enabled, it reads from `x-forwarded-for`.
|
|
363
|
+
* Otherwise, it uses the direct remote address from uWS.
|
|
364
|
+
*/
|
|
365
|
+
get ip() {
|
|
366
|
+
if (this._remote_ip) {
|
|
367
|
+
return this._remote_ip;
|
|
368
|
+
}
|
|
369
|
+
const x_forwarded_for = this.locals._xff;
|
|
370
|
+
const trust_proxy = this.engine?.options.trust_proxy;
|
|
371
|
+
if (trust_proxy && x_forwarded_for) {
|
|
372
|
+
// take the first IP in the x-forwarded-for chain (the original client)
|
|
373
|
+
const commaIdx = x_forwarded_for.indexOf(",");
|
|
374
|
+
this._remote_ip =
|
|
375
|
+
commaIdx === -1
|
|
376
|
+
? x_forwarded_for.trim()
|
|
377
|
+
: x_forwarded_for.slice(0, commaIdx).trim();
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
try {
|
|
381
|
+
this._remote_ip = Buffer.from(this._raw_response.getRemoteAddressAsText()).toString();
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
// Fallback for when the socket is already closed
|
|
385
|
+
this._remote_ip = "";
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return this._remote_ip;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* The proxy IP as reported by uWS.
|
|
392
|
+
*/
|
|
393
|
+
get proxy_ip() {
|
|
394
|
+
if (this._remote_proxy_ip) {
|
|
395
|
+
return this._remote_proxy_ip;
|
|
396
|
+
}
|
|
397
|
+
this._remote_proxy_ip = Buffer.from(this._raw_response.getProxiedRemoteAddressAsText()).toString();
|
|
398
|
+
return this._remote_proxy_ip;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The middleware iteration loop.
|
|
3
|
+
*
|
|
4
|
+
* Walks through the middleware array one element at a time, calling each
|
|
5
|
+
* middleware's `execute` method with a `next` callback that recurses into the
|
|
6
|
+
* next middleware or the final route handler.
|
|
7
|
+
*/
|
|
8
|
+
import type Engine from "./engine.js";
|
|
9
|
+
import type Request from "./request.js";
|
|
10
|
+
import type Response from "./response.js";
|
|
11
|
+
import type { Route } from "linebridge/classes/Route/index";
|
|
12
|
+
import type { Handler } from "linebridge/classes/Handler/index";
|
|
13
|
+
/**
|
|
14
|
+
* Recursively iterates the middleware chain, then calls the route handler.
|
|
15
|
+
*
|
|
16
|
+
* On the first call (cursor === 0) it parses the request body (if needed).
|
|
17
|
+
* Each middleware receives `(req, res, next)`. If a middleware does not call `next()`,
|
|
18
|
+
* the iterator auto-advances to avoid hanging (unless the response was already sent).
|
|
19
|
+
*
|
|
20
|
+
* @param cursor - Zero-based index into the middleware array. Increments on each recursion.
|
|
21
|
+
*/
|
|
22
|
+
export default function (this: Engine, request: Request<any>, response: Response<any>, route: Route<typeof this.server>, allMiddlewares: Handler[], cursor?: number): Promise<void>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The middleware iteration loop.
|
|
3
|
+
*
|
|
4
|
+
* Walks through the middleware array one element at a time, calling each
|
|
5
|
+
* middleware's `execute` method with a `next` callback that recurses into the
|
|
6
|
+
* next middleware or the final route handler.
|
|
7
|
+
*/
|
|
8
|
+
/** HTTP methods that never carry a request body. */
|
|
9
|
+
const BODYLESS_METHODS = new Set(["GET", "HEAD", "OPTIONS", "TRACE"]);
|
|
10
|
+
/**
|
|
11
|
+
* Recursively iterates the middleware chain, then calls the route handler.
|
|
12
|
+
*
|
|
13
|
+
* On the first call (cursor === 0) it parses the request body (if needed).
|
|
14
|
+
* Each middleware receives `(req, res, next)`. If a middleware does not call `next()`,
|
|
15
|
+
* the iterator auto-advances to avoid hanging (unless the response was already sent).
|
|
16
|
+
*
|
|
17
|
+
* @param cursor - Zero-based index into the middleware array. Increments on each recursion.
|
|
18
|
+
*/
|
|
19
|
+
export default async function (request, response, route, allMiddlewares, cursor = 0) {
|
|
20
|
+
if (response.completed)
|
|
21
|
+
return;
|
|
22
|
+
const middleware = allMiddlewares[cursor];
|
|
23
|
+
try {
|
|
24
|
+
// on the first middleware, parse the request body before passing control
|
|
25
|
+
if (cursor == 0) {
|
|
26
|
+
response._cork = true;
|
|
27
|
+
if (!BODYLESS_METHODS.has(request.method)) {
|
|
28
|
+
await request.parseBody();
|
|
29
|
+
if (response.completed)
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (middleware) {
|
|
34
|
+
// track the cursor position to detect double-next() calls
|
|
35
|
+
response._track_middleware_cursor(cursor);
|
|
36
|
+
let nextCalled = false;
|
|
37
|
+
/**
|
|
38
|
+
* Advances to the next middleware or the route handler.
|
|
39
|
+
*/
|
|
40
|
+
const next = async () => {
|
|
41
|
+
nextCalled = true;
|
|
42
|
+
await this.request_iterator(request, response, route, allMiddlewares, cursor + 1);
|
|
43
|
+
};
|
|
44
|
+
await middleware.execute(request, response, next);
|
|
45
|
+
// if middleware didn't call next() and didn't send a response, auto-advance
|
|
46
|
+
if (!nextCalled && !response.completed) {
|
|
47
|
+
await next();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
// no more middlewares - execute the route handler
|
|
52
|
+
await route.handler.execute(request, response);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
console.error("Unhandled error:", error);
|
|
57
|
+
if (!response.completed) {
|
|
58
|
+
response.status(500).json({ error: error.message });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|