@nodefony/http 10.0.0-alpha.9 → 10.0.0-beta.1

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 (94) hide show
  1. package/README.md +7 -0
  2. package/dist/index.js +4 -2
  3. package/dist/nodefony/command/assetsPublishCommand.js +1 -1
  4. package/dist/nodefony/command/certificatesCommand.js +1 -1
  5. package/dist/nodefony/command/networkCommand.js +1 -1
  6. package/dist/nodefony/command/proxyGenerateCommand.js +1 -1
  7. package/dist/nodefony/config/config.js +22 -1
  8. package/dist/nodefony/interfaces/IRouting.js +1 -0
  9. package/dist/nodefony/interfaces/ISecurity.js +1 -0
  10. package/dist/nodefony/service/ProfilerAdminApi.js +2 -1
  11. package/dist/nodefony/service/audit-logger.js +16 -13
  12. package/dist/nodefony/service/certificates.js +25 -13
  13. package/dist/nodefony/service/error-renderer.js +11 -8
  14. package/dist/nodefony/service/http-kernel.js +340 -163
  15. package/dist/nodefony/service/pretty-request-logger.js +3 -10
  16. package/dist/nodefony/service/servers/clientError.js +2 -2
  17. package/dist/nodefony/service/servers/server-http.js +20 -20
  18. package/dist/nodefony/service/servers/server-https.js +38 -39
  19. package/dist/nodefony/service/servers/server-static.js +13 -17
  20. package/dist/nodefony/service/servers/server-websocket-secure.js +8 -7
  21. package/dist/nodefony/service/servers/server-websocket.js +8 -7
  22. package/dist/nodefony/service/servers/wsHeartbeat.js +2 -2
  23. package/dist/nodefony/service/sessions/sessions-service.js +19 -12
  24. package/dist/nodefony/service/trace.js +1 -1
  25. package/dist/nodefony/service/upload/upload-service.js +8 -7
  26. package/dist/nodefony/src/assets/prebuiltUi.js +1 -1
  27. package/dist/nodefony/src/context/Context.js +34 -19
  28. package/dist/nodefony/src/context/forwarded.js +4 -4
  29. package/dist/nodefony/src/context/http/HttpContext.js +133 -88
  30. package/dist/nodefony/src/context/http/Request.js +93 -44
  31. package/dist/nodefony/src/context/http/Response.js +107 -51
  32. package/dist/nodefony/src/context/http/parser.js +9 -10
  33. package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
  34. package/dist/nodefony/src/context/http2/Response.js +53 -25
  35. package/dist/nodefony/src/context/metaData.js +1 -1
  36. package/dist/nodefony/src/context/trustProxy.js +4 -3
  37. package/dist/nodefony/src/context/websocket/Response.js +13 -6
  38. package/dist/nodefony/src/context/websocket/WebsocketContext.js +59 -26
  39. package/dist/nodefony/src/context/websocket/wsCloseReason.js +41 -0
  40. package/dist/nodefony/src/cookies/cookie.js +3 -3
  41. package/dist/nodefony/src/errors/httpError.js +6 -6
  42. package/dist/nodefony/src/errors/thrownMessage.js +27 -0
  43. package/dist/nodefony/src/profiler/FrameProfile.js +4 -3
  44. package/dist/nodefony/src/profiler/Profiler.js +4 -4
  45. package/dist/nodefony/src/servers/kernelServers.js +29 -0
  46. package/dist/nodefony/src/servers/portBinder.js +8 -21
  47. package/dist/nodefony/src/session/session.js +18 -10
  48. package/dist/nodefony/src/session/sessionStoreFailure.js +2 -1
  49. package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +24 -9
  50. package/dist/types/index.d.ts +16 -0
  51. package/dist/types/nodefony/config/config.d.ts +22 -0
  52. package/dist/types/nodefony/interfaces/IContext.d.ts +24 -7
  53. package/dist/types/nodefony/interfaces/ICookie.d.ts +18 -18
  54. package/dist/types/nodefony/interfaces/IHttpKernel.d.ts +10 -4
  55. package/dist/types/nodefony/interfaces/IResponse.d.ts +1 -1
  56. package/dist/types/nodefony/interfaces/IRouting.d.ts +87 -0
  57. package/dist/types/nodefony/interfaces/ISecurity.d.ts +45 -0
  58. package/dist/types/nodefony/interfaces/ISession.d.ts +8 -8
  59. package/dist/types/nodefony/interfaces/IUpload.d.ts +9 -9
  60. package/dist/types/nodefony/interfaces/index.d.ts +2 -0
  61. package/dist/types/nodefony/service/audit-logger.d.ts +14 -10
  62. package/dist/types/nodefony/service/certificates.d.ts +10 -10
  63. package/dist/types/nodefony/service/http-kernel.d.ts +192 -42
  64. package/dist/types/nodefony/service/servers/server-http.d.ts +4 -1
  65. package/dist/types/nodefony/service/servers/server-https.d.ts +4 -1
  66. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +4 -1
  67. package/dist/types/nodefony/service/servers/server-websocket.d.ts +4 -1
  68. package/dist/types/nodefony/service/sessions/sessions-service.d.ts +6 -2
  69. package/dist/types/nodefony/service/upload/upload-service.d.ts +1 -1
  70. package/dist/types/nodefony/src/context/Context.d.ts +21 -10
  71. package/dist/types/nodefony/src/context/forwarded.d.ts +3 -3
  72. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +75 -18
  73. package/dist/types/nodefony/src/context/http/Request.d.ts +39 -2
  74. package/dist/types/nodefony/src/context/http/Response.d.ts +47 -7
  75. package/dist/types/nodefony/src/context/http/parser.d.ts +1 -1
  76. package/dist/types/nodefony/src/context/http2/Response.d.ts +32 -2
  77. package/dist/types/nodefony/src/context/websocket/Response.d.ts +4 -4
  78. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +11 -2
  79. package/dist/types/nodefony/src/context/websocket/wsCloseReason.d.ts +21 -0
  80. package/dist/types/nodefony/src/cookies/cookie.d.ts +9 -9
  81. package/dist/types/nodefony/src/errors/httpError.d.ts +7 -7
  82. package/dist/types/nodefony/src/errors/thrownMessage.d.ts +12 -0
  83. package/dist/types/nodefony/src/profiler/Profiler.d.ts +28 -28
  84. package/dist/types/nodefony/src/servers/kernelServers.d.ts +20 -0
  85. package/dist/types/nodefony/src/servers/portBinder.d.ts +6 -19
  86. package/dist/types/nodefony/src/session/session.d.ts +12 -12
  87. package/dist/types/nodefony/src/session/storage/RevocationGuardStorage.d.ts +1 -1
  88. package/docs/cookies.md +20 -20
  89. package/docs/observabilite.md +25 -25
  90. package/docs/rate-limit.md +18 -18
  91. package/docs/servers.md +52 -52
  92. package/docs/session.md +65 -60
  93. package/docs/upload.md +36 -30
  94. package/package.json +7 -7
@@ -4,7 +4,6 @@ import { resolveForwarded } from "../src/context/forwarded.js";
4
4
  import { MemoryRateLimitStore } from "../src/rateLimit/MemoryRateLimitStore.js";
5
5
  import { WsConnectionCounter } from "../src/rateLimit/WsConnectionCounter.js";
6
6
  import { compileDomainPatterns, compileTrustedHosts, isDomainAllowed } from "../src/context/domainMatcher.js";
7
- import Context from "../src/context/Context.js";
8
7
  import WebsocketContext from "../src/context/websocket/WebsocketContext.js";
9
8
  import HttpContext from "../src/context/http/HttpContext.js";
10
9
  import DefaultErrorRenderer from "./error-renderer.js";
@@ -12,10 +11,12 @@ import DefaultRequestLogger from "./request-logger.js";
12
11
  import JsonAuditLogger from "./audit-logger.js";
13
12
  import PrettyRequestLogger from "./pretty-request-logger.js";
14
13
  import { resolveTraceparent } from "./trace.js";
14
+ import { asServersConfig, configuredServerPort } from "../src/servers/kernelServers.js";
15
15
  import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
16
16
  import __decorate from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
17
- import { GcScheduler, Module, RequestContext, Service, injectable, logColor, nodefonyError, servedUrl, writeRuntimeState } from "nodefony";
17
+ import { GcScheduler, Module, RequestContext, Service, finallyMaybe, injectable, isPromise, logColor, nodefonyError, servedUrl, thenMaybe, writeRuntimeState } from "nodefony";
18
18
  import cluster from "node:cluster";
19
+ import http from "node:http";
19
20
  import Ws from "ws";
20
21
  //#region nodefony/service/http-kernel.ts
21
22
  const HEALTH_HEADERS = {
@@ -46,6 +47,11 @@ const perfProbe = process.env.NF_PERF_PROBE === "1" ? {
46
47
  reqAcceptNs: 0
47
48
  } : null;
48
49
  if (perfProbe) globalThis.__nfPerfProbe = perfProbe;
50
+ /**
51
+ * Règlement ignoré d'une promesse dont personne n'attend l'issue — UNE fonction
52
+ * de module, jamais une fermeture allouée par requête.
53
+ */
54
+ const ignoreSettled = () => void 0;
49
55
  const serviceName = "HttpKernel";
50
56
  const WS_DEV_LOOPBACK = /* @__PURE__ */ new Set([
51
57
  "localhost",
@@ -153,7 +159,7 @@ let HttpKernel = class HttpKernel extends Service {
153
159
  this.computeSecurityHeaderCaches();
154
160
  this._trustProxyChecker = null;
155
161
  this._wsOriginPolicy = Object.create(null);
156
- this.trustedHosts = this.options?.trustedHosts;
162
+ this.trustedHosts = this.options.trustedHosts;
157
163
  this.regAlias = this.compileAlias();
158
164
  this.configureRateLimit();
159
165
  this.configureWsConnectionLimit();
@@ -239,7 +245,7 @@ let HttpKernel = class HttpKernel extends Service {
239
245
  this.serviceCerticats = this.get("certificates");
240
246
  this.serverStatic = this.get("server-static");
241
247
  this.domain = this.kernel?.domain;
242
- this.trustedHosts = this.options?.trustedHosts;
248
+ this.trustedHosts = this.options.trustedHosts;
243
249
  this.regAlias = this.compileAlias();
244
250
  this.sessionService = this.get("sessions");
245
251
  this.profiler = this.get("profiler");
@@ -271,7 +277,7 @@ let HttpKernel = class HttpKernel extends Service {
271
277
  * @returns le {@link TrustProxyChecker} partagé.
272
278
  */
273
279
  getTrustProxyChecker() {
274
- if (this._trustProxyChecker === null) this._trustProxyChecker = buildTrustProxy(this.options?.trustProxy);
280
+ this._trustProxyChecker ??= buildTrustProxy(this.options.trustProxy);
275
281
  return this._trustProxyChecker;
276
282
  }
277
283
  /**
@@ -287,7 +293,7 @@ let HttpKernel = class HttpKernel extends Service {
287
293
  getWsOriginPolicy(cfgKey) {
288
294
  let policy = this._wsOriginPolicy[cfgKey];
289
295
  if (!policy) {
290
- const raw = this.options[cfgKey]?.allowedOrigins;
296
+ const raw = this.options[cfgKey].allowedOrigins;
291
297
  if (raw === true) policy = {
292
298
  disabled: true,
293
299
  extra: []
@@ -352,7 +358,7 @@ let HttpKernel = class HttpKernel extends Service {
352
358
  * gagne toujours sur la config (idempotent — last setter wins).
353
359
  */
354
360
  applyRequestLoggerFromConfig() {
355
- const kernelLog = this.kernel?.options?.log ?? {};
361
+ const kernelLog = this.kernel?.options.log ?? {};
356
362
  let format = kernelLog.requestFormat ?? "auto";
357
363
  if (format === "auto") {
358
364
  const env = this.kernel?.environment;
@@ -370,7 +376,17 @@ let HttpKernel = class HttpKernel extends Service {
370
376
  this.requestLogger = new JsonAuditLogger(opts);
371
377
  }
372
378
  }
373
- async handle(request, response, type) {
379
+ /**
380
+ * Ouvre le scope `request` de la requête et la confie au pipeline HTTP.
381
+ *
382
+ * Pas `async` : rien n'y est attendu — la fonction rend directement ce que
383
+ * rend le pipeline, sans l'envelopper dans une promesse (#505).
384
+ *
385
+ * @returns le contexte servi, ou sa promesse (celle de {@link handleHttp}).
386
+ * @throws de façon synchrone si le scope `request` n'est pas déclaré — faute
387
+ * de configuration ; l'appelant ({@link onHttpRequest}) l'absorbe.
388
+ */
389
+ handle(request, response, type) {
374
390
  let scope;
375
391
  if (perfProbe) {
376
392
  const t0 = process.hrtime.bigint();
@@ -378,7 +394,7 @@ let HttpKernel = class HttpKernel extends Service {
378
394
  perfProbe.enterScopeNs += Number(process.hrtime.bigint() - t0);
379
395
  perfProbe.count++;
380
396
  } else scope = this.container?.enterScope("request");
381
- if (this.kernel?.debug) this.log(logColor.cyanBgBlue(`${request.url}`), "DEBUG", `${type}`);
397
+ if (this.kernel?.debug) this.log(logColor.cyanBgBlue(`${request.url}`), "DEBUG", type);
382
398
  return this.handleHttp(scope, request, response, type);
383
399
  }
384
400
  /**
@@ -392,13 +408,16 @@ let HttpKernel = class HttpKernel extends Service {
392
408
  * elle, exécute du code utilisateur (`initialize()`) et résout des dépendances
393
409
  * DI : elle n'a rien à faire avant que la requête soit autorisée.
394
410
  *
411
+ * Synchrone : le match est un calcul pur, rien n'y est attendu (#505).
412
+ *
395
413
  * @param checkFirewall - calcule `context.secure` (zone protégée) ; laissé à
396
414
  * `false` par un appelant qui l'a déjà tranché.
415
+ * @returns le résolveur de la route, posé sur `context.resolver`.
397
416
  * @throws HttpError 404 quand aucune route ne matche, ou l'exception portée
398
- * par le résolveur (405…) — inchangé, et toujours avant le firewall : une
399
- * route inexistante ne devient pas un 401.
417
+ * par le résolveur (405…) — de façon SYNCHRONE, et toujours avant le
418
+ * firewall : une route inexistante ne devient pas un 401.
400
419
  */
401
- async prepareFrontController(context, checkFirewall = true) {
420
+ prepareFrontController(context, checkFirewall = true) {
402
421
  if (!this.router) throw new Error("kernel HTTP not ready");
403
422
  if (this.firewall && checkFirewall) context.secure = this.firewall.isSecure(context);
404
423
  let resolver;
@@ -429,7 +448,7 @@ let HttpKernel = class HttpKernel extends Service {
429
448
  * `Resolver.executeAction` créer l'instance une fois la requête autorisée.
430
449
  */
431
450
  async handleFrontController(context, checkFirewall = true) {
432
- return await (await this.prepareFrontController(context, checkFirewall)).newController(context);
451
+ return this.prepareFrontController(context, checkFirewall).newController(context);
433
452
  }
434
453
  /**
435
454
  * Handles errors by setting appropriate response codes and messages.
@@ -484,49 +503,77 @@ let HttpKernel = class HttpKernel extends Service {
484
503
  }
485
504
  async onError(error, context, _extraHeaders) {
486
505
  if (context) context.error = error;
487
- switch (true) {
488
- case context instanceof HttpContext: {
489
- const result = this.errorRenderer.renderHttp(error, context);
490
- if (error instanceof HttpError || error instanceof nodefonyError) error.code = result.status;
491
- context.response.setStatusCode(result.status, result.message);
492
- if (result.headers) context.response.setHeaders(result.headers);
493
- if (this.kernel?.debug) this.log(error.toString(), "ERROR");
494
- if (context.finished || context.sended) {
495
- this.log(error.toString(), "DEBUG", "onError on closed context");
496
- return context;
497
- }
498
- if (!context.response.isHeaderSent()) {
499
- try {
500
- await context.render(result.body);
501
- } catch (e) {
502
- this.log(e, "CRITIC");
503
- throw e;
504
- }
505
- return context;
506
- }
507
- await context.close();
506
+ if (context instanceof HttpContext) {
507
+ const result = this.errorRenderer.renderHttp(error, context);
508
+ if (error instanceof HttpError || error instanceof nodefonyError) error.code = result.status;
509
+ context.response.setStatusCode(result.status, http.STATUS_CODES[result.status] ?? "Unknown Error");
510
+ if (result.headers) context.response.setHeaders(result.headers);
511
+ if (this.kernel?.debug) this.log(error.toString(), "ERROR");
512
+ if (context.finished || context.sended) {
513
+ this.log(error.toString(), "DEBUG", "onError on closed context");
508
514
  return context;
509
515
  }
510
- case context instanceof WebsocketContext: try {
511
- const wsResult = this.errorRenderer.renderWebsocket(error, context);
512
- if (context.response && context.response.connection) {
513
- context.close(wsResult.code, wsResult.reason);
514
- return context;
515
- }
516
- if (context.request && !context.rejected) {
517
- context.reject(wsResult.code, wsResult.reason);
518
- return context;
516
+ if (!context.response.isHeaderSent()) {
517
+ try {
518
+ await context.render(result.body);
519
+ } catch (e) {
520
+ this.log(e, "CRITIC");
521
+ throw e;
519
522
  }
520
- } catch (e) {
521
- throw error;
523
+ return context;
524
+ }
525
+ await context.close();
526
+ return context;
527
+ }
528
+ if (context instanceof WebsocketContext) try {
529
+ const wsResult = this.errorRenderer.renderWebsocket(error, context);
530
+ if (context.response?.connection) {
531
+ context.close(wsResult.code, wsResult.reason);
532
+ return context;
522
533
  }
523
- default: throw error;
534
+ if (context.request && !context.rejected) {
535
+ context.reject(wsResult.code, wsResult.reason);
536
+ return context;
537
+ }
538
+ } catch {
539
+ throw error;
524
540
  }
541
+ throw error;
525
542
  }
526
543
  compileAlias() {
527
544
  return compileTrustedHosts(this.domain, this.trustedHosts, this.kernel?.environment === "development");
528
545
  }
529
- async onHttpRequest(request, response, type) {
546
+ /**
547
+ * Point d'entrée d'une requête HTTP, appelé par les serveurs depuis leur
548
+ * écouteur `request`.
549
+ *
550
+ * Ne lève et ne rejette JAMAIS : dans un écouteur d'événement, une exception
551
+ * deviendrait une erreur non capturée du processus. Ce qui échappe au
552
+ * pipeline a déjà été traité — rendu au client par `onError`, qui journalise
553
+ * en CRITIC l'échec de son propre rendu avant de le relever. C'était le
554
+ * `.catch` vide des serveurs ; il vit désormais ici, une seule fois.
555
+ *
556
+ * @returns `undefined` quand la requête est servie sans pipeline (sonde de
557
+ * santé, 429) ; sinon la promesse de la fin du pipeline — elle ne rejette
558
+ * pas, et personne n'est tenu de l'attendre.
559
+ */
560
+ onHttpRequest(request, response, type) {
561
+ let pipeline;
562
+ try {
563
+ pipeline = this.dispatchHttpRequest(request, response, type);
564
+ } catch {
565
+ return;
566
+ }
567
+ return isPromise(pipeline) ? Promise.resolve(pipeline).then(ignoreSettled, ignoreSettled) : void 0;
568
+ }
569
+ /**
570
+ * Le travail d'{@link onHttpRequest} : en-têtes de transport, sondes de
571
+ * santé, rate-limit, hook `onServerRequest`, puis le pipeline.
572
+ *
573
+ * @returns `undefined` quand la requête est servie ici ; sinon la promesse du
574
+ * pipeline.
575
+ */
576
+ dispatchHttpRequest(request, response, type) {
530
577
  const headerServer = this.options.headerServer;
531
578
  if (headerServer != null && headerServer !== "") response.setHeader("Server", headerServer);
532
579
  if (this.secContentTypeOptions !== null) response.setHeader("X-Content-Type-Options", this.secContentTypeOptions);
@@ -534,11 +581,17 @@ let HttpKernel = class HttpKernel extends Service {
534
581
  if (this.secHsts !== null && (type === "https" || type === "http2")) response.setHeader("Strict-Transport-Security", this.secHsts);
535
582
  if (this.#healthPaths !== null) {
536
583
  const url = request.url;
537
- if (url === this.#healthPaths.liveness) return this.#respondHealth(response, true);
538
- if (url === this.#healthPaths.readiness) return this.#respondHealth(response, !this.#terminating && this.kernel?.servable === true);
584
+ if (url === this.#healthPaths.liveness) {
585
+ this.#respondHealth(response, true);
586
+ return;
587
+ }
588
+ if (url === this.#healthPaths.readiness) {
589
+ this.#respondHealth(response, !this.#terminating && this.kernel?.servable === true);
590
+ return;
591
+ }
539
592
  }
540
593
  if (this.rateLimiter !== null) {
541
- const ip = resolveForwarded(request.headers, request.socket?.remoteAddress, this.getTrustProxyChecker()).clientIp;
594
+ const ip = resolveForwarded(request.headers, request.socket.remoteAddress, this.getTrustProxyChecker()).clientIp;
542
595
  if (ip !== null) {
543
596
  const verdict = this.rateLimiter.hit(ip);
544
597
  response.setHeader("X-RateLimit-Limit", verdict.limit);
@@ -553,12 +606,8 @@ let HttpKernel = class HttpKernel extends Service {
553
606
  }
554
607
  }
555
608
  }
556
- if (this.listenerCount("onServerRequest")) await this.fireAsync("onServerRequest", request, response, type).catch((e) => {
557
- throw e;
558
- });
559
- return this.handle(request, response, type).catch((e) => {
560
- throw e;
561
- });
609
+ if (this.listenerCount("onServerRequest")) return this.fireAsync("onServerRequest", request, response, type).then(() => this.handle(request, response, type));
610
+ return this.handle(request, response, type);
562
611
  }
563
612
  async initServers() {
564
613
  const servers = [];
@@ -623,18 +672,18 @@ let HttpKernel = class HttpKernel extends Service {
623
672
  if (serverHttp?.active && serverHttp.port > 0) {
624
673
  ports.push(serverHttp.port);
625
674
  urls.push(servedUrl("http", serverHttp.domain, serverHttp.port));
626
- const d = this.kernel?.options.servers?.http;
627
- if (d && d.port) desired.push(d.port);
675
+ const port = configuredServerPort(asServersConfig(this.kernel?.options.servers), "http");
676
+ if (port) desired.push(port);
628
677
  }
629
678
  if (serverHttps?.active && serverHttps.port > 0) {
630
679
  ports.push(serverHttps.port);
631
680
  urls.push(servedUrl("https", serverHttps.domain, serverHttps.port));
632
- const d = this.kernel?.options.servers?.https;
633
- if (d && d.port) desired.push(d.port);
681
+ const port = configuredServerPort(asServersConfig(this.kernel?.options.servers), "https");
682
+ if (port) desired.push(port);
634
683
  }
635
684
  if (ports.length === 0) return;
636
685
  writeRuntimeState(process.cwd(), {
637
- pid: cluster.isWorker ? process.ppid ?? process.pid : process.pid,
686
+ pid: cluster.isWorker ? process.ppid : process.pid,
638
687
  ports,
639
688
  desiredPorts: desired.length > 0 ? desired : void 0,
640
689
  urls
@@ -648,16 +697,22 @@ let HttpKernel = class HttpKernel extends Service {
648
697
  * session (lazy). Remplace l'ancien `sessionAutoStart` global « démarre sur
649
698
  * toutes les routes » (le moteur du ×23).
650
699
  *
700
+ * Le contrat de retour suit ce qui est réellement attendu : `null` tout de
701
+ * suite quand aucune session n'est à ouvrir (le cas nominal — ni intent, ni
702
+ * cookie), la promesse de la session seulement quand le store est interrogé
703
+ * (#505).
704
+ *
651
705
  * @param context - contexte HTTP/HTTP2/WS courant.
652
- * @returns la session active, ou `null` si la requête n'en requiert aucune.
706
+ * @returns `null` si la requête n'en requiert aucune ; sinon la promesse de
707
+ * la session active.
653
708
  */
654
- async startSession(context) {
709
+ startSession(context) {
655
710
  if (!this.sessionService) return null;
656
711
  const intent = context.sessionIntent;
657
712
  if (context.security?.stateless === true) {
658
713
  if (intent) {
659
714
  const name = context.security.name;
660
- if (this.#statelessIntentVues === null) this.#statelessIntentVues = /* @__PURE__ */ new Set();
715
+ this.#statelessIntentVues ??= /* @__PURE__ */ new Set();
661
716
  if (!this.#statelessIntentVues.has(name)) {
662
717
  this.#statelessIntentVues.add(name);
663
718
  this.log(`Zone "${name}" is stateless: the session requested by this route is IGNORED (no session opened, no cookie sent). Drop \`stateless: true\` on the area, or stop requiring a session on its routes.`, "WARNING", "SESSION");
@@ -666,36 +721,71 @@ let HttpKernel = class HttpKernel extends Service {
666
721
  return null;
667
722
  }
668
723
  if (!intent && !context.hasSession()) return null;
669
- return await this.sessionService.start(context, intent?.readOnly);
724
+ return this.sessionService.start(context, intent?.readOnly);
670
725
  }
671
726
  /**
672
- * Teardown post-réponse — fire-and-forget depuis le `once("close")` posé par
673
- * `createHttpContext` (un seul fire possible : once auto-détaché). Loggue la
674
- * requête, draine les hooks afterResponse/onFinish, libère le scope DI.
727
+ * Teardown post-réponse — fire-and-forget depuis l'écouteur `close` posé par
728
+ * `createHttpContext`. Loggue la requête, draine les hooks
729
+ * afterResponse/onFinish, libère le scope DI.
730
+ *
731
+ * Synchrone dans le cas nominal — aucun hook `onAfterResponse`, aucun écouteur
732
+ * `onFinish` : rien n'y est attendu (#505). Ne lève et ne rejette jamais —
733
+ * l'appelant est un écouteur d'événement : une faute est journalisée, et le
734
+ * scope TOUJOURS libéré.
735
+ *
736
+ * @returns `undefined` quand le démontage est terminé ; sinon la promesse de
737
+ * sa fin, qui ne rejette pas.
675
738
  */
676
- async teardownHttp(context, scope) {
677
- if (context.finished) return;
739
+ teardownHttp(context, scope) {
740
+ if (context.finished) return void 0;
678
741
  try {
679
742
  if (context.waitAsync && !context.sended && this.kernel?.environment === "development") this.log(`Action "${context.resolver?.route?.name ?? context.url}" returned a non-renderable value (void/null/class instance) and never sent a response. Use 'return <object|string|number|boolean|Buffer>' (auto-JSON) or send/stream manually.`, "WARNING");
680
743
  context.logRequest();
681
744
  context.logPhasesVerbose();
682
745
  this.profiler?.collect(context);
683
- await context._runAfterResponse();
746
+ const afterResponse = context._runAfterResponse();
747
+ if (afterResponse !== void 0 || context.listenerCount("onFinish")) return this.finishTeardownHttp(context, scope, afterResponse);
748
+ this.releaseHttpContext(context, scope);
749
+ } catch (e) {
750
+ this.failTeardownHttp(e, context, scope);
751
+ }
752
+ }
753
+ /**
754
+ * Suite ASYNCHRONE du démontage, quand il y a des hooks à attendre :
755
+ * `onAfterResponse` puis `onFinish`, avant de libérer le contexte. Ne rejette
756
+ * jamais (cf {@link teardownHttp}).
757
+ */
758
+ async finishTeardownHttp(context, scope, afterResponse) {
759
+ try {
760
+ if (afterResponse !== void 0) await afterResponse;
684
761
  if (context.listenerCount("onFinish")) await context.fireAsync("onFinish", context);
762
+ this.releaseHttpContext(context, scope);
763
+ } catch (e) {
764
+ this.failTeardownHttp(e, context, scope);
765
+ }
766
+ }
767
+ /** Fin d'un contexte HTTP : marqué fini, scope DI libéré, contexte nettoyé. */
768
+ releaseHttpContext(context, scope) {
769
+ context.finished = true;
770
+ if (perfProbe) {
771
+ const t0 = process.hrtime.bigint();
772
+ this.container?.leaveScope(scope);
773
+ perfProbe.leaveScopeNs += Number(process.hrtime.bigint() - t0);
774
+ } else this.container?.leaveScope(scope);
775
+ context.clean();
776
+ }
777
+ /**
778
+ * R2 — le démontage est fire-and-forget : un throw (hook onFinish /
779
+ * afterResponse) serait une erreur non capturée du processus. On journalise,
780
+ * et on GARANTIT la libération du scope DI (sinon le scope `request` fuit à
781
+ * chaque hook qui lève).
782
+ */
783
+ failTeardownHttp(e, context, scope) {
784
+ this.log(e, "ERROR", "TEARDOWN");
785
+ if (!context.finished) {
685
786
  context.finished = true;
686
- if (perfProbe) {
687
- const t0 = process.hrtime.bigint();
688
- this.container?.leaveScope(scope);
689
- perfProbe.leaveScopeNs += Number(process.hrtime.bigint() - t0);
690
- } else this.container?.leaveScope(scope);
787
+ this.container?.leaveScope(scope);
691
788
  context.clean();
692
- } catch (e) {
693
- this.log(e, "ERROR", "TEARDOWN");
694
- if (!context.finished) {
695
- context.finished = true;
696
- this.container?.leaveScope(scope);
697
- context.clean();
698
- }
699
789
  }
700
790
  }
701
791
  createHttpContext(scope, request, response, type) {
@@ -708,7 +798,7 @@ let HttpKernel = class HttpKernel extends Service {
708
798
  perfProbe.ctxNs += Number(process.hrtime.bigint() - t0);
709
799
  perfProbe.t0 = 0n;
710
800
  } else context = new HttpContext(scope, request, response, type);
711
- response.once("close", () => {
801
+ response.on("close", () => {
712
802
  if (!response.writableEnded) {
713
803
  context._abortIfPending("Connection closed before response finished");
714
804
  if (!context.sended) context.response.statusCode = 499;
@@ -721,87 +811,176 @@ let HttpKernel = class HttpKernel extends Service {
721
811
  throw e;
722
812
  }
723
813
  }
724
- async handleHttp(scope, request, response, type) {
814
+ /**
815
+ * Crée le contexte HTTP de la requête et la sert dans sa bulle ALS ; toute
816
+ * erreur — synchrone ou rejet — passe par {@link onError}.
817
+ *
818
+ * Pas `async` : un GET servi par une action synchrone traverse le pipeline
819
+ * sans une seule promesse (#505). Une promesse seulement quand une étape
820
+ * attend, ou quand l'erreur est rendue (`onError`).
821
+ *
822
+ * @returns le contexte servi, ou sa promesse.
823
+ */
824
+ handleHttp(scope, request, response, type) {
725
825
  let context = null;
726
826
  try {
727
827
  context = this.createHttpContext(scope, request, response, type);
728
- if (this.listenerCount("onCreateContext")) await this.fireAsync("onCreateContext", context);
729
- context.traceparent = resolveTraceparent(request.headers?.traceparent);
730
- const profilerQueries = this.profiler ? [] : null;
731
- context.profilerQueries = profilerQueries;
732
- context.profiling = profilerQueries !== null;
733
- return await RequestContext.run({
734
- requestId: context.requestId,
735
- scheme: context.scheme,
736
- traceparent: context.traceparent,
737
- queries: profilerQueries ?? void 0,
738
- context
739
- }, async () => {
740
- if (this.firewall?.handleCors(context) === 204) {
741
- context.response.writeHead(204);
742
- context.response.end();
743
- return context;
744
- }
745
- context.phaseStart("resolve");
746
- context.resolver = this.router ? this.router.resolve(context) : null;
747
- context.phaseEnd("resolve");
748
- this.firewall?.applySecurityHeaders(context);
749
- if (this.serverStatic && context.resolver?.resolve !== true && !context.resolver?.exception && (this.kernel?.options.servers.statics || this.kernel?.options.statics || this.serverStatic.hasMounts())) {
750
- response.removeHeader("Content-Type");
751
- await this.serverStatic.handle(request, response).catch(() => void 0);
752
- }
753
- const streamBody = context.resolver?.resolve === true && context.resolver.route?.bodyStream === true;
754
- context.phaseStart("parse");
755
- try {
756
- if (!streamBody) await context.request.initialize();
757
- } finally {
758
- context.phaseEnd("parse");
759
- }
760
- const ctx = await this.onRequestEnd(context);
761
- if (ctx instanceof Context) {
762
- ctx.phaseStart("action");
763
- try {
764
- return await ctx.handle();
765
- } finally {
766
- ctx.phaseEnd("action");
767
- }
768
- }
769
- return context;
770
- });
828
+ const created = context;
829
+ const served = this.listenerCount("onCreateContext") ? this.fireAsync("onCreateContext", created).then(() => this.serveHttpContext(created, scope, request, response)) : this.serveHttpContext(created, scope, request, response);
830
+ if (!isPromise(served)) return served;
831
+ return served.catch((e) => this.failHttp(e, created, scope));
771
832
  } catch (e) {
772
- return await this.onError(e, context);
833
+ return this.failHttp(e, context, scope);
773
834
  }
774
835
  }
775
- async onRequestEnd(context, error) {
776
- if (!context) throw new nodefonyError("Bad context", 500);
836
+ /**
837
+ * Traceparent, tampon du profileur, puis le pipeline dans la bulle ALS de la
838
+ * requête (`RequestContext.run`).
839
+ */
840
+ serveHttpContext(context, scope, request, response) {
841
+ context.traceparent = resolveTraceparent(request.headers.traceparent);
842
+ const profilerQueries = this.profiler ? [] : null;
843
+ context.profilerQueries = profilerQueries;
844
+ context.profiling = profilerQueries !== null;
845
+ return RequestContext.run({
846
+ requestId: context.requestId,
847
+ scheme: context.scheme,
848
+ traceparent: context.traceparent,
849
+ queries: profilerQueries ?? void 0,
850
+ context,
851
+ scope
852
+ }, () => this.runHttpPipeline(context, request, response));
853
+ }
854
+ /**
855
+ * Chemin d'erreur de {@link handleHttp} : libère le scope d'un contexte qui
856
+ * n'a pas pu se construire, puis rend l'erreur au client (`onError`).
857
+ */
858
+ failHttp(e, context, scope) {
859
+ if (context === null) this.container?.leaveScope(scope);
860
+ return this.onError(e, context);
861
+ }
862
+ /**
863
+ * Le trajet d'une requête HTTP DANS la bulle ALS : CORS, calque de
864
+ * configuration, route, en-têtes de sécurité, repli statique, corps, gardes,
865
+ * action.
866
+ *
867
+ * Chaque étape rend sa valeur, ou une promesse quand elle attend réellement
868
+ * (préflight à terminer, hook asynchrone, fichier statique, corps à lire,
869
+ * session, pare-feu, action asynchrone) — et la suite ne s'enchaîne par
870
+ * `then` que dans ce cas. Un `await` sur une valeur déjà connue suspendait le
871
+ * pipeline pour rien : ~50 évènements asynchrones par GET (#505).
872
+ *
873
+ * @returns le contexte servi, ou la promesse de ce contexte.
874
+ * @throws l'erreur d'une étape synchrone — {@link handleHttp} la traite comme
875
+ * un rejet (même `catch`, même `onError`).
876
+ */
877
+ runHttpPipeline(httpContext, request, response) {
878
+ if (this.firewall?.handleCors(httpContext) === 204) {
879
+ httpContext.response.writeHead(204);
880
+ return thenMaybe(httpContext.response.end(), () => httpContext);
881
+ }
882
+ if (this.kernel && this.kernel.listenerCount("onRequestScope") > 0) return this.kernel.fireAsync("onRequestScope", httpContext).then(() => this.routeHttpRequest(httpContext, request, response));
883
+ return this.routeHttpRequest(httpContext, request, response);
884
+ }
885
+ /**
886
+ * Route, en-têtes de sécurité applicatifs, repli statique — puis le corps.
887
+ * N'attend que le repli statique, et seulement quand aucune route ne matche.
888
+ */
889
+ routeHttpRequest(httpContext, request, response) {
890
+ httpContext.phaseStart("resolve");
891
+ httpContext.resolver = this.router ? this.router.resolve(httpContext) : null;
892
+ httpContext.phaseEnd("resolve");
893
+ this.firewall?.applySecurityHeaders(httpContext);
894
+ if (this.serverStatic && httpContext.resolver?.resolve !== true && !httpContext.resolver?.exception && (asServersConfig(this.kernel?.options.servers)?.statics || this.kernel?.options.statics || this.serverStatic.hasMounts())) {
895
+ response.removeHeader("Content-Type");
896
+ return this.serverStatic.handle(request, response).catch(() => void 0).then(() => this.serveHttpRequest(httpContext));
897
+ }
898
+ return this.serveHttpRequest(httpContext);
899
+ }
900
+ /**
901
+ * Le corps — lu seulement s'il y en a un à lire —, puis les gardes
902
+ * ({@link onRequestEnd}) et l'action. Chacun n'est attendu que s'il attend.
903
+ */
904
+ serveHttpRequest(httpContext) {
905
+ const streamBody = httpContext.resolver?.resolve === true && httpContext.resolver.route?.bodyStream === true;
906
+ httpContext.phaseStart("parse");
907
+ const parsed = finallyMaybe(() => streamBody ? void 0 : httpContext.request.initialize(), () => httpContext.phaseEnd("parse"));
908
+ return thenMaybe(parsed, () => thenMaybe(this.onRequestEnd(httpContext), (guarded) => {
909
+ guarded.phaseStart("action");
910
+ return finallyMaybe(() => guarded.handle(), () => guarded.phaseEnd("action"));
911
+ }));
912
+ }
913
+ /**
914
+ * Les gardes d'une requête HTTP, corps lu : en-têtes de scheme, domaine, hook
915
+ * `beforeResolve`, armement de la route, CSRF, session, pare-feu — dans cet
916
+ * ordre.
917
+ *
918
+ * Synchrone tant qu'aucune garde n'attend (#505) : le hook `beforeResolve`
919
+ * n'est attendu que s'il a un écouteur, la session que si la requête en
920
+ * ouvre une, le pare-feu que sur une zone protégée ou à accès contrôlé.
921
+ *
922
+ * @param context - le contexte de la requête.
923
+ * @param error - une erreur survenue en amont : relevée telle quelle.
924
+ * @returns le contexte une fois les gardes passées, ou la promesse de ce
925
+ * contexte quand une garde attend.
926
+ * @throws de façon SYNCHRONE l'erreur reçue, l'exception du résolveur (404,
927
+ * 405…) et le refus CSRF ; un refus du pare-feu ou de session rompt la
928
+ * promesse rendue.
929
+ */
930
+ onRequestEnd(context, error) {
777
931
  if (error) throw error;
778
- if (this.options[context.scheme].headers) context.response.setHeaders(this.options[context.scheme].headers);
932
+ const schemeHeaders = context.scheme === "https" ? this.options.https.headers : context.scheme === "http" ? this.options.http.headers : null;
933
+ if (schemeHeaders) context.response.setHeaders(schemeHeaders);
779
934
  if (this.kernel?.options.domainCheck) this.checkValidDomain(context);
780
- if (this.listenerCount("beforeResolve")) await this.fireAsync("beforeResolve", context);
781
- await this.prepareFrontController(context);
935
+ if (this.listenerCount("beforeResolve")) return this.fireAsync("beforeResolve", context).then(() => this.armAndGuard(context));
936
+ return this.armAndGuard(context);
937
+ }
938
+ /**
939
+ * Suite d'{@link onRequestEnd} : armement de la route, CSRF, session, puis
940
+ * pare-feu. Synchrone tant que ni la session ni le pare-feu n'attendent.
941
+ */
942
+ armAndGuard(context) {
943
+ this.prepareFrontController(context);
782
944
  this.firewall?.enforceCsrf(context);
783
- await this.startSession(context);
784
- if (context.secure || context.isControlledAccess) {
785
- context.phaseStart("firewall");
945
+ return thenMaybe(this.startSession(context), () => context.secure || context.isControlledAccess ? this.runFirewall(context) : context);
946
+ }
947
+ /**
948
+ * Le pare-feu d'une requête en zone protégée : `handleSecurity`, puis les
949
+ * hooks `afterAuth` (succès) ou `onAuthFailure` (refus, relevé ensuite).
950
+ *
951
+ * @returns la promesse du contexte autorisé.
952
+ * @throws (rejet) le refus du pare-feu, après `onAuthFailure`.
953
+ */
954
+ async runFirewall(context) {
955
+ context.phaseStart("firewall");
956
+ try {
786
957
  try {
787
- try {
788
- await this.firewall?.handleSecurity(context);
789
- if (this.listenerCount("afterAuth")) await this.fireAsync("afterAuth", context);
790
- } catch (authError) {
791
- await this.fireAsync("onAuthFailure", context, authError).catch((e) => this.log(e, "ERROR", "onAuthFailure"));
792
- throw authError;
793
- }
794
- } finally {
795
- context.phaseEnd("firewall");
958
+ await this.firewall?.handleSecurity(context);
959
+ if (this.listenerCount("afterAuth")) await this.fireAsync("afterAuth", context);
960
+ } catch (authError) {
961
+ await this.fireAsync("onAuthFailure", context, authError).catch((e) => this.log(e, "ERROR", "onAuthFailure"));
962
+ throw authError;
796
963
  }
797
- return context;
964
+ } finally {
965
+ context.phaseEnd("firewall");
798
966
  }
799
967
  return context;
800
968
  }
801
969
  createWebsocketContext(scope, req, ws, type) {
802
970
  const context = new WebsocketContext(scope, req, ws, type);
803
- context.once("onFinish", async (wscontext) => {
804
- if (!context) return;
971
+ context.once("onFinish", () => {
972
+ this.finishWebsocket(context, scope);
973
+ });
974
+ return context;
975
+ }
976
+ /**
977
+ * Teardown d'une connexion WS à sa fermeture : signal d'abandon, hooks
978
+ * post-réponse, persistance de la session, puis libération du scope.
979
+ * Ne rejette jamais : toute erreur est journalisée (l'appelant est un
980
+ * écouteur d'événement synchrone, qui n'attend rien).
981
+ */
982
+ async finishWebsocket(context, scope) {
983
+ try {
805
984
  if (context.finished) return;
806
985
  context._abortIfPending("WebSocket closed");
807
986
  await context._runAfterResponse();
@@ -810,18 +989,19 @@ let HttpKernel = class HttpKernel extends Service {
810
989
  } catch (e) {
811
990
  this.log(e, "ERROR", "WS onFinish saveSession");
812
991
  }
813
- this.container?.leaveScope(wscontext.container);
992
+ this.container?.leaveScope(scope);
814
993
  context.clean();
815
994
  context.finished = true;
816
- });
817
- return context;
995
+ } catch (e) {
996
+ this.log(e, "ERROR", "WS onFinish");
997
+ }
818
998
  }
819
999
  async onWebsocketRequest(ws, req, type) {
820
1000
  const wsConnCounter = this.#wsConnCounter;
821
1001
  if (this.rateLimiter !== null || wsConnCounter !== null) {
822
- const ip = resolveForwarded(req.headers, req.socket?.remoteAddress, this.getTrustProxyChecker()).clientIp;
1002
+ const ip = resolveForwarded(req.headers, req.socket.remoteAddress, this.getTrustProxyChecker()).clientIp;
823
1003
  if (ip !== null) {
824
- if (this.rateLimiter !== null && this.rateLimiter.hit(ip).limited) {
1004
+ if (this.rateLimiter?.hit(ip).limited) {
825
1005
  if (ws.readyState === Ws.OPEN) ws.close(1013, "rate limit");
826
1006
  return;
827
1007
  }
@@ -834,13 +1014,9 @@ let HttpKernel = class HttpKernel extends Service {
834
1014
  }
835
1015
  }
836
1016
  }
837
- await this.fireAsync("onServerRequest", req, null, type).catch((e) => {
838
- throw e;
839
- });
1017
+ await this.fireAsync("onServerRequest", req, null, type);
840
1018
  const scope = this.container?.enterScope("request");
841
- return this.handleWebsocket(scope, ws, req, type).catch((e) => {
842
- throw e;
843
- });
1019
+ return this.handleWebsocket(scope, ws, req, type);
844
1020
  }
845
1021
  async handleWebsocket(scope, ws, req, type) {
846
1022
  let context = null;
@@ -852,7 +1028,7 @@ let HttpKernel = class HttpKernel extends Service {
852
1028
  }
853
1029
  try {
854
1030
  if (context) {
855
- context.traceparent = resolveTraceparent(req.headers?.traceparent);
1031
+ context.traceparent = resolveTraceparent(req.headers.traceparent);
856
1032
  context.profiling = this.profiler !== null;
857
1033
  }
858
1034
  const wsRunId = context?.requestId ?? "ws-no-ctx";
@@ -862,7 +1038,8 @@ let HttpKernel = class HttpKernel extends Service {
862
1038
  requestId: wsRunId,
863
1039
  scheme: wsScheme,
864
1040
  ...wsTrace ? { traceparent: wsTrace } : {},
865
- context
1041
+ context,
1042
+ scope
866
1043
  }, async () => {
867
1044
  await this.onConnect(context, error);
868
1045
  if (this.firewall && (context?.secure || context?.isControlledAccess)) {
@@ -922,7 +1099,7 @@ let HttpKernel = class HttpKernel extends Service {
922
1099
  throw e;
923
1100
  }
924
1101
  if (!context.sessionStarting) await this.startSession(context);
925
- return await context.connect();
1102
+ return context.connect();
926
1103
  }
927
1104
  checkValidDomain(context) {
928
1105
  if (context.validDomain) return 200;