@nodefony/http 10.0.0-beta.3 → 10.0.0-beta.4

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 (124) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  2. package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
  3. package/dist/index.js +12 -10
  4. package/dist/nodefony/command/assetsPublishCommand.js +5 -3
  5. package/dist/nodefony/command/certificatesCommand.js +5 -3
  6. package/dist/nodefony/command/networkCommand.js +5 -3
  7. package/dist/nodefony/command/proxyGenerateCommand.js +8 -3
  8. package/dist/nodefony/config/config.js +1 -2
  9. package/dist/nodefony/config/defineModuleConfig.js +1 -2
  10. package/dist/nodefony/service/HttpAdminApi.js +2 -2
  11. package/dist/nodefony/service/ProfilerAdminApi.js +1 -2
  12. package/dist/nodefony/service/audit-logger.js +20 -13
  13. package/dist/nodefony/service/certificates.js +97 -13
  14. package/dist/nodefony/service/error-renderer.js +29 -12
  15. package/dist/nodefony/service/http-kernel.js +118 -43
  16. package/dist/nodefony/service/pretty-request-logger.js +5 -4
  17. package/dist/nodefony/service/request-logger.js +24 -16
  18. package/dist/nodefony/service/reverse-proxy.js +3 -3
  19. package/dist/nodefony/service/servers/clientError.js +22 -3
  20. package/dist/nodefony/service/servers/server-h2c.js +126 -0
  21. package/dist/nodefony/service/servers/server-http.js +5 -8
  22. package/dist/nodefony/service/servers/server-https.js +12 -24
  23. package/dist/nodefony/service/servers/server-static.js +3 -3
  24. package/dist/nodefony/service/servers/server-websocket-secure.js +10 -18
  25. package/dist/nodefony/service/servers/server-websocket.js +10 -18
  26. package/dist/nodefony/service/servers/serverShutdown.js +78 -18
  27. package/dist/nodefony/service/servers/wsHeartbeat.js +1 -2
  28. package/dist/nodefony/service/sessions/sessions-service.js +7 -14
  29. package/dist/nodefony/service/trace.js +1 -2
  30. package/dist/nodefony/service/upload/upload-service.js +5 -8
  31. package/dist/nodefony/service/x509.js +1 -2
  32. package/dist/nodefony/src/assets/collectAssets.js +1 -2
  33. package/dist/nodefony/src/assets/prebuiltUi.js +1 -2
  34. package/dist/nodefony/src/context/Context.js +23 -31
  35. package/dist/nodefony/src/context/domainMatcher.js +1 -2
  36. package/dist/nodefony/src/context/forwarded.js +1 -2
  37. package/dist/nodefony/src/context/http/HttpContext.js +7 -6
  38. package/dist/nodefony/src/context/http/Request.js +33 -29
  39. package/dist/nodefony/src/context/http/Response.js +23 -6
  40. package/dist/nodefony/src/context/http/SseStream.js +1 -2
  41. package/dist/nodefony/src/context/http/parser.js +36 -10
  42. package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
  43. package/dist/nodefony/src/context/http2/Request.js +4 -8
  44. package/dist/nodefony/src/context/http2/Response.js +22 -9
  45. package/dist/nodefony/src/context/metaData.js +1 -2
  46. package/dist/nodefony/src/context/requestId.js +1 -2
  47. package/dist/nodefony/src/context/responseEnded.js +1 -2
  48. package/dist/nodefony/src/context/transport.js +45 -0
  49. package/dist/nodefony/src/context/trustProxy.js +1 -2
  50. package/dist/nodefony/src/context/websocket/Response.js +33 -6
  51. package/dist/nodefony/src/context/websocket/WebsocketContext.js +92 -17
  52. package/dist/nodefony/src/context/websocket/isWebsocketContext.js +17 -0
  53. package/dist/nodefony/src/context/websocket/websocketUrl.js +1 -2
  54. package/dist/nodefony/src/context/websocket/wsBackpressure.js +1 -2
  55. package/dist/nodefony/src/context/websocket/wsCloseReason.js +1 -2
  56. package/dist/nodefony/src/context/websocket/wsLogContent.js +1 -2
  57. package/dist/nodefony/src/cookies/cookie.js +3 -7
  58. package/dist/nodefony/src/errors/httpError.js +1 -2
  59. package/dist/nodefony/src/errors/thrownMessage.js +1 -2
  60. package/dist/nodefony/src/profiler/FrameProfile.js +1 -2
  61. package/dist/nodefony/src/profiler/Profiler.js +102 -20
  62. package/dist/nodefony/src/profiler/aggregate.js +422 -0
  63. package/dist/nodefony/src/proxy/forward.js +29 -5
  64. package/dist/nodefony/src/proxy/generateProxyConfig.js +14 -6
  65. package/dist/nodefony/src/proxy/rules.js +12 -6
  66. package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +1 -2
  67. package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +1 -2
  68. package/dist/nodefony/src/rateLimit/rateLimitFilters.js +1 -2
  69. package/dist/nodefony/src/servers/http2Limits.js +50 -0
  70. package/dist/nodefony/src/servers/kernelServers.js +12 -3
  71. package/dist/nodefony/src/servers/portBinder.js +41 -10
  72. package/dist/nodefony/src/servers/upgradeDispatch.js +86 -7
  73. package/dist/nodefony/src/session/session.js +2 -3
  74. package/dist/nodefony/src/session/sessionStoreFailure.js +1 -2
  75. package/dist/nodefony/src/session/storage/MemorySessionStorage.js +1 -2
  76. package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +1 -2
  77. package/dist/nodefony/src/session/storage/sessionFilters.js +1 -2
  78. package/dist/nodefony/src/session/storage/sessionSort.js +1 -2
  79. package/dist/types/index.d.ts +7 -3
  80. package/dist/types/nodefony/interfaces/IContext.d.ts +63 -4
  81. package/dist/types/nodefony/interfaces/IRequest.d.ts +9 -4
  82. package/dist/types/nodefony/interfaces/IRouting.d.ts +10 -0
  83. package/dist/types/nodefony/interfaces/index.d.ts +2 -2
  84. package/dist/types/nodefony/service/certificates.d.ts +55 -4
  85. package/dist/types/nodefony/service/error-renderer.d.ts +31 -0
  86. package/dist/types/nodefony/service/http-kernel.d.ts +46 -10
  87. package/dist/types/nodefony/service/servers/clientError.d.ts +16 -0
  88. package/dist/types/nodefony/service/servers/server-h2c.d.ts +51 -0
  89. package/dist/types/nodefony/service/servers/server-http.d.ts +3 -3
  90. package/dist/types/nodefony/service/servers/server-https.d.ts +4 -4
  91. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +1 -3
  92. package/dist/types/nodefony/service/servers/server-websocket.d.ts +1 -3
  93. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +30 -16
  94. package/dist/types/nodefony/src/context/Context.d.ts +13 -4
  95. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +1 -1
  96. package/dist/types/nodefony/src/context/http/Request.d.ts +20 -15
  97. package/dist/types/nodefony/src/context/http/Response.d.ts +11 -0
  98. package/dist/types/nodefony/src/context/http/parser.d.ts +18 -2
  99. package/dist/types/nodefony/src/context/http2/Request.d.ts +1 -1
  100. package/dist/types/nodefony/src/context/http2/Response.d.ts +15 -5
  101. package/dist/types/nodefony/src/context/transport.d.ts +53 -0
  102. package/dist/types/nodefony/src/context/websocket/Response.d.ts +18 -2
  103. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +57 -7
  104. package/dist/types/nodefony/src/context/websocket/isWebsocketContext.d.ts +15 -0
  105. package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +13 -12
  106. package/dist/types/nodefony/src/profiler/Profiler.d.ts +68 -15
  107. package/dist/types/nodefony/src/profiler/aggregate.d.ts +215 -0
  108. package/dist/types/nodefony/src/proxy/forward.d.ts +14 -2
  109. package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +16 -0
  110. package/dist/types/nodefony/src/proxy/rules.d.ts +3 -0
  111. package/dist/types/nodefony/src/servers/http2Limits.d.ts +37 -0
  112. package/dist/types/nodefony/src/servers/kernelServers.d.ts +10 -1
  113. package/dist/types/nodefony/src/servers/portBinder.d.ts +29 -5
  114. package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +33 -3
  115. package/docs/cookies.md +85 -78
  116. package/docs/observabilite.md +80 -80
  117. package/docs/rate-limit.md +83 -83
  118. package/docs/reverse-proxy.md +54 -54
  119. package/docs/servers.md +338 -224
  120. package/docs/session.md +179 -179
  121. package/docs/sse.md +24 -20
  122. package/docs/upload.md +120 -117
  123. package/package.json +8 -9
  124. package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
@@ -1,6 +1,6 @@
1
1
  import { extractClientIp, resolveFromRight } from "./trustProxy.js";
2
2
  import { isIPv4 } from "node:net";
3
- //#region nodefony/src/context/forwarded.ts
3
+ // nodefony/src/context/forwarded.ts
4
4
  /**
5
5
  * Découpe une chaîne sur `sep` au **niveau supérieur uniquement**, en respectant
6
6
  * les `quoted-string` RFC 9110 §5.6.4 (`value = token / quoted-string`, RFC 7239 §4) : un
@@ -181,5 +181,4 @@ function resolveForwarded(headers, socketAddress, checker) {
181
181
  function hasForwardingHeaders(headers) {
182
182
  return Boolean(headers.forwarded || headers["x-forwarded-for"] || headers["x-forwarded-proto"] || headers["x-forwarded-scheme"] || headers["x-forwarded-host"]);
183
183
  }
184
- //#endregion
185
184
  export { forwardedNodeIp, hasForwardingHeaders, parseForwarded, resolveForwarded };
@@ -1,5 +1,6 @@
1
1
  import HttpError from "../../errors/httpError.js";
2
2
  import { responseEnded } from "../responseEnded.js";
3
+ import { alpnProtocolOf } from "../transport.js";
3
4
  import Cookie from "../../cookies/cookie.js";
4
5
  import Context from "../Context.js";
5
6
  import { sanitizeRequestId } from "../requestId.js";
@@ -11,7 +12,7 @@ import { describeSessionStoreFailure } from "../../session/sessionStoreFailure.j
11
12
  import { openSseStream } from "./SseStream.js";
12
13
  import { finallyMaybe, isPromise, thenMaybe, typeOf } from "nodefony";
13
14
  import url, { URL } from "node:url";
14
- //#region nodefony/src/context/http/HttpContext.ts
15
+ // nodefony/src/context/http/HttpContext.ts
15
16
  /**
16
17
  * Même URL (chemin, requête, hôte) sous un autre schéma et un autre port.
17
18
  * `port` absent → port par défaut du schéma (omis de l'URL).
@@ -58,12 +59,13 @@ var HttpContext = class extends Context {
58
59
  request;
59
60
  response;
60
61
  uploadService;
61
- constructor(container, request, response, type) {
62
- super(container, type);
62
+ constructor(container, request, response, serverType) {
63
+ super(container, serverType);
64
+ this.alpnProtocol = alpnProtocolOf(request);
63
65
  this.uploadService = this.get("upload");
64
66
  const perfSub = PERF_PROBE_SUB ? globalThis.__nfPerfProbe : void 0;
65
67
  if (perfSub && perfSub.t0 !== 0n) perfSub.uploadNs += Number(process.hrtime.bigint() - perfSub.t0);
66
- if (this.type === "http2") {
68
+ if (this.alpnProtocol === "h2" || this.alpnProtocol === "h2c") {
67
69
  this.request = new Http2Request(request, this);
68
70
  this.response = new Http2Response(response, this);
69
71
  } else {
@@ -124,7 +126,7 @@ var HttpContext = class extends Context {
124
126
  _onTimeout() {
125
127
  if (this.listenerCount("onTimeout")) this.fire("onTimeout", this);
126
128
  this._abortIfPending("Request timeout");
127
- let error = null;
129
+ let error;
128
130
  if (this.response.stream) error = new HttpError("Gateway Timeout", 504, this);
129
131
  else error = new HttpError("Request Timeout", 408, this);
130
132
  this.httpKernel?.onError(error, this);
@@ -414,5 +416,4 @@ var HttpContext = class extends Context {
414
416
  this.response.setContentType("html", encoding);
415
417
  }
416
418
  };
417
- //#endregion
418
419
  export { HttpContext as default, switchUrlScheme };
@@ -1,9 +1,9 @@
1
1
  import HttpError from "../../errors/httpError.js";
2
2
  import { hasForwardingHeaders, resolveForwarded } from "../forwarded.js";
3
3
  import { hostnameOf } from "../domainMatcher.js";
4
- import { Parser, ParserJson, ParserQs, ParserXml, acceptParser } from "./parser.js";
4
+ import { Parser, ParserJson, ParserQs, ParserRaw, ParserXml, acceptParser } from "./parser.js";
5
5
  import { isCanonicalAuthority, splitTarget } from "./urlFastPath.js";
6
- import { RequestContext, extend, useConfig } from "nodefony";
6
+ import { RequestContext, useConfig } from "nodefony";
7
7
  import { createHash, randomUUID } from "node:crypto";
8
8
  import { URL } from "node:url";
9
9
  import fs from "node:fs";
@@ -12,7 +12,7 @@ import os from "node:os";
12
12
  import path from "node:path";
13
13
  import QS from "qs";
14
14
  import { Busboy } from "@fastify/busboy";
15
- //#region nodefony/src/context/http/Request.ts
15
+ // nodefony/src/context/http/Request.ts
16
16
  const reg = /(.*)[[][\]]$/u;
17
17
  /**
18
18
  * Supprime un fichier temporaire d'upload sans jamais faire échouer le nettoyage :
@@ -34,6 +34,7 @@ const CHARSET_ALIASES = {
34
34
  "utf-16le": "utf16le"
35
35
  };
36
36
  const QUOTE_TRIM = /^["']|["']$/gu;
37
+ const NO_FILES = Object.freeze([]);
37
38
  const parse = {
38
39
  POST: true,
39
40
  PUT: true,
@@ -104,7 +105,7 @@ var HttpRequest = class {
104
105
  let u = this.#url;
105
106
  if (u === null) {
106
107
  u = this.getUrl(this.sUrl);
107
- u.query = this.queryGet;
108
+ u.query = this.query;
108
109
  this.#url = u;
109
110
  }
110
111
  return u;
@@ -113,24 +114,28 @@ var HttpRequest = class {
113
114
  this.#url = u;
114
115
  }
115
116
  parser = null;
116
- queryPost = {};
117
- queryGet = {};
118
- queryFile = [];
119
- query = {};
120
117
  /**
121
- * Le CORPS de la requête, parsé — nom universel de l'écosystème (Express,
122
- * Fastify, NestJS), alias de {@link queryPost}.
123
- *
124
- * Ne pas confondre avec {@link query}, qui FUSIONNE la query string et le
125
- * corps : `body` ne rend jamais un paramètre d'URL. Le data plane admin
126
- * expose déjà cette sémantique (`IAdminRequest.body`) ; ce getter la rend
127
- * disponible partout.
118
+ * Le CORPS de la requête, parsé (JSON, urlencoded, XML, champs multipart) —
119
+ * nom universel de l'écosystème (Express, Fastify, NestJS) et celui
120
+ * d'`IAdminRequest.body`. Jamais un paramètre d'URL : ceux-là sont dans
121
+ * {@link query}, et les deux ne fusionnent pas.
122
+ */
123
+ body = {};
124
+ /** Fichiers reçus, alloués au PREMIER fichier seulement (cf {@link files}). */
125
+ #uploads = null;
126
+ /**
127
+ * Les fichiers reçus dans un corps multipart, déjà écrits dans le répertoire
128
+ * temporaire — lus par `@UploadedFile()` / `@UploadedFiles()`. Les champs
129
+ * texte du même corps sont dans {@link body}.
128
130
  *
129
- * Getter (aucune allocation) : `queryPost` reste la source unique.
131
+ * Lecture seule : une requête sans upload rend un vide partagé et gelé,
132
+ * aucune allocation.
130
133
  */
131
- get body() {
132
- return this.queryPost;
134
+ get files() {
135
+ return this.#uploads ?? NO_FILES;
133
136
  }
137
+ /** Les paramètres d'URL, seuls — jamais un champ du corps ({@link body}). */
138
+ query = {};
134
139
  queryStringOptions;
135
140
  charset = "utf8";
136
141
  uploadOption = {};
@@ -185,7 +190,6 @@ var HttpRequest = class {
185
190
  if (this.search) query = QS.parse(this.search.slice(1), this.queryStringOptions ?? {});
186
191
  else query = {};
187
192
  if (this.#url !== null) this.#url.query = query;
188
- this.queryGet = query;
189
193
  this.query = query;
190
194
  if (PERF_PROBE_SUB) perfMark("reqQueryNs");
191
195
  this.contentType = this.getContentType(this.request);
@@ -201,11 +205,11 @@ var HttpRequest = class {
201
205
  * remplace le `once("onRequestEnd")` du ctor qui allouait 1 onceWrapper +
202
206
  * 1 closure à CHAQUE requête.
203
207
  *
204
- * Il porte le CORPS (`queryPost`), pas la fusion `query` : un seul sens de
208
+ * Il porte le CORPS (`body`), jamais les paramètres d'URL : un seul sens de
205
209
  * `body` dans tout le framework, celui de l'écosystème et d'`IAdminRequest`.
206
210
  */
207
211
  fireRequestEnd() {
208
- this.request.body = this.queryPost;
212
+ this.request.body = this.body;
209
213
  if (this.context.listenerCount("onRequestEnd") === 0) return false;
210
214
  return this.context.fireAsync("onRequestEnd", this);
211
215
  }
@@ -315,7 +319,7 @@ var HttpRequest = class {
315
319
  await this.fireRequestEnd();
316
320
  return BODY_DONE;
317
321
  }
318
- this.parser = new Parser(this);
322
+ this.parser = new ParserRaw(this);
319
323
  return this.parser;
320
324
  }
321
325
  }
@@ -362,8 +366,7 @@ var HttpRequest = class {
362
366
  return this.parser;
363
367
  }
364
368
  const { fields, files } = await this.streamMultipart(bb);
365
- this.queryPost = fields;
366
- this.query = extend({}, this.query, this.queryPost);
369
+ this.body = fields;
367
370
  for (const pf of files) await this.createFileUpload(pf.field, pf.file);
368
371
  this.context.requestEnded = true;
369
372
  await this.fireRequestEnd();
@@ -478,7 +481,7 @@ var HttpRequest = class {
478
481
  }
479
482
  accepts(Type) {
480
483
  let parts = [];
481
- let subtype = "*";
484
+ let subtype;
482
485
  let type = "*";
483
486
  if (Type) parts = Type.split("/");
484
487
  switch (parts.length) {
@@ -496,7 +499,7 @@ var HttpRequest = class {
496
499
  }
497
500
  async createFileUpload(name, file) {
498
501
  const fileUpload = await this.context.uploadService?.createUploadFile(file, name);
499
- if (fileUpload) this.queryFile.push(fileUpload);
502
+ if (fileUpload) (this.#uploads ??= []).push(fileUpload);
500
503
  return fileUpload;
501
504
  }
502
505
  getMethod() {
@@ -561,7 +564,9 @@ var HttpRequest = class {
561
564
  /**
562
565
  * Scheme effectif côté client : `Forwarded`/`X-Forwarded-*` résolu de façon
563
566
  * canonique (this.forwarded, gated proxy de confiance) ; sinon le transport
564
- * réel. HTTP/2 remplace la lecture du socket par le pseudo-header `:scheme`.
567
+ * réel. Vaut pour HTTP/2 aussi : le pseudo-en-tête `:scheme` vient du client
568
+ * (RFC 9113 §8.3.1) et n'est jamais cru — sur h2c, `:scheme: https` ferait
569
+ * passer une connexion claire pour chiffrée.
565
570
  */
566
571
  resolveScheme(request) {
567
572
  if (this.forwarded?.proto) return this.forwarded.proto;
@@ -589,9 +594,8 @@ var HttpRequest = class {
589
594
  return new URL(sUrl, baseUrl);
590
595
  }
591
596
  log(pci, severity, msgid, msg) {
592
- if (!msgid) msgid = `${this.context.type} REQUEST `;
597
+ if (!msgid) msgid = `${this.context.serverType} REQUEST `;
593
598
  return this.context.log(pci, severity, msgid, msg);
594
599
  }
595
600
  };
596
- //#endregion
597
601
  export { HttpRequest as default };
@@ -2,7 +2,7 @@ import Cookie from "../../cookies/cookie.js";
2
2
  import { thenMaybe, typeOf } from "nodefony";
3
3
  import http from "node:http";
4
4
  import mime from "mime-types";
5
- //#region nodefony/src/context/http/Response.ts
5
+ // nodefony/src/context/http/Response.ts
6
6
  const ANSI_REGEX = new RegExp(["[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)", "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))"].join("|"), "g");
7
7
  const stripAnsi = function(val) {
8
8
  return typeof val === "string" ? val.replace(ANSI_REGEX, "") : val;
@@ -55,7 +55,8 @@ var HttpResponse = class {
55
55
  constructor(response, context) {
56
56
  this.context = context;
57
57
  this.response = response;
58
- this.timeout = this.context.httpKernel?.responseTimeout[this.context.type];
58
+ const responseTimeout = this.context.httpKernel?.responseTimeout;
59
+ this.timeout = this.context.serverType === "https" ? responseTimeout?.https : responseTimeout?.http;
59
60
  }
60
61
  /**
61
62
  * Filet posé juste avant l'émission des en-têtes : si aucun `Content-Type`
@@ -153,16 +154,33 @@ var HttpResponse = class {
153
154
  const serialized = [];
154
155
  for (const cookie of cookies) {
155
156
  const s = cookie.serialize();
156
- this.log(`ADD COOKIE ==> ${s}`, "DEBUG");
157
+ this.logCookie(cookie, s);
157
158
  serialized.push(s);
158
159
  }
159
160
  return this.setHeader("Set-Cookie", serialized);
160
161
  }
161
162
  setCookie(cookie) {
162
163
  const serialize = cookie.serialize();
163
- this.log(`ADD COOKIE ==> ${serialize}`, "DEBUG");
164
+ this.logCookie(cookie, serialize);
164
165
  return this.setHeader("Set-Cookie", serialize);
165
166
  }
167
+ /**
168
+ * Journalise en DEBUG un cookie posé, sa VALEUR MASQUÉE : nom et attributs
169
+ * seulement. Le journal de développement se lit par le plan d'administration
170
+ * — un identifiant de session y entrerait chez quiconque le consulte.
171
+ * La ligne n'est construite que si DEBUG passe le gate du syslog pour ce
172
+ * `msgid` (debug ciblé compris) : rien n'est alloué en production.
173
+ *
174
+ * @param cookie - le cookie posé.
175
+ * @param serialized - sa forme `Set-Cookie`, déjà calculée pour l'en-tête.
176
+ */
177
+ logCookie(cookie, serialized) {
178
+ const msgid = `${this.context.serverType} RESPONSE `;
179
+ const syslog = this.context.syslog;
180
+ if (syslog && !syslog.severityEnabled("DEBUG", msgid)) return;
181
+ const attrs = serialized.indexOf("; ", cookie.name.length + 1);
182
+ this.log(`ADD COOKIE ==> ${cookie.name}=<masqué>${attrs === -1 ? "" : serialized.slice(attrs)}`, "DEBUG", msgid);
183
+ }
166
184
  setHeader(name, value) {
167
185
  if (this.response) {
168
186
  if (this.flushing) {
@@ -459,9 +477,8 @@ var HttpResponse = class {
459
477
  return this;
460
478
  }
461
479
  log(pci, severity, msgid, msg) {
462
- if (!msgid) msgid = `${this.context.type} RESPONSE `;
480
+ if (!msgid) msgid = `${this.context.serverType} RESPONSE `;
463
481
  return this.context.log(pci, severity, msgid, msg);
464
482
  }
465
483
  };
466
- //#endregion
467
484
  export { HttpResponse as default };
@@ -3,7 +3,7 @@ import Http2Response from "../http2/Response.js";
3
3
  import { RequestContext } from "nodefony";
4
4
  import http from "node:http";
5
5
  import { AsyncResource } from "node:async_hooks";
6
- //#region nodefony/src/context/http/SseStream.ts
6
+ // nodefony/src/context/http/SseStream.ts
7
7
  /** Battement de cœur par défaut, en ms (§9.2.7 : « every 15 seconds or so »). */
8
8
  const SSE_HEARTBEAT_MS = 15e3;
9
9
  /** Délai de blocage d'écriture par défaut, en ms. */
@@ -346,5 +346,4 @@ function startSseStream(context, options, settings) {
346
346
  if (options.retry !== void 0) sse.retry(options.retry);
347
347
  return sse;
348
348
  }
349
- //#endregion
350
349
  export { SSE_HEARTBEAT_MS, SSE_MAX_EVENT_BYTES, SSE_STALL_TIMEOUT_MS, SseStream, acceptsEventStream, openSseStream };
@@ -1,8 +1,8 @@
1
1
  import HttpError from "../../errors/httpError.js";
2
- import { escapeRegExp, extend } from "nodefony";
2
+ import { escapeRegExp } from "nodefony";
3
3
  import QS from "qs";
4
4
  import xml2js from "xml2js";
5
- //#region nodefony/src/context/http/parser.ts
5
+ // nodefony/src/context/http/parser.ts
6
6
  var Parser = class {
7
7
  request;
8
8
  chunks;
@@ -73,6 +73,27 @@ var Parser = class {
73
73
  return this;
74
74
  }
75
75
  };
76
+ /**
77
+ * Corps d'un type que le framework ne décode pas : livré TEL QU'IL EST REÇU,
78
+ * jamais remplacé par un objet vide qui mentirait.
79
+ *
80
+ * - `text/*` → une chaîne, décodée au `charset` du `Content-Type` (UTF-8 par
81
+ * défaut) — comme une trame texte RFC 6455 ;
82
+ * - tout autre type, et un corps sans `Content-Type` (RFC 9110 §8.3 : supposé
83
+ * `application/octet-stream`) → les OCTETS : le `Buffer` déjà assemblé dans
84
+ * `request.data`, sans copie — donc un `Uint8Array`, le type d'octets que
85
+ * la socket livrera aussi (#618). Un même schéma passe par les deux portes.
86
+ *
87
+ * Un corps VIDE laisse `body` tel quel : une requête sans charge ne change pas.
88
+ */
89
+ var ParserRaw = class extends Parser {
90
+ async parse() {
91
+ await super.parse();
92
+ const data = this.request.data;
93
+ if (data.length > 0) this.request.body = this.request.contentType?.startsWith("text/") ? data.toString(this.request.charset) : data;
94
+ return this;
95
+ }
96
+ };
76
97
  var ParserQs = class extends Parser {
77
98
  parserOptions;
78
99
  charset = "utf8";
@@ -83,8 +104,7 @@ var ParserQs = class extends Parser {
83
104
  }
84
105
  async parse() {
85
106
  await super.parse();
86
- this.request.queryPost = QS.parse(this.request.data.toString(this.charset), this.parserOptions);
87
- this.request.query = extend({}, this.request.query, this.request.queryPost);
107
+ this.request.body = QS.parse(this.request.data.toString(this.charset), this.parserOptions);
88
108
  this.request.context.requestEnded = true;
89
109
  return this;
90
110
  }
@@ -102,7 +122,7 @@ var ParserXml = class extends Parser {
102
122
  return new Promise((resolve, reject) => {
103
123
  this.xmlParser.parseString(this.request.data.toString(this.charset), (err, result) => {
104
124
  if (err) return reject(err);
105
- this.request.queryPost = result;
125
+ this.request.body = result;
106
126
  this.request.context.requestEnded = true;
107
127
  return resolve(this);
108
128
  });
@@ -110,7 +130,14 @@ var ParserXml = class extends Parser {
110
130
  }
111
131
  };
112
132
  /**
113
- * Parse un corps `application/json` (ou `*+json`) en objet → `queryPost`
133
+ * Vrai pour un objet ou un tableau JSON — ce que `body` peut porter.
134
+ * Un tableau passe : `@Body()` le rend tel quel (envoi en lot).
135
+ */
136
+ function isJsonContainer(value) {
137
+ return typeof value === "object" && value !== null;
138
+ }
139
+ /**
140
+ * Parse un corps `application/json` (ou `*+json`) en objet → `body`
114
141
  * (source lue par le décorateur `@Body`). Reprend le rôle de l'ancien plugin
115
142
  * `json` de formidable, supprimé avec le passage à busboy (multipart seul).
116
143
  * Lenient : un corps JSON malformé est ignoré (pas de throw — `parse()` est
@@ -127,8 +154,8 @@ var ParserJson = class extends Parser {
127
154
  await super.parse();
128
155
  const text = this.request.data.toString(this.charset).trim();
129
156
  if (text) try {
130
- this.request.queryPost = JSON.parse(text);
131
- this.request.query = extend({}, this.request.query, this.request.queryPost);
157
+ const parsed = JSON.parse(text);
158
+ if (isJsonContainer(parsed)) this.request.body = parsed;
132
159
  } catch {}
133
160
  this.request.context.requestEnded = true;
134
161
  return this;
@@ -183,5 +210,4 @@ const acceptParser = function(acc) {
183
210
  return ACCEPT_ANY;
184
211
  }
185
212
  };
186
- //#endregion
187
- export { Parser, ParserJson, ParserQs, ParserXml, acceptParser };
213
+ export { Parser, ParserJson, ParserQs, ParserRaw, ParserXml, acceptParser };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/context/http/urlFastPath.ts
1
+ // nodefony/src/context/http/urlFastPath.ts
2
2
  const PATH_SAFE = /* @__PURE__ */ new Uint8Array(128);
3
3
  const SEARCH_SAFE = /* @__PURE__ */ new Uint8Array(128);
4
4
  const HOST_SAFE = /* @__PURE__ */ new Uint8Array(128);
@@ -123,5 +123,4 @@ function isDigitsFrom(s, from) {
123
123
  }
124
124
  return true;
125
125
  }
126
- //#endregion
127
126
  export { isCanonicalAuthority, splitTarget };
@@ -1,5 +1,6 @@
1
+ import { requestAuthority } from "../transport.js";
1
2
  import HttpRequest from "../http/Request.js";
2
- //#region nodefony/src/context/http2/Request.ts
3
+ // nodefony/src/context/http2/Request.ts
3
4
  var Http2Request = class extends HttpRequest {
4
5
  request;
5
6
  constructor(request, context) {
@@ -7,8 +8,9 @@ var Http2Request = class extends HttpRequest {
7
8
  this.context = context;
8
9
  this.request = request;
9
10
  }
11
+ /** `:authority`, sinon `Host` — cf `requestAuthority` (RFC 9113 §8.3.1). */
10
12
  getHost() {
11
- return this.headers[":authority"];
13
+ return requestAuthority(this.request.headers);
12
14
  }
13
15
  getUserAgent() {
14
16
  return this.headers["user-agent"];
@@ -19,11 +21,5 @@ var Http2Request = class extends HttpRequest {
19
21
  getRawTarget() {
20
22
  return this.headers[":path"];
21
23
  }
22
- resolveScheme() {
23
- if (this.forwarded?.proto) return this.forwarded.proto;
24
- if (this.headers[":scheme"] === "https") return "https";
25
- return "http";
26
- }
27
24
  };
28
- //#endregion
29
25
  export { Http2Request as default };
@@ -2,8 +2,13 @@ import HttpResponse from "../http/Response.js";
2
2
  import { thenMaybe } from "nodefony";
3
3
  import http from "node:http";
4
4
  import http2 from "node:http2";
5
- //#region nodefony/src/context/http2/Response.ts
5
+ // nodefony/src/context/http2/Response.ts
6
6
  const HTTP2_HEADER_STATUS = http2.constants.HTTP2_HEADER_STATUS;
7
+ /** Réponse Node déjà libérée : aucun en-tête posé à reprendre. */
8
+ const emptyHeaders = {
9
+ getHeaderNames: () => [],
10
+ getHeader: () => void 0
11
+ };
7
12
  /**
8
13
  * Compose les en-têtes d'une réponse HTTP/2 : ceux posés sur la réponse, puis
9
14
  * ceux passés à `writeHead`, qui gagnent.
@@ -14,14 +19,19 @@ const HTTP2_HEADER_STATUS = http2.constants.HTTP2_HEADER_STATUS;
14
19
  * (`ERR_HTTP2_HEADER_SINGLE_VALUE`), soit une 500 sur tout média servi en
15
20
  * HTTP/2. HTTP/2 n'a de toute façon que des noms en minuscules (RFC 9113 §8.2.1).
16
21
  *
22
+ * Les en-têtes posés se lisent nom par nom (`getHeaderNames`, déjà en
23
+ * minuscules) : `getHeaders()` les recopiait d'abord dans un objet sans
24
+ * prototype, avant cette fusion — 3,8 µs par réponse pour 12 en-têtes,
25
+ * contre 1,1 µs en une passe.
26
+ *
17
27
  * @param statusMessage - message de statut, recopié en `x-status-message`
18
- * @param current - en-têtes déjà posés sur la réponse
28
+ * @param current - la réponse Node qui porte les en-têtes déjà posés
19
29
  * @param extra - en-têtes passés à `writeHead` (objet, ou liste plate clé/valeur)
20
30
  * @returns les en-têtes à transmettre à `respond()`
21
31
  */
22
32
  function mergeResponseHeaders(statusMessage, current, extra) {
23
33
  const merged = { "x-status-message": statusMessage };
24
- for (const key in current) merged[key.toLowerCase()] = current[key];
34
+ for (const name of current.getHeaderNames()) merged[name] = current.getHeader(name);
25
35
  if (Array.isArray(extra)) for (let i = 0; i + 1 < extra.length; i += 2) merged[String(extra[i]).toLowerCase()] = extra[i + 1];
26
36
  else if (extra) for (const key in extra) {
27
37
  const value = extra[key];
@@ -58,7 +68,7 @@ var Http2Response = class extends HttpResponse {
58
68
  this.statusMessage = this.getStatusMessage();
59
69
  this.setLength();
60
70
  this.ensureContentTypeHeader();
61
- this.headers = mergeResponseHeaders(this.statusMessage, this.getHeaders(), headers);
71
+ this.headers = mergeResponseHeaders(this.statusMessage, this.response ?? emptyHeaders, headers);
62
72
  this.headers[HTTP2_HEADER_STATUS] = this.statusCode;
63
73
  const requestId = this.context.requestId;
64
74
  if (requestId) this.headers["x-request-id"] = requestId;
@@ -69,9 +79,9 @@ var Http2Response = class extends HttpResponse {
69
79
  } else return super.writeHead(statusCode, headers);
70
80
  }
71
81
  /**
72
- * Écrit le corps sur le flux HTTP/2. Synchrone pour une redirection, un flux
73
- * fermé ou le repli HTTP/1 ; une promesse quand le corps est écrit sur le
74
- * flux, réglée par le rappel d'écriture (#505).
82
+ * Écrit le corps sur le flux HTTP/2 et, pour une réponse unique, le termine
83
+ * d'un seul `end(corps)`. Synchrone, sauf en streaming (`flush()`) : une
84
+ * promesse réglée par le rappel d'écriture, la contre-pression (#505).
75
85
  *
76
86
  * @returns la réponse, ou sa promesse.
77
87
  */
@@ -87,6 +97,10 @@ var Http2Response = class extends HttpResponse {
87
97
  return this;
88
98
  }
89
99
  if (chunk) this.setBody(chunk);
100
+ if (!_flush && !this.flushing) {
101
+ stream.end(this.payload, this.payloadEncoding);
102
+ return this;
103
+ }
90
104
  return new Promise((resolve, reject) => {
91
105
  stream.write(this.payload, this.payloadEncoding, (error) => {
92
106
  if (error) return reject(error);
@@ -102,7 +116,7 @@ var Http2Response = class extends HttpResponse {
102
116
  */
103
117
  end(chunk, encoding) {
104
118
  if (this.stream) {
105
- if (this.stream.destroyed || this.stream.closed) return this.stream;
119
+ if (this.stream.writableEnded || this.stream.destroyed || this.stream.closed) return this.stream;
106
120
  return this.stream.end(chunk, encoding || this.encoding);
107
121
  }
108
122
  return super.end(chunk, encoding);
@@ -121,5 +135,4 @@ var Http2Response = class extends HttpResponse {
121
135
  };
122
136
  }
123
137
  };
124
- //#endregion
125
138
  export { Http2Response as default, mergeResponseHeaders };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/context/metaData.ts
1
+ // nodefony/src/context/metaData.ts
2
2
  /**
3
3
  * Assemble l'enveloppe `metaData` d'une requête EN PLACE (builder monomorphe) :
4
4
  * met à jour les champs de `target.nodefony` dans un ordre fixe → V8 garde une
@@ -43,5 +43,4 @@ function buildMetaData(target, src, override) {
43
43
  }
44
44
  return target;
45
45
  }
46
- //#endregion
47
46
  export { buildMetaData };
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/context/requestId.ts
1
+ // nodefony/src/context/requestId.ts
2
2
  /**
3
3
  * Validation de l'identifiant de corrélation `X-Request-Id` fourni par le
4
4
  * client, avant qu'il ne soit adopté comme `Context.requestId`.
@@ -37,5 +37,4 @@ function sanitizeRequestId(raw) {
37
37
  if (!raw || raw.length > 128) return null;
38
38
  return SAFE_REQUEST_ID.test(raw) ? raw : null;
39
39
  }
40
- //#endregion
41
40
  export { MAX_REQUEST_ID_LENGTH, sanitizeRequestId };
@@ -1,5 +1,5 @@
1
1
  import http2 from "node:http2";
2
- //#region nodefony/src/context/responseEnded.ts
2
+ // nodefony/src/context/responseEnded.ts
3
3
  /**
4
4
  * La réponse a-t-elle été TERMINÉE par le serveur ? Règle UNIQUE, pour HTTP/1.1
5
5
  * comme pour HTTP/2 — l'écouteur `close` du noyau (client parti → 499) et le
@@ -23,5 +23,4 @@ function responseEnded(response) {
23
23
  const stream = response.stream;
24
24
  return stream.writableEnded && !stream.aborted;
25
25
  }
26
- //#endregion
27
26
  export { responseEnded };
@@ -0,0 +1,45 @@
1
+ // nodefony/src/context/transport.ts
2
+ /**
3
+ * Famille du protocole traité par un contexte, déduite du serveur qui l'a
4
+ * reçu. Règle UNIQUE : `Context` la pose à la construction.
5
+ *
6
+ * @param serverType - identité du serveur (`http`, `https`, `ws`, `wss`)
7
+ * @returns `"websocket"` pour `ws`/`wss`, `"http"` sinon
8
+ */
9
+ function protocolNameOf(serverType) {
10
+ return serverType === "ws" || serverType === "wss" ? "websocket" : "http";
11
+ }
12
+ /**
13
+ * Protocole du saut qui a porté cette requête jusqu'au processus, au sens du
14
+ * registre IANA des identifiants ALPN. Règle UNIQUE : `HttpContext` la lit pour
15
+ * choisir ses classes de requête et de réponse.
16
+ *
17
+ * La valeur se CONSTATE sur la requête, elle ne se déduit pas du serveur : un
18
+ * même serveur TLS reçoit HTTP/1.1 et h2 (`allowHTTP1`). Pour h2, c'est la
19
+ * session de Node qui tranche — `Http2Session.alpnProtocol` vaut `"h2c"` sur
20
+ * une session en clair, l'ALPN négocié (`"h2"`) sur une session TLS.
21
+ *
22
+ * @param request - la requête Node reçue par le serveur
23
+ * @returns `"http/1.1"`, `"h2"` ou `"h2c"`
24
+ */
25
+ function alpnProtocolOf(request) {
26
+ if (request.httpVersionMajor !== 2) return "http/1.1";
27
+ return (request.stream?.session)?.alpnProtocol === "h2c" ? "h2c" : "h2";
28
+ }
29
+ /**
30
+ * Autorité (hôte et port) d'une requête, HTTP/1.1 comme HTTP/2. Règle UNIQUE
31
+ * des lecteurs du serveur.
32
+ *
33
+ * `:authority` d'abord, puis `Host` (RFC 9113 §8.3.1). Le repli n'est pas un
34
+ * confort : un intermédiaire qui convertit une requête HTTP/1.1 de forme origine
35
+ * (`GET /chemin`) en HTTP/2 ne DOIT PAS fabriquer `:authority` — il garde
36
+ * `Host`. C'est ce que fait HAProxy `proto h2` vers un port h2c : sans ce repli,
37
+ * l'hôte est introuvable et la requête part en 421.
38
+ *
39
+ * @param headers - en-têtes de la requête Node (`IncomingMessage` ou `Http2ServerRequest`).
40
+ * @returns l'autorité, ou `undefined` si la requête n'en porte aucune.
41
+ */
42
+ function requestAuthority(headers) {
43
+ return headers[":authority"] ?? headers.host;
44
+ }
45
+ export { alpnProtocolOf, protocolNameOf, requestAuthority };
@@ -1,5 +1,5 @@
1
1
  import { BlockList, isIPv4, isIPv6 } from "node:net";
2
- //#region nodefony/src/context/trustProxy.ts
2
+ // nodefony/src/context/trustProxy.ts
3
3
  const PRESETS = {
4
4
  loopback: [[
5
5
  "127.0.0.0",
@@ -164,5 +164,4 @@ function extractClientIp(xff, socketAddress, checker) {
164
164
  }
165
165
  return resolveFromRight(s.split(",").map((part) => part.trim()).filter(Boolean), socketAddress, checker);
166
166
  }
167
- //#endregion
168
167
  export { buildTrustProxy, extractClientIp, resolveFromRight };