tina4-nodejs 3.13.36 → 3.13.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CLAUDE.md +51 -19
  2. package/package.json +5 -3
  3. package/packages/cli/src/bin.ts +7 -0
  4. package/packages/cli/src/commands/init.ts +1 -0
  5. package/packages/cli/src/commands/metrics.ts +154 -0
  6. package/packages/cli/src/commands/routes.ts +3 -3
  7. package/packages/core/public/js/tina4-dev-admin.js +212 -212
  8. package/packages/core/public/js/tina4-dev-admin.min.js +212 -212
  9. package/packages/core/src/auth.ts +112 -2
  10. package/packages/core/src/cache.ts +2 -2
  11. package/packages/core/src/devAdmin.ts +75 -26
  12. package/packages/core/src/devMailbox.ts +4 -0
  13. package/packages/core/src/dotenv.ts +13 -4
  14. package/packages/core/src/events.ts +86 -4
  15. package/packages/core/src/graphql.ts +182 -128
  16. package/packages/core/src/htmlElement.ts +62 -3
  17. package/packages/core/src/index.ts +14 -8
  18. package/packages/core/src/logger.ts +1 -1
  19. package/packages/core/src/mcp.test.ts +1 -1
  20. package/packages/core/src/messenger.ts +111 -11
  21. package/packages/core/src/metrics.ts +232 -33
  22. package/packages/core/src/middleware.ts +129 -39
  23. package/packages/core/src/plan.ts +1 -1
  24. package/packages/core/src/queue.ts +1 -1
  25. package/packages/core/src/queueBackends/kafkaBackend.ts +1 -1
  26. package/packages/core/src/queueBackends/mongoBackend.ts +1 -1
  27. package/packages/core/src/queueBackends/rabbitmqBackend.ts +1 -1
  28. package/packages/core/src/rateLimiter.ts +1 -1
  29. package/packages/core/src/response.ts +90 -6
  30. package/packages/core/src/router.ts +2 -2
  31. package/packages/core/src/server.ts +26 -4
  32. package/packages/core/src/session.ts +130 -18
  33. package/packages/core/src/sessionHandlers/databaseHandler.ts +10 -0
  34. package/packages/core/src/sessionHandlers/mongoHandler.ts +21 -4
  35. package/packages/core/src/sessionHandlers/redisHandler.ts +28 -7
  36. package/packages/core/src/sessionHandlers/valkeyHandler.ts +27 -8
  37. package/packages/core/src/testClient.ts +1 -1
  38. package/packages/core/src/websocket.ts +247 -33
  39. package/packages/core/src/websocketBackplane.ts +210 -10
  40. package/packages/core/src/wsdl.ts +55 -21
  41. package/packages/orm/src/adapters/pg-types.d.ts +60 -0
  42. package/packages/orm/src/adapters/postgres.ts +26 -4
  43. package/packages/orm/src/adapters/sqlite.ts +112 -13
  44. package/packages/orm/src/baseModel.ts +8 -3
  45. package/packages/orm/src/cachedDatabase.ts +15 -6
  46. package/packages/orm/src/database.ts +257 -55
  47. package/packages/orm/src/index.ts +2 -1
  48. package/packages/orm/src/migration.ts +2 -2
  49. package/packages/orm/src/seeder.ts +443 -65
  50. package/packages/swagger/src/ui.ts +1 -1
@@ -22,32 +22,45 @@ export class MiddlewareChain {
22
22
  this.middlewares.push(fn);
23
23
  }
24
24
 
25
+ /**
26
+ * Run the chain in REGISTRATION order — each middleware runs exactly once,
27
+ * in the order it was attached via use(). The chain advances from ONE
28
+ * source only: `next()`. (The old runner double-advanced — a for-loop index
29
+ * AND next() both incremented — so every other middleware was silently
30
+ * skipped. Fixed by driving the chain purely by next(), mirroring Python's
31
+ * _make_mw_continuation Russian-doll continuation.)
32
+ *
33
+ * A middleware may stop the chain by:
34
+ * - not calling next() (it owns the response), or
35
+ * - ending the response (res.raw.writableEnded).
36
+ * Returns true when the whole chain ran to completion (handler may proceed),
37
+ * false when it was short-circuited.
38
+ */
25
39
  async run(req: Tina4Request, res: Tina4Response): Promise<boolean> {
26
- let index = 0;
27
- let completed = true;
40
+ const dispatch = async (i: number): Promise<void> => {
41
+ if (i >= this.middlewares.length) return;
42
+ let advanced = false;
28
43
 
29
- const next = (): void => {
30
- index++;
31
- };
44
+ const next = (): void => {
45
+ advanced = true;
46
+ };
32
47
 
33
- for (index = 0; index < this.middlewares.length; index++) {
34
- const prevIndex = index;
35
- await this.middlewares[index](req, res, next);
48
+ await this.middlewares[i](req, res, next);
36
49
 
37
- // If response was already sent, stop the chain
38
- if (res.raw.writableEnded) {
39
- completed = false;
40
- break;
41
- }
50
+ // The middleware owns the response stop the chain.
51
+ if (res.raw.writableEnded) return;
42
52
 
43
- // If next() wasn't called, stop the chain
44
- if (index === prevIndex) {
45
- // next() increments index, so if it wasn't called, index stays the same
46
- // But we increment in the for loop, so we need to check differently
53
+ // next() was called advance to the following middleware (exactly one
54
+ // step). next() not called → the middleware short-circuited; stop here.
55
+ if (advanced) {
56
+ await dispatch(i + 1);
47
57
  }
48
- }
58
+ };
49
59
 
50
- return completed;
60
+ await dispatch(0);
61
+
62
+ // Completed (handler may proceed) iff no middleware ended the response.
63
+ return !res.raw.writableEnded;
51
64
  }
52
65
  }
53
66
 
@@ -64,6 +77,39 @@ export class MiddlewareChain {
64
77
  * If a "before" method returns a response whose status code is >= 400
65
78
  * the chain short-circuits and runBefore returns shouldContinue = false.
66
79
  */
80
+ /**
81
+ * Produce the deterministic clean 500 for a throwing class-based middleware
82
+ * (M2). Mirrors Python's _middleware_500: LOG via Log.error (class + method +
83
+ * error type + message — never silent) then return a 500 with the exact JSON
84
+ * body shape shared across all four frameworks. The worker never crashes and
85
+ * no unhandled exception leaks.
86
+ */
87
+ function middleware500(
88
+ res: Tina4Response,
89
+ mwClass: any,
90
+ methodName: string,
91
+ error: unknown,
92
+ ): Tina4Response {
93
+ const clsName = mwClass?.name ?? mwClass?.constructor?.name ?? "Middleware";
94
+ const err = error as { name?: string; message?: string };
95
+ const type = err?.name ?? (error as object)?.constructor?.name ?? "Error";
96
+ const message = err?.message ?? String(error);
97
+ try {
98
+ Log.error(`Middleware ${clsName}.${methodName} raised ${type}: ${message}`);
99
+ } catch {
100
+ /* never let a broken logger swallow the 500 */
101
+ }
102
+ // res is callable (json) in real Response; tolerate either shape.
103
+ if (typeof (res as any).json === "function") {
104
+ (res as any).json({ error: "Internal Server Error", status: 500 }, 500);
105
+ } else if (typeof (res as any) === "function") {
106
+ (res as any)({ error: "Internal Server Error", status: 500 }, 500);
107
+ } else if (typeof (res as any).status === "function") {
108
+ (res as any).status(500);
109
+ }
110
+ return res;
111
+ }
112
+
67
113
  export class MiddlewareRunner {
68
114
  /** Globally registered middleware classes (parity with PHP/Ruby/Python orchestrators). */
69
115
  private static globalMiddleware: any[] = [];
@@ -90,16 +136,43 @@ export class MiddlewareRunner {
90
136
  }
91
137
 
92
138
  /**
93
- * Execute every beforeX static method found on the supplied classes,
94
- * in order. Returns the (possibly mutated) request and response pair and a
95
- * boolean indicating whether the route handler should still run.
139
+ * Discover the before-prefixed / after-prefixed method names on a
140
+ * middleware class in DEFINITION order (M1).
141
+ * `Object.getOwnPropertyNames` returns a class's own
142
+ * static method names in source-declaration order — we deliberately do NOT
143
+ * sort() them, so within a class the hooks run in the order they were
144
+ * written (parity with Python walking __dict__, PHP get_class_methods, Ruby
145
+ * instance_methods(false)). Cross-class order is the natural iteration of
146
+ * the registered classes = REGISTRATION order.
147
+ */
148
+ private static methodNames(cls: any, prefix: string): string[] {
149
+ return Object.getOwnPropertyNames(cls).filter(
150
+ (name) => typeof cls[name] === "function" && name.startsWith(prefix),
151
+ );
152
+ }
153
+
154
+ /**
155
+ * Execute every beforeX static method found on the supplied classes.
156
+ *
157
+ * ORDER (M1): cross-class = REGISTRATION order (the order classes were
158
+ * attached via Router.use / MiddlewareRunner.use); within a class =
159
+ * DEFINITION order (source order, never alphabetical). before_* run before
160
+ * the handler.
161
+ *
162
+ * THROW (M2): each before* call is wrapped — a throwing middleware is
163
+ * LOGGED and produces a deterministic clean 500 (it never crashes the
164
+ * worker / leaks an unhandled exception), and the chain short-circuits
165
+ * (skip = true, handler skipped).
96
166
  *
97
- * Short-circuits when a before method sets a status >= 400.
167
+ * Short-circuits (skip = true, handler skipped) when a before* sets a
168
+ * status >= 400 or ends/500s the response.
98
169
  *
99
170
  * ASYNC — each hook is awaited so middleware can perform async work (e.g.
100
171
  * the distributed responseCache before-hook awaiting `backend.get`). Awaiting
101
172
  * a synchronous hook that returns an array is harmless (the array resolves
102
173
  * immediately), so existing sync hooks keep working unchanged.
174
+ *
175
+ * Returns [req, res, shouldContinue].
103
176
  */
104
177
  static async runBefore(
105
178
  classes: any[],
@@ -107,13 +180,16 @@ export class MiddlewareRunner {
107
180
  res: Tina4Response,
108
181
  ): Promise<[Tina4Request, Tina4Response, boolean]> {
109
182
  for (const cls of classes) {
110
- const methods = Object.getOwnPropertyNames(cls).filter(
111
- (name) => typeof cls[name] === "function" && name.startsWith("before"),
112
- );
113
- for (const method of methods) {
114
- const result = await cls[method](req, res);
115
- if (Array.isArray(result)) {
116
- [req, res] = result as [Tina4Request, Tina4Response];
183
+ for (const method of MiddlewareRunner.methodNames(cls, "before")) {
184
+ try {
185
+ const result = await cls[method](req, res);
186
+ if (Array.isArray(result)) {
187
+ [req, res] = result as [Tina4Request, Tina4Response];
188
+ }
189
+ } catch (error) {
190
+ // Throw → logged clean 500, skip the handler (deterministic).
191
+ res = middleware500(res, cls, method, error);
192
+ return [req, res, false];
117
193
  }
118
194
  // Short-circuit if the middleware set an error status
119
195
  if (res.raw.statusCode >= 400 || res.raw.writableEnded) {
@@ -125,8 +201,19 @@ export class MiddlewareRunner {
125
201
  }
126
202
 
127
203
  /**
128
- * Execute every afterX static method found on the supplied classes,
129
- * in order. Returns the (possibly mutated) request and response pair.
204
+ * Execute every afterX static method found on the supplied classes.
205
+ *
206
+ * ORDER (M1): cross-class = REGISTRATION order; within a class = DEFINITION
207
+ * order. after_* run after the handler.
208
+ *
209
+ * THROW (M2): each after* call is wrapped — a throwing after middleware is
210
+ * LOGGED and produces a clean 500, then the remaining after* STILL run
211
+ * (they may add headers / logging). No unhandled exception leaks.
212
+ *
213
+ * AFTER-ON-4xx RULE (M2): after_* ALWAYS run, even when a before_*
214
+ * short-circuited with status >= 400 and the handler was skipped — so they
215
+ * can still add headers / logging. The dispatcher calls runAfter
216
+ * unconditionally after the before/handler block (see server.ts).
130
217
  *
131
218
  * ASYNC — each hook is awaited (e.g. the responseCache after-hook awaiting
132
219
  * `backend.set`). Awaiting a synchronous hook is harmless, so existing sync
@@ -138,13 +225,16 @@ export class MiddlewareRunner {
138
225
  res: Tina4Response,
139
226
  ): Promise<[Tina4Request, Tina4Response]> {
140
227
  for (const cls of classes) {
141
- const methods = Object.getOwnPropertyNames(cls).filter(
142
- (name) => typeof cls[name] === "function" && name.startsWith("after"),
143
- );
144
- for (const method of methods) {
145
- const result = await cls[method](req, res);
146
- if (Array.isArray(result)) {
147
- [req, res] = result as [Tina4Request, Tina4Response];
228
+ for (const method of MiddlewareRunner.methodNames(cls, "after")) {
229
+ try {
230
+ const result = await cls[method](req, res);
231
+ if (Array.isArray(result)) {
232
+ [req, res] = result as [Tina4Request, Tina4Response];
233
+ }
234
+ } catch (error) {
235
+ // Throw → logged clean 500, but remaining after* STILL run.
236
+ res = middleware500(res, cls, method, error);
237
+ continue;
148
238
  }
149
239
  }
150
240
  }
@@ -487,7 +487,7 @@ export const Plan = {
487
487
  }),
488
488
  signal: AbortSignal.timeout(120_000),
489
489
  });
490
- result = await resp.json();
490
+ result = (await resp.json()) as Record<string, unknown>;
491
491
  } catch (e) {
492
492
  return { ok: false, error: `AI backend unreachable: ${(e as Error).message}` };
493
493
  }
@@ -71,7 +71,7 @@ export interface QueueBackendInterface {
71
71
  push(queue: string, payload: unknown, delay?: number): string;
72
72
  pop(queue: string): QueueJob | null;
73
73
  size(queue: string): number;
74
- clear(queue: string): number;
74
+ clear(queue: string): void;
75
75
  }
76
76
 
77
77
  // ── Queue ────────────────────────────────────────────────────
@@ -328,7 +328,7 @@ export class KafkaBackend implements QueueBackend {
328
328
  const id = randomUUID();
329
329
  const now = new Date().toISOString();
330
330
 
331
- const job: QueueJob = {
331
+ const job = {
332
332
  id,
333
333
  payload,
334
334
  status: "pending",
@@ -198,7 +198,7 @@ export class MongoBackend implements QueueBackend {
198
198
  const id = randomUUID();
199
199
  const now = new Date().toISOString();
200
200
 
201
- const job: QueueJob = {
201
+ const job = {
202
202
  id,
203
203
  payload,
204
204
  status: "pending",
@@ -510,7 +510,7 @@ export class RabbitMQBackend implements QueueBackend {
510
510
  const id = randomUUID();
511
511
  const now = new Date().toISOString();
512
512
 
513
- const job: QueueJob = {
513
+ const job = {
514
514
  id,
515
515
  payload,
516
516
  status: "pending",
@@ -172,7 +172,7 @@ export class RateLimiter {
172
172
 
173
173
  /** Apply rate limiting to a request/response pair. Sets headers and 429 if exceeded. */
174
174
  apply(request: Tina4Request, response: Tina4Response): [Tina4Request, Tina4Response] {
175
- const ip = (request as Record<string, unknown>).ip as string ?? "unknown";
175
+ const ip = request.ip ?? "unknown";
176
176
  const result = this.check(ip);
177
177
 
178
178
  response.header("X-RateLimit-Limit", String(result.limit));
@@ -1,8 +1,29 @@
1
1
  import type { ServerResponse } from "node:http";
2
2
  import fs from "node:fs";
3
3
  import nodePath from "node:path";
4
+ import { once } from "node:events";
4
5
  import type { Tina4Response, CookieOptions } from "./types.js";
5
6
 
7
+ /**
8
+ * Best-effort close of a streaming source on client disconnect or a mid-stream
9
+ * error. Async generators expose `.return()`; some custom iterables expose
10
+ * `.close()`/`.aclose()`. Any failure here is swallowed — cleanup is advisory.
11
+ */
12
+ async function closeSource(source: AsyncIterable<unknown>): Promise<void> {
13
+ const s = source as {
14
+ return?: () => unknown;
15
+ close?: () => unknown;
16
+ aclose?: () => unknown;
17
+ };
18
+ try {
19
+ if (typeof s.return === "function") await s.return();
20
+ else if (typeof s.aclose === "function") await s.aclose();
21
+ else if (typeof s.close === "function") await s.close();
22
+ } catch {
23
+ /* cleanup is best-effort */
24
+ }
25
+ }
26
+
6
27
  /** Cache Frond instances by template directory to avoid repeated instantiation. */
7
28
  const _frondCache = new Map<string, InstanceType<any>>();
8
29
 
@@ -116,8 +137,15 @@ function toJsonable(data: unknown): unknown {
116
137
  export function createResponse(res: ServerResponse): Tina4Response {
117
138
 
118
139
  // ── Guard: prevent writing after headers are sent ──
119
- const safeEnd = (...args: Parameters<typeof res.end>) => {
120
- if (!res.headersSent) (res.end as Function)(...args);
140
+ const safeEnd = (chunk?: string | Buffer, encoding?: BufferEncoding) => {
141
+ if (res.headersSent) return;
142
+ if (chunk === undefined) {
143
+ res.end();
144
+ } else if (encoding === undefined) {
145
+ res.end(chunk);
146
+ } else {
147
+ res.end(chunk, encoding);
148
+ }
121
149
  };
122
150
  const safeSetHeader = (name: string, value: string | number | readonly string[]) => {
123
151
  if (!res.headersSent) res.setHeader(name, value);
@@ -354,12 +382,68 @@ export function createResponse(res: ServerResponse): Tina4Response {
354
382
  "X-Accel-Buffering": "no",
355
383
  });
356
384
 
357
- for await (const chunk of source) {
358
- const data = typeof chunk === "string" ? chunk : chunk.toString();
359
- res.write(data);
385
+ // True once the client has gone away (socket destroyed) or the response
386
+ // has been finished keep checking so we bail cleanly mid-stream rather
387
+ // than writing into a dead socket or buffering forever.
388
+ const streamClosed = (): boolean =>
389
+ res.writableEnded || (res.socket?.destroyed ?? false);
390
+
391
+ // Keep-alive heartbeat: periodically write a ':' SSE comment line on a
392
+ // long-lived stream so proxies/load-balancers don't reap an idle but
393
+ // healthy connection. Opt-out via TINA4_SSE_HEARTBEAT=0 (any non-positive
394
+ // value disables it). The interval is unref'd so it never holds the
395
+ // process open on its own.
396
+ const heartbeatSeconds = parseFloat(process.env.TINA4_SSE_HEARTBEAT ?? "15");
397
+ let heartbeat: ReturnType<typeof setInterval> | null = null;
398
+ if (Number.isFinite(heartbeatSeconds) && heartbeatSeconds > 0) {
399
+ heartbeat = setInterval(() => {
400
+ if (streamClosed()) return;
401
+ try {
402
+ res.write(": keep-alive\n\n");
403
+ } catch {
404
+ /* write race with a closing socket — the loop's guard handles it */
405
+ }
406
+ }, heartbeatSeconds * 1000);
407
+ heartbeat.unref?.();
408
+ }
409
+
410
+ const stopHeartbeat = (): void => {
411
+ if (heartbeat !== null) {
412
+ clearInterval(heartbeat);
413
+ heartbeat = null;
414
+ }
415
+ };
416
+
417
+ try {
418
+ for await (const chunk of source) {
419
+ // Client disconnected mid-stream — stop cleanly. Closing the source
420
+ // (best-effort) lets the producer release resources.
421
+ if (streamClosed()) {
422
+ await closeSource(source);
423
+ break;
424
+ }
425
+ const data = typeof chunk === "string" ? chunk : chunk.toString();
426
+ const ok = res.write(data);
427
+ // Slow-client backpressure: when write() returns false the kernel
428
+ // buffer is full. Wait for it to drain before pulling the next chunk
429
+ // so we don't unboundedly buffer ahead of a client that can't keep up.
430
+ if (!ok && !streamClosed()) {
431
+ await once(res, "drain").catch(() => {
432
+ /* socket errored/closed while waiting — loop guard handles it */
433
+ });
434
+ }
435
+ }
436
+ } catch (err) {
437
+ // The generator/source itself raised mid-stream. Log and stop cleanly —
438
+ // end the stream rather than crashing the request handler/worker.
439
+ const { Log } = await import("./logger.js");
440
+ Log.error(`SSE/stream source error: ${err instanceof Error ? err.message : String(err)}`);
441
+ await closeSource(source);
442
+ } finally {
443
+ stopHeartbeat();
360
444
  }
361
445
 
362
- res.end();
446
+ if (!res.writableEnded) res.end();
363
447
  return response;
364
448
  };
365
449
 
@@ -615,10 +615,10 @@ export class RouteGroup {
615
615
  constructor(
616
616
  private router: Router,
617
617
  private prefix: string,
618
- private groupMiddlewares?: Middleware[],
618
+ private groupMiddlewares?: MiddlewareSpec[],
619
619
  ) {}
620
620
 
621
- private mergeMiddlewares(routeMiddlewares?: Middleware[]): Middleware[] | undefined {
621
+ private mergeMiddlewares(routeMiddlewares?: MiddlewareSpec[]): MiddlewareSpec[] | undefined {
622
622
  const group = this.groupMiddlewares ?? [];
623
623
  const route = routeMiddlewares ?? [];
624
624
  const merged = [...group, ...route];
@@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url";
6
6
  import { execFileSync, exec } from "node:child_process";
7
7
  import cluster from "node:cluster";
8
8
  import os from "node:os";
9
+ import type { Socket } from "node:net";
9
10
  import type { Tina4Config, Tina4Request, Tina4Response } from "./types.js";
10
11
  import { Router, defaultRouter, runRouteMiddlewares } from "./router.js";
11
12
  import { validToken, getPayload, refreshToken } from "./auth.js";
@@ -675,9 +676,23 @@ export async function startServer(config?: Tina4Config): Promise<{
675
676
  router: Router;
676
677
  port: number;
677
678
  }> {
678
- // Load .env early so TINA4_DEBUG is available for cluster decision
679
+ // Load env early so TINA4_DEBUG is available for the cluster decision.
680
+ // Precedence MUST be: real environment (set before boot) > .env.local > .env.
681
+ // loadEnv(override=false) is first-wins (only sets a key not already present),
682
+ // so load .env.local BEFORE .env — both with override=false. A real env var
683
+ // set before boot is already present and wins over both; .env.local fills
684
+ // local-only keys; .env fills whatever neither set. Loading .env.local with
685
+ // override=true would let a stray gitignored .env.local clobber an explicitly
686
+ // set real env var (e.g. a production TINA4_SECRET) — never do that.
687
+ loadEnv(".env.local");
679
688
  loadEnv();
680
689
 
690
+ // Auto-generate a per-machine dev secret to a gitignored .env.local when one
691
+ // is missing (dev only, never CI/prod). Must run after env load and before
692
+ // any auth use. Local import avoids a load-time cycle through auth.
693
+ const { ensureDevSecret } = await import("./auth.js");
694
+ ensureDevSecret();
695
+
681
696
  // Refuse to boot with pre-3.12 un-prefixed env vars set.
682
697
  _checkLegacyEnvVars();
683
698
 
@@ -972,8 +987,8 @@ ${reset}
972
987
  rawRes.setHeader("Content-Length", String(accumulated));
973
988
  }
974
989
  const realCb = typeof chunk === "function" ? chunk : cb;
975
- return origEnd(undefined, undefined, realCb);
976
990
  void origWrite; // referenced to keep tsc happy
991
+ return typeof realCb === "function" ? origEnd(realCb) : origEnd();
977
992
  }) as typeof rawRes.end;
978
993
  }
979
994
 
@@ -1151,7 +1166,14 @@ ${reset}
1151
1166
  ];
1152
1167
  if (globalMiddleware.length > 0) {
1153
1168
  const [, , proceed] = await MiddlewareRunner.runBefore(globalMiddleware, req, res);
1154
- if (!proceed || res.raw.writableEnded) return;
1169
+ if (!proceed || res.raw.writableEnded) {
1170
+ // AFTER-ON-4xx RULE (M2): after_* ALWAYS run even when a before_*
1171
+ // short-circuited (4xx / clean 500 / response ended), so they can
1172
+ // still add headers / logging. Run them, then stop the handler.
1173
+ await MiddlewareRunner.runAfter(globalMiddleware, req, res);
1174
+ if (!res.raw.writableEnded) res.raw.end();
1175
+ return;
1176
+ }
1155
1177
  }
1156
1178
 
1157
1179
  // Run per-route middlewares if any
@@ -1403,7 +1425,7 @@ ${reset}
1403
1425
  (remoteAddress, p) => WsTracker.add(remoteAddress, p),
1404
1426
  (id) => { WsTracker.remove(id); },
1405
1427
  );
1406
- server.on("upgrade", (req: IncomingMessage, socket, head) => {
1428
+ server.on("upgrade", (req: IncomingMessage, socket: Socket, head: Buffer) => {
1407
1429
  const upPath = (req.url ?? "/").split("?")[0];
1408
1430
  if (upPath === "/__dev_reload") {
1409
1431
  devReloadWs.handleUpgrade(req, socket, head);