@nodefony/http 10.0.0-beta.5 → 10.0.0-beta.7

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 (130) hide show
  1. package/README.md +24 -0
  2. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorateParam.js +1 -1
  4. package/dist/index.js +30 -26
  5. package/dist/nodefony/command/proxyGenerateCommand.js +2 -2
  6. package/dist/nodefony/config/config.js +29 -4
  7. package/dist/nodefony/grpc/GrpcCall.js +298 -0
  8. package/dist/nodefony/grpc/GrpcEndpoint.js +528 -0
  9. package/dist/nodefony/grpc/index.js +4 -0
  10. package/dist/nodefony/interfaces/IRpcCall.js +1 -0
  11. package/dist/nodefony/service/ProfilerAdminApi.js +5 -2
  12. package/dist/nodefony/service/audit-logger.js +52 -6
  13. package/dist/nodefony/service/error-renderer.js +47 -13
  14. package/dist/nodefony/service/http-kernel.js +306 -49
  15. package/dist/nodefony/service/pretty-request-logger.js +5 -3
  16. package/dist/nodefony/service/request-logger.js +21 -6
  17. package/dist/nodefony/service/reverse-proxy.js +8 -3
  18. package/dist/nodefony/service/servers/clientError.js +53 -4
  19. package/dist/nodefony/service/servers/server-h2c.js +6 -6
  20. package/dist/nodefony/service/servers/server-http.js +6 -4
  21. package/dist/nodefony/service/servers/server-https.js +14 -7
  22. package/dist/nodefony/service/servers/server-static.js +1 -6
  23. package/dist/nodefony/service/servers/server-websocket-secure.js +7 -11
  24. package/dist/nodefony/service/servers/server-websocket.js +7 -11
  25. package/dist/nodefony/service/servers/serverShutdown.js +43 -6
  26. package/dist/nodefony/service/sessions/sessions-service.js +7 -7
  27. package/dist/nodefony/service/upload/upload-service.js +2 -2
  28. package/dist/nodefony/src/assets/prebuiltUi.js +23 -5
  29. package/dist/nodefony/src/context/Context.js +12 -1
  30. package/dist/nodefony/src/context/http/ByteStream.js +364 -0
  31. package/dist/nodefony/src/context/http/HttpContext.js +139 -32
  32. package/dist/nodefony/src/context/http/Request.js +111 -22
  33. package/dist/nodefony/src/context/http/Response.js +459 -37
  34. package/dist/nodefony/src/context/http/SseStream.js +83 -148
  35. package/dist/nodefony/src/context/http/StreamLifecycle.js +582 -0
  36. package/dist/nodefony/src/context/http/parser.js +29 -11
  37. package/dist/nodefony/src/context/http/urlFastPath.js +80 -1
  38. package/dist/nodefony/src/context/http/webResponse.js +56 -0
  39. package/dist/nodefony/src/context/http2/Request.js +1 -1
  40. package/dist/nodefony/src/context/http2/Response.js +55 -12
  41. package/dist/nodefony/src/context/requestTrace.js +6 -2
  42. package/dist/nodefony/src/context/rpcOutcome.js +20 -0
  43. package/dist/nodefony/src/context/transport.js +20 -1
  44. package/dist/nodefony/src/context/websocket/Response.js +15 -3
  45. package/dist/nodefony/src/context/websocket/WebsocketContext.js +86 -18
  46. package/dist/nodefony/src/context/websocket/websocketUrl.js +7 -0
  47. package/dist/nodefony/src/context/websocket/wsBackpressure.js +15 -15
  48. package/dist/nodefony/src/context/websocket/wsFrameRate.js +71 -0
  49. package/dist/nodefony/src/context/websocket/wsLogContent.js +55 -17
  50. package/dist/nodefony/src/cookies/cookie.js +57 -14
  51. package/dist/nodefony/src/errors/httpError.js +58 -25
  52. package/dist/nodefony/src/grpc/cors.js +59 -0
  53. package/dist/nodefony/src/grpc/limits.js +16 -0
  54. package/dist/nodefony/src/grpc/refusal.js +271 -0
  55. package/dist/nodefony/src/profiler/Profiler.js +14 -1
  56. package/dist/nodefony/src/profiler/aggregate.js +2 -2
  57. package/dist/nodefony/src/proxy/forward.js +109 -9
  58. package/dist/nodefony/src/proxy/generateProxyConfig.js +97 -17
  59. package/dist/nodefony/src/proxy/rules.js +29 -14
  60. package/dist/nodefony/src/servers/http2Goaway.js +76 -0
  61. package/dist/nodefony/src/servers/http2Limits.js +41 -15
  62. package/dist/nodefony/src/servers/http2StreamGrace.js +100 -0
  63. package/dist/nodefony/src/servers/receiveDeadline.js +177 -0
  64. package/dist/nodefony/src/servers/upgradeDispatch.js +62 -4
  65. package/dist/types/index.d.ts +11 -2
  66. package/dist/types/nodefony/config/config.d.ts +14 -0
  67. package/dist/types/nodefony/grpc/GrpcCall.d.ts +142 -0
  68. package/dist/types/nodefony/grpc/GrpcEndpoint.d.ts +98 -0
  69. package/dist/types/nodefony/grpc/index.d.ts +15 -0
  70. package/dist/types/nodefony/interfaces/IContext.d.ts +9 -0
  71. package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +14 -0
  72. package/dist/types/nodefony/interfaces/IRequest.d.ts +2 -1
  73. package/dist/types/nodefony/interfaces/IRequestTrace.d.ts +10 -0
  74. package/dist/types/nodefony/interfaces/IRouting.d.ts +19 -0
  75. package/dist/types/nodefony/interfaces/IRpcCall.d.ts +83 -0
  76. package/dist/types/nodefony/interfaces/ISecurity.d.ts +27 -2
  77. package/dist/types/nodefony/interfaces/IUpload.d.ts +51 -4
  78. package/dist/types/nodefony/interfaces/index.d.ts +2 -1
  79. package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +2 -1
  80. package/dist/types/nodefony/service/audit-logger.d.ts +33 -3
  81. package/dist/types/nodefony/service/error-renderer.d.ts +27 -5
  82. package/dist/types/nodefony/service/http-kernel.d.ts +36 -3
  83. package/dist/types/nodefony/service/request-logger.d.ts +10 -0
  84. package/dist/types/nodefony/service/servers/clientError.d.ts +3 -2
  85. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +7 -5
  86. package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +9 -4
  87. package/dist/types/nodefony/src/context/Context.d.ts +9 -1
  88. package/dist/types/nodefony/src/context/http/ByteStream.d.ts +109 -0
  89. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +99 -3
  90. package/dist/types/nodefony/src/context/http/Request.d.ts +46 -9
  91. package/dist/types/nodefony/src/context/http/Response.d.ts +188 -8
  92. package/dist/types/nodefony/src/context/http/SseStream.d.ts +33 -8
  93. package/dist/types/nodefony/src/context/http/StreamLifecycle.d.ts +106 -0
  94. package/dist/types/nodefony/src/context/http/parser.d.ts +17 -3
  95. package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +40 -0
  96. package/dist/types/nodefony/src/context/http/webResponse.d.ts +31 -0
  97. package/dist/types/nodefony/src/context/http2/Response.d.ts +40 -5
  98. package/dist/types/nodefony/src/context/requestTrace.d.ts +3 -1
  99. package/dist/types/nodefony/src/context/rpcOutcome.d.ts +11 -0
  100. package/dist/types/nodefony/src/context/transport.d.ts +17 -0
  101. package/dist/types/nodefony/src/context/websocket/Response.d.ts +6 -1
  102. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +48 -0
  103. package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +4 -0
  104. package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +12 -2
  105. package/dist/types/nodefony/src/context/websocket/wsFrameRate.d.ts +69 -0
  106. package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +16 -8
  107. package/dist/types/nodefony/src/cookies/cookie.d.ts +20 -2
  108. package/dist/types/nodefony/src/errors/httpError.d.ts +29 -4
  109. package/dist/types/nodefony/src/grpc/cors.d.ts +32 -0
  110. package/dist/types/nodefony/src/grpc/limits.d.ts +14 -0
  111. package/dist/types/nodefony/src/grpc/refusal.d.ts +136 -0
  112. package/dist/types/nodefony/src/profiler/Profiler.d.ts +11 -1
  113. package/dist/types/nodefony/src/proxy/forward.d.ts +37 -2
  114. package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +15 -2
  115. package/dist/types/nodefony/src/proxy/rules.d.ts +20 -0
  116. package/dist/types/nodefony/src/servers/http2Goaway.d.ts +41 -0
  117. package/dist/types/nodefony/src/servers/http2Limits.d.ts +27 -14
  118. package/dist/types/nodefony/src/servers/http2StreamGrace.d.ts +34 -0
  119. package/dist/types/nodefony/src/servers/receiveDeadline.d.ts +77 -0
  120. package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +41 -1
  121. package/docs/byte-stream.md +406 -0
  122. package/docs/cookies.md +13 -3
  123. package/docs/diagnostics-channel.md +45 -1
  124. package/docs/index.md +8 -0
  125. package/docs/reverse-proxy.md +15 -3
  126. package/docs/servers.md +67 -43
  127. package/docs/session.md +3 -3
  128. package/docs/sse.md +27 -20
  129. package/docs/upload.md +43 -17
  130. package/package.json +38 -18
@@ -1,8 +1,43 @@
1
1
  import Cookie from "../../cookies/cookie.js";
2
2
  import { thenMaybe, typeOf } from "nodefony";
3
- import http from "node:http";
3
+ import http, { validateHeaderName, validateHeaderValue } from "node:http";
4
4
  import mime from "mime-types";
5
5
  // nodefony/src/context/http/Response.ts
6
+ /**
7
+ * Plafond, en octets, d'un corps de requête HTTP/1.1 non lu que le serveur
8
+ * accepte de vider pour garder la connexion réutilisable — 256 Kio, le geste du
9
+ * serveur HTTP de Go que Connect reprend (ADR-0026, D5). Au-delà, la réponse
10
+ * ferme la connexion.
11
+ */
12
+ const UNREAD_BODY_DRAIN_LIMIT = 262144;
13
+ /**
14
+ * Silence, en millisecondes, au bout duquel une connexion HTTP/1.1 fermée sur
15
+ * un corps non lu est détruite — 5 s, le `lingering_timeout` par défaut de
16
+ * nginx. La durée TOTALE reste bornée par le délai de réception de Node
17
+ * (`requestTimeout`), qui court tant que le corps n'est pas fini.
18
+ */
19
+ const UNREAD_BODY_LINGER_MS = 5e3;
20
+ /**
21
+ * Fermeture ATTARDÉE (RFC 9112 §9.6) d'une connexion qui part en
22
+ * `connection: close` avec un corps encore en route : la réponse écrite, le
23
+ * serveur ferme son côté écriture et continue de LIRE — Node jette les octets
24
+ * du corps non consommé — jusqu'à ce que le client ferme, ou après
25
+ * {@link UNREAD_BODY_LINGER_MS} de silence.
26
+ *
27
+ * Sans elle, Node détruit la socket aussitôt la réponse écrite
28
+ * (`destroySoon`) : le client qui écrit encore son corps prend une
29
+ * réinitialisation (`EPIPE`, `ECONNRESET`) et ne lit jamais la réponse — un
30
+ * 413 sur un envoi de 5 Mo devenait une erreur réseau. Posée sur CETTE socket
31
+ * seulement, qui ne sert plus d'autre requête.
32
+ *
33
+ * @param socket - la socket de la connexion qui se ferme.
34
+ */
35
+ function lingerBeforeClose(socket) {
36
+ socket.destroySoon = () => {
37
+ socket.setTimeout(UNREAD_BODY_LINGER_MS, () => socket.destroy());
38
+ socket.end();
39
+ };
40
+ }
6
41
  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
42
  const stripAnsi = function(val) {
8
43
  return typeof val === "string" ? val.replace(ANSI_REGEX, "") : val;
@@ -15,6 +50,40 @@ const REDIRECT_STATUS_CODES = /* @__PURE__ */ new Set([
15
50
  308
16
51
  ]);
17
52
  const NO_CONTENT_LENGTH_STATUS = /* @__PURE__ */ new Set([204, 304]);
53
+ const NO_CONTENT_STATUS = /* @__PURE__ */ new Set([
54
+ 204,
55
+ 205,
56
+ 304
57
+ ]);
58
+ /**
59
+ * Vrai pour un statut dont la réponse n'a, par définition, aucun contenu —
60
+ * 204, 205 et 304 (RFC 9110 §15.3.5, §15.3.6, §15.4.5).
61
+ *
62
+ * Seule source de cette liste : la réponse s'en sert pour ne jamais écrire de
63
+ * corps, le résolveur pour terminer une action qui ne rend rien.
64
+ *
65
+ * @param status - le code de statut de la réponse.
66
+ * @returns `true` quand aucun corps ne doit partir.
67
+ */
68
+ function isContentlessStatus(status) {
69
+ return NO_CONTENT_STATUS.has(status);
70
+ }
71
+ /** Espace ou tabulation : l'OWS de RFC 9110 §5.6.3. */
72
+ const isOws = (code) => code === 32 || code === 9;
73
+ /**
74
+ * Valeur d'en-tête sans espace de bord (RFC 9110 §5.5 ; RFC 9113 §8.2.1 en fait
75
+ * un MUST NOT en HTTP/2). Ne copie la chaîne que dans le cas rare où il y a
76
+ * quelque chose à rogner.
77
+ */
78
+ function trimFieldValue(value) {
79
+ const last = value.length - 1;
80
+ if (last < 0 || !isOws(value.charCodeAt(0)) && !isOws(value.charCodeAt(last))) return value;
81
+ let start = 0;
82
+ let end = last;
83
+ while (start <= end && isOws(value.charCodeAt(start))) start++;
84
+ while (end >= start && isOws(value.charCodeAt(end))) end--;
85
+ return value.slice(start, end + 1);
86
+ }
18
87
  const CONTENT_TYPE_CACHE_MAX = 64;
19
88
  const contentTypeCache = /* @__PURE__ */ new Map();
20
89
  /**
@@ -38,6 +107,26 @@ function resolveContentType(type) {
38
107
  if (contentTypeCache.size < CONTENT_TYPE_CACHE_MAX) contentTypeCache.set(type, resolved);
39
108
  return resolved;
40
109
  }
110
+ /**
111
+ * Valeur d'en-tête sous la forme que Node accepte (une liste figée est copiée),
112
+ * rognée de ses espaces de bord.
113
+ */
114
+ function toHeaderValue(value) {
115
+ if (typeof value === "number") return value;
116
+ if (typeof value === "string") return trimFieldValue(value);
117
+ return value.map(trimFieldValue);
118
+ }
119
+ /**
120
+ * Contrôle d'un en-tête comme le fait `setHeader` natif — nom, puis chaque
121
+ * valeur (un nombre est toujours valide).
122
+ *
123
+ * @throws TypeError `ERR_INVALID_HTTP_TOKEN` / `ERR_INVALID_CHAR`.
124
+ */
125
+ function assertHeader(name, value) {
126
+ validateHeaderName(name);
127
+ if (typeof value === "string") validateHeaderValue(name, value);
128
+ else if (typeof value !== "number") for (const item of value) validateHeaderValue(name, item);
129
+ }
41
130
  var HttpResponse = class {
42
131
  context;
43
132
  response;
@@ -46,12 +135,38 @@ var HttpResponse = class {
46
135
  flushing = false;
47
136
  encoding = "utf-8";
48
137
  #text = null;
49
- #textEncoding = "utf-8";
138
+ #textEncoding = "utf8";
50
139
  #buffer = null;
51
140
  contentType = "application/octet-stream";
52
141
  headers = {};
53
142
  timeout;
54
143
  cookies = {};
144
+ /**
145
+ * En-têtes posés, remis TELS QUELS en un seul `writeHead(statut, liste)` :
146
+ * un `setHeader` natif par en-tête coûtait validation, rangement dans la
147
+ * table de Node puis relecture au `writeHead` — 41 % de plus pour le même
148
+ * bloc (`micro-write-head.mjs`). Et un seul `setHeader` natif avant le
149
+ * `writeHead` suffit à faire retomber Node sur son chemin lent.
150
+ * `null` tant que rien n'est posé ; amorcée avec les en-têtes du transport.
151
+ */
152
+ #headers = null;
153
+ /**
154
+ * Pare-feu qui posera les en-têtes de sécurité applicatifs juste avant
155
+ * l'émission des en-têtes, une fois le type de la réponse fixé : la politique
156
+ * suit ce type (ADR-0024 §C11). Armé par le noyau HTTP au routage, consommé
157
+ * une fois ; `null` sans module de sécurité.
158
+ */
159
+ headerPolicy = null;
160
+ /**
161
+ * Vrai une fois la réserve recopiée sur la réponse Node ({@link commitHeaders}) :
162
+ * le transport y est déjà, il ne s'amorce plus.
163
+ */
164
+ #committed = false;
165
+ /**
166
+ * Méthode `setHeader` native retenue pendant que l'interception du repli
167
+ * statique est armée ; `null` sinon (et sur toute réponse routée).
168
+ */
169
+ #nativeSetHeader = null;
55
170
  constructor(response, context) {
56
171
  this.context = context;
57
172
  this.response = response;
@@ -68,16 +183,16 @@ var HttpResponse = class {
68
183
  * contenu (RFC 9110 §8.3), et il n'y en a pas.
69
184
  */
70
185
  ensureContentTypeHeader() {
71
- if (this.response && !this.response.hasHeader("content-type") && !this.#isBodyless()) this.response.setHeader("content-type", this.contentType);
186
+ if (this.response && !this.hasHeader("content-type") && !this.#isBodyless()) this.setHeader("content-type", this.contentType);
72
187
  }
73
188
  /**
74
- * Vrai quand la réponse n'a pas de corps : 204 et 304 n'en ont jamais
75
- * (RFC 9110 §15.3.5, §15.4.5) ; ailleurs, un corps posé et VIDE. Un corps
76
- * jamais posé ne dit rien — un flux peut suivre l'en-tête — et `HEAD` rend
77
- * les en-têtes du `GET` (§9.3.2) : tous deux gardent le défaut.
189
+ * Vrai quand la réponse n'a pas de corps : 204, 205 et 304 n'en ont jamais
190
+ * (RFC 9110 §15.3.5, §15.3.6, §15.4.5) ; ailleurs, un corps posé et VIDE. Un
191
+ * corps jamais posé ne dit rien — un flux peut suivre l'en-tête — et `HEAD`
192
+ * rend les en-têtes du `GET` (§9.3.2) : tous deux gardent le défaut.
78
193
  */
79
194
  #isBodyless() {
80
- if (NO_CONTENT_LENGTH_STATUS.has(this.statusCode)) return true;
195
+ if (NO_CONTENT_STATUS.has(this.statusCode)) return true;
81
196
  if (this.context.method === "HEAD") return false;
82
197
  if (this.#text !== null) return this.#text === "" && !this.flushing;
83
198
  return this.#buffer?.length === 0 && !this.flushing;
@@ -100,9 +215,12 @@ var HttpResponse = class {
100
215
  /**
101
216
  * Corps tel qu'il part sur le fil : le texte s'il n'a jamais été encodé, sinon
102
217
  * les octets — vide légal si rien n'a été posé (`write(null)` jetterait
103
- * ERR_STREAM_NULL_VALUES).
218
+ * ERR_STREAM_NULL_VALUES). Vide aussi sous un statut sans contenu : un corps
219
+ * posé par l'application sous un 205 ne part pas (RFC 9110 §15.3.6) — Node
220
+ * ne l'écarte de lui-même que pour 204 et 304.
104
221
  */
105
222
  get payload() {
223
+ if (NO_CONTENT_STATUS.has(this.statusCode)) return "";
106
224
  return this.#text ?? this.#buffer ?? "";
107
225
  }
108
226
  /** Encodage à passer avec {@link payload} (ignoré par Node pour des octets). */
@@ -113,6 +231,7 @@ var HttpResponse = class {
113
231
  this.response = null;
114
232
  this.body = null;
115
233
  this.cookies = {};
234
+ this.#headers = null;
116
235
  }
117
236
  isHeaderSent() {
118
237
  if (this.response) return this.response.headersSent;
@@ -126,7 +245,10 @@ var HttpResponse = class {
126
245
  this.timeout = ms;
127
246
  }
128
247
  addCookie(cookie) {
129
- if (cookie instanceof Cookie) return this.cookies[cookie.name] = cookie;
248
+ if (cookie instanceof Cookie) {
249
+ cookie.assertGrammar();
250
+ return this.cookies[cookie.name] = cookie;
251
+ }
130
252
  throw new Error("Response addCookies not valid cookies");
131
253
  }
132
254
  deleteCookie(cookie) {
@@ -150,19 +272,22 @@ var HttpResponse = class {
150
272
  const cookies = Object.values(this.cookies);
151
273
  const first = cookies.at(0);
152
274
  if (first === void 0) return;
153
- if (cookies.length === 1) return this.setCookie(first);
275
+ if (cookies.length === 1) {
276
+ this.setCookie(first);
277
+ return;
278
+ }
154
279
  const serialized = [];
155
280
  for (const cookie of cookies) {
156
281
  const s = cookie.serialize();
157
282
  this.logCookie(cookie, s);
158
283
  serialized.push(s);
159
284
  }
160
- return this.setHeader("Set-Cookie", serialized);
285
+ this.setHeader("Set-Cookie", serialized);
161
286
  }
162
287
  setCookie(cookie) {
163
288
  const serialize = cookie.serialize();
164
289
  this.logCookie(cookie, serialize);
165
- return this.setHeader("Set-Cookie", serialize);
290
+ this.setHeader("Set-Cookie", serialize);
166
291
  }
167
292
  /**
168
293
  * Journalise en DEBUG un cookie posé, sa VALEUR MASQUÉE : nom et attributs
@@ -181,20 +306,219 @@ var HttpResponse = class {
181
306
  const attrs = serialized.indexOf("; ", cookie.name.length + 1);
182
307
  this.log(`ADD COOKIE ==> ${cookie.name}=<masqué>${attrs === -1 ? "" : serialized.slice(attrs)}`, "DEBUG", msgid);
183
308
  }
309
+ /**
310
+ * Pose un en-tête de la réponse ; en flux chunké déjà ouvert, le range en
311
+ * trailer. Le nom et la valeur sont VALIDÉS ici, comme le ferait Node : une
312
+ * valeur invalide (saut de ligne…) lève tout de suite et n'entre jamais dans
313
+ * la réserve — sinon elle lèverait au `writeHead`, et la page d'erreur
314
+ * rejouerait le même en-tête.
315
+ *
316
+ * @param name - nom de l'en-tête (casse indifférente).
317
+ * @param value - valeur ; une liste émet une ligne par élément (`Set-Cookie`).
318
+ * @throws TypeError `ERR_INVALID_HTTP_TOKEN` / `ERR_INVALID_CHAR` sur un nom
319
+ * ou une valeur qu'un en-tête HTTP ne peut pas porter.
320
+ */
184
321
  setHeader(name, value) {
185
322
  if (this.response) {
186
323
  if (this.flushing) {
187
324
  const obj = {};
188
- obj[name] = value;
325
+ obj[name] = toHeaderValue(value);
189
326
  this.addTrailers(obj);
190
327
  return;
191
328
  }
192
329
  if (!this.response.headersSent) {
330
+ assertHeader(name, value);
193
331
  const lower = name.toLowerCase();
194
- if (lower === "vary") return this.response.setHeader(lower, this.#mergeVary(value));
195
- return this.response.setHeader(lower, value);
332
+ this.#put(lower, lower === "vary" ? this.#mergeVary(value) : toHeaderValue(value));
333
+ }
334
+ }
335
+ }
336
+ /**
337
+ * Pose un en-tête dont la valeur a DÉJÀ été validée par l'appelant — une
338
+ * table figée et contrôlée au démarrage (en-têtes de sécurité). Node la
339
+ * validera une fois, au `writeHead` ; la revalider ici effaçait le gain de
340
+ * l'écriture groupée (`micro-write-head.mjs`, ligne « plat validé »).
341
+ *
342
+ * ⚠️ Jamais pour une valeur venue d'une requête : elle passe par {@link setHeader}.
343
+ *
344
+ * @param name - nom de l'en-tête (casse indifférente).
345
+ * @param value - valeur contrôlée au démarrage.
346
+ */
347
+ setValidatedHeader(name, value) {
348
+ if (this.response === null || this.response.headersSent) return;
349
+ if (this.flushing) {
350
+ this.setHeader(name, value);
351
+ return;
352
+ }
353
+ this.#put(name.toLowerCase(), value);
354
+ }
355
+ /**
356
+ * Pose un en-tête validé au démarrage SAUF si la réponse en porte déjà un :
357
+ * la valeur du framework est un défaut, celle que l'application a écrite
358
+ * gagne. Même coût que {@link setValidatedHeader} — le rangement cherche
359
+ * l'en-tête de toute façon.
360
+ *
361
+ * @param name - nom de l'en-tête (casse indifférente).
362
+ * @param value - valeur contrôlée au démarrage.
363
+ */
364
+ setDefaultHeader(name, value) {
365
+ if (this.response === null || this.response.headersSent) return;
366
+ const lower = name.toLowerCase();
367
+ if (this.flushing) {
368
+ if (!this.hasHeader(lower)) this.setHeader(name, value);
369
+ return;
370
+ }
371
+ if (this.#indexOf(lower) === -1) this.#store().push(lower, value);
372
+ }
373
+ /**
374
+ * Pose les en-têtes de la politique armée ({@link headerPolicy}) pour une
375
+ * réponse de type `contentType` — une fois, juste avant l'émission.
376
+ *
377
+ * Un `304` n'en reçoit aucun : ses en-têtes REMPLACENT ceux de la
378
+ * représentation en cache (RFC 9111 §4.3.4). Une politique courte y
379
+ * réduirait la page gardée, un nonce neuf la désaccorderait de ses balises.
380
+ *
381
+ * @param contentType - type de la réponse ; `null` = sans contenu ; `undefined`
382
+ * = inconnu (politique complète).
383
+ * @param servedAsFile - corps écrit par un tiers (fichier statique, relais),
384
+ * qui ne peut pas porter de nonce.
385
+ */
386
+ applyHeaderPolicy(contentType, servedAsFile = false) {
387
+ const policy = this.headerPolicy;
388
+ if (policy === null) return;
389
+ this.headerPolicy = null;
390
+ if (this.statusCode === 304) return;
391
+ policy.applySecurityHeaders(this.context, contentType, servedAsFile);
392
+ }
393
+ /**
394
+ * Type qui choisit la politique : `null` pour une réponse sans contenu (204,
395
+ * corps vide) — même si un `Content-Type` y a été posé, rien n'y sera rendu ;
396
+ * sinon le `Content-Type` posé.
397
+ */
398
+ contentTypeForPolicy() {
399
+ if (this.#isBodyless()) return null;
400
+ const value = this.getHeader("content-type");
401
+ return typeof value === "string" ? value : null;
402
+ }
403
+ /**
404
+ * La réserve, amorcée au premier usage avec les en-têtes du transport
405
+ * (`Server`, `X-Content-Type-Options`, `X-Frame-Options`, HSTS) que le
406
+ * noyau a figés au démarrage — posés EN TÊTE, comme à l'entrée de la requête.
407
+ */
408
+ #store() {
409
+ if (this.#headers === null) {
410
+ const transport = this.#committed ? null : this.context.httpKernel?.transportHeaders(this.context.serverType);
411
+ this.#headers = transport ? [...transport] : [];
412
+ }
413
+ return this.#headers;
414
+ }
415
+ /** Position du nom (minuscules) dans la réserve, ou -1. */
416
+ #indexOf(lower) {
417
+ const list = this.#headers;
418
+ if (list === null) return -1;
419
+ for (let i = 0; i < list.length; i += 2) if (list[i] === lower) return i;
420
+ return -1;
421
+ }
422
+ /** Range un en-tête, en remplaçant sa valeur s'il est déjà posé. */
423
+ #put(lower, value) {
424
+ const list = this.#store();
425
+ const at = this.#indexOf(lower);
426
+ if (at === -1) list.push(lower, value);
427
+ else list[at + 1] = value;
428
+ }
429
+ /**
430
+ * Range les en-têtes de corrélation du contexte — `X-Request-Id` et le
431
+ * `traceparent` W3C — que toute réponse renvoie, quel qu'en soit l'émetteur.
432
+ * Validés par Node à la remise, comme le reste de la réserve.
433
+ */
434
+ putCorrelationHeaders() {
435
+ if (this.context.requestId) this.#put("x-request-id", this.context.requestId);
436
+ if (this.context.traceparent) this.#put("traceparent", this.context.traceparent);
437
+ }
438
+ /**
439
+ * Recopie la réserve sur la réponse Node, pour un émetteur qui écrit sans
440
+ * passer par {@link writeHead} — fichier statique, relais vers un amont,
441
+ * adaptateur RPC. La réserve se vide ; ce qui se pose ensuite y retourne, et
442
+ * Node fusionne les deux au `writeHead` (l'émetteur l'emporte, nom par nom).
443
+ *
444
+ * @param contentType - type de la réponse que l'émetteur écrira, quand il
445
+ * est connu d'avance (adaptateur RPC). Omis : il se lit à l'extension du
446
+ * chemin, comme le serveur statique le fera.
447
+ */
448
+ commitHeaders(contentType) {
449
+ const res = this.response;
450
+ if (res === null || res.headersSent) return;
451
+ if (this.headerPolicy !== null) {
452
+ let type = contentType;
453
+ if (type === void 0) {
454
+ const pathname = this.context.request.pathname;
455
+ const found = pathname.endsWith("/") ? false : mime.lookup(pathname);
456
+ if (found !== false) type = found;
196
457
  }
458
+ this.applyHeaderPolicy(type, true);
197
459
  }
460
+ this.putCorrelationHeaders();
461
+ const list = this.#store();
462
+ for (let i = 0; i + 1 < list.length; i += 2) {
463
+ const name = list[i];
464
+ const value = list[i + 1];
465
+ if (typeof name === "string" && value !== void 0) res.setHeader(name, value);
466
+ }
467
+ this.#headers = null;
468
+ this.#committed = true;
469
+ }
470
+ /**
471
+ * Recopie la réserve sur la réponse Node au PREMIER `setHeader` qu'un tiers
472
+ * y fera — fichier statique servi, redirection de dossier — et seulement
473
+ * alors : un raté du repli statique (la 404 d'une route inconnue) ne recopie
474
+ * rien, sa réponse part ensuite en un seul `writeHead`. À désarmer par
475
+ * {@link disarmNativeWrite} quand le tiers rend la main sans avoir écrit.
476
+ *
477
+ * L'interception est une propriété PROPRE de la réponse, posée une fois avec
478
+ * des attributs constants — toutes les réponses interceptées partagent donc
479
+ * la même forme — et désarmée en lui RÉAFFECTANT la méthode native, jamais
480
+ * en la supprimant : un `delete` fait basculer la réponse Node en mode
481
+ * dictionnaire (constaté par `%HasFastProperties` sur Node 26, 200 réponses
482
+ * sur 200), et chaque accès suivant de `node:http` le paie — mesuré in situ
483
+ * sur un 404 : `writeHead` +1,1 µs, `end` +4,3 µs.
484
+ */
485
+ commitBeforeNativeWrite() {
486
+ const res = this.response;
487
+ if (res === null || res.headersSent || this.#nativeSetHeader !== null) return;
488
+ const native = res.setHeader;
489
+ this.#nativeSetHeader = native;
490
+ Object.defineProperty(res, "setHeader", {
491
+ configurable: true,
492
+ enumerable: false,
493
+ writable: true,
494
+ value: (name, value) => {
495
+ this.disarmNativeWrite();
496
+ this.commitHeaders();
497
+ return Reflect.apply(native, res, [name, value]);
498
+ }
499
+ });
500
+ }
501
+ /**
502
+ * Retire l'interception posée par {@link commitBeforeNativeWrite} : la
503
+ * propriété propre reçoit la méthode native — la forme de l'objet ne change
504
+ * pas, la réponse reste en mode rapide pour V8.
505
+ */
506
+ disarmNativeWrite() {
507
+ const native = this.#nativeSetHeader;
508
+ if (native === null) return;
509
+ this.#nativeSetHeader = null;
510
+ const res = this.response;
511
+ if (res !== null) Object.defineProperty(res, "setHeader", { value: native });
512
+ }
513
+ /**
514
+ * Retire un en-tête, de la réserve comme de la réponse Node.
515
+ *
516
+ * @param name - nom de l'en-tête (casse indifférente).
517
+ */
518
+ removeHeader(name) {
519
+ const at = this.#indexOf(name.toLowerCase());
520
+ if (at !== -1) this.#headers?.splice(at, 2);
521
+ if (this.response && !this.response.headersSent) this.response.removeHeader(name);
198
522
  }
199
523
  /**
200
524
  * Union des tokens `Vary` déjà posés et de ceux qu'on ajoute, sans doublon.
@@ -207,7 +531,7 @@ var HttpResponse = class {
207
531
  */
208
532
  #mergeVary(value) {
209
533
  const incoming = (Array.isArray(value) ? value.join(",") : String(value)).split(",").map((t) => t.trim()).filter(Boolean);
210
- const current = this.response?.getHeader("vary");
534
+ const current = this.getHeader("vary");
211
535
  const all = [...(Array.isArray(current) ? current.join(",") : String(current ?? "")).split(",").map((t) => t.trim()).filter(Boolean), ...incoming];
212
536
  if (all.some((t) => t === "*")) return "*";
213
537
  const seen = /* @__PURE__ */ new Set();
@@ -226,10 +550,10 @@ var HttpResponse = class {
226
550
  const value = obj[head];
227
551
  if (value !== void 0) this.setHeader(head, value);
228
552
  }
229
- return this.headers = this.response?.getHeaders();
553
+ return this.headers = this.getHeaders();
230
554
  }
231
555
  this.log("headers already sended ", "WARNING");
232
- return this.headers = this.response.getHeaders();
556
+ return this.headers = this.getHeaders();
233
557
  }
234
558
  setContentType(type, encoding) {
235
559
  if (type && encoding) {
@@ -238,8 +562,12 @@ var HttpResponse = class {
238
562
  const mytype = resolved.bare;
239
563
  this.contentType = mytype;
240
564
  this.encoding = encoding;
241
- if (mytype === "application/json" || mytype.endsWith("+json")) return this.setHeader("Content-Type", mytype);
242
- return this.setHeader("Content-Type", `${mytype}; charset=${encoding}`);
565
+ if (mytype === "application/json" || mytype.endsWith("+json")) {
566
+ this.setHeader("Content-Type", mytype);
567
+ return;
568
+ }
569
+ this.setHeader("Content-Type", `${mytype}; charset=${encoding}`);
570
+ return;
243
571
  }
244
572
  }
245
573
  if (type && !encoding) {
@@ -249,10 +577,11 @@ var HttpResponse = class {
249
577
  this.contentType = mytype;
250
578
  const charset = resolved.charset;
251
579
  if (charset) this.encoding = charset;
252
- return this.setHeader("Content-Type", mytype);
580
+ this.setHeader("Content-Type", mytype);
581
+ return;
253
582
  }
254
583
  }
255
- return this.setHeader("Content-Type", `${this.contentType}; charset=${this.encoding}`);
584
+ this.setHeader("Content-Type", `${this.contentType}; charset=${this.encoding}`);
256
585
  }
257
586
  setFileMimeType(type, encoding) {
258
587
  let myType = this.getMimeType(type);
@@ -261,7 +590,7 @@ var HttpResponse = class {
261
590
  myType = "application/octet-stream";
262
591
  }
263
592
  this.contentType = myType;
264
- return this.setContentType(myType, encoding || this.encoding);
593
+ this.setContentType(myType, encoding || this.encoding);
265
594
  }
266
595
  setContentTypeByExtension(extention) {
267
596
  const ismime = mime.contentType(extention);
@@ -269,7 +598,8 @@ var HttpResponse = class {
269
598
  this.contentType = ismime;
270
599
  let charset = mime.charset(this.contentType);
271
600
  if (charset) this.encoding = charset;
272
- return this.setHeader("Content-Type", ismime);
601
+ this.setHeader("Content-Type", ismime);
602
+ return;
273
603
  }
274
604
  this.log(`setContentTypeByExtension: ${extention} not found`, "WARNING");
275
605
  }
@@ -337,19 +667,23 @@ var HttpResponse = class {
337
667
  #setText(text, encoding) {
338
668
  this.#buffer = null;
339
669
  this.#text = text;
340
- this.#textEncoding = encoding;
670
+ this.#textEncoding = encoding === "utf-8" ? "utf8" : encoding;
341
671
  }
342
672
  setLength(body) {
343
673
  if (this.response?.headersSent) throw new Error("Headers already sended");
344
674
  if (this.getHeader("Transfer-Encoding") === "chunked") {
345
- this.response?.removeHeader("Content-Length");
675
+ this.removeHeader("Content-Length");
346
676
  return 0;
347
677
  }
348
678
  if (NO_CONTENT_LENGTH_STATUS.has(this.statusCode)) return 0;
679
+ if (this.statusCode === 205) {
680
+ this.#put("content-length", "0");
681
+ return 0;
682
+ }
349
683
  const actualBody = body || this.#text || this.#buffer;
350
684
  if (actualBody) {
351
685
  const length = typeof actualBody === "string" && actualBody === this.#text ? Buffer.byteLength(actualBody, this.#textEncoding) : Buffer.byteLength(actualBody);
352
- this.setHeader("Content-Length", String(length));
686
+ this.#put("content-length", String(length));
353
687
  return length;
354
688
  }
355
689
  return 0;
@@ -362,27 +696,68 @@ var HttpResponse = class {
362
696
  if (this.statusCode > 599) this.statusCode = 500;
363
697
  }
364
698
  this.statusMessage = this.getStatusMessage();
365
- if (this.context.requestId) this.response.setHeader("x-request-id", this.context.requestId);
366
- if (this.context.traceparent) this.response.setHeader("traceparent", this.context.traceparent);
699
+ if (headers) this.#apply(headers);
700
+ this.putCorrelationHeaders();
367
701
  this.setLength();
368
702
  this.ensureContentTypeHeader();
703
+ this.applyHeaderPolicy(this.contentTypeForPolicy());
704
+ const list = this.#store();
369
705
  const std = http.STATUS_CODES[this.statusCode];
370
- if (this.statusMessage === std) this.response.writeHead(this.statusCode, headers);
706
+ if (this.statusMessage === std) this.response.writeHead(this.statusCode, list);
371
707
  else {
372
708
  const safeMsg = this.statusMessage.replace(/[^\x20-\x7E]/g, "").trim() || (std ?? "Unknown Error");
373
- this.response.writeHead(this.statusCode, safeMsg, headers);
709
+ this.response.writeHead(this.statusCode, safeMsg, list);
374
710
  }
375
711
  } else {
376
712
  this.log("Headers already sent !!", "WARNING");
377
713
  throw new Error(`Headers already sent !!`);
378
714
  }
379
715
  }
716
+ /**
717
+ * Range dans la réserve les en-têtes passés à `writeHead` — ils l'emportent
718
+ * sur ceux déjà posés, comme chez Node.
719
+ */
720
+ #apply(headers) {
721
+ if (Array.isArray(headers)) {
722
+ for (let i = 0; i + 1 < headers.length; i += 2) {
723
+ const value = headers[i + 1];
724
+ if (value !== void 0) this.setHeader(String(headers[i]), value);
725
+ }
726
+ return;
727
+ }
728
+ for (const name in headers) {
729
+ const value = headers[name];
730
+ if (value !== void 0) this.setHeader(name, value);
731
+ }
732
+ }
733
+ /** La réserve amorcée — pour une remise sous une autre forme (HTTP/2). */
734
+ headerList() {
735
+ return this.#store();
736
+ }
380
737
  addTrailers(headers) {
381
738
  return this.response?.addTrailers(headers);
382
739
  }
740
+ /**
741
+ * Écrit un morceau d'une réponse en flux, sans la terminer — c'est `end()`
742
+ * qui la clôt après le dernier.
743
+ *
744
+ * Au premier morceau, les en-têtes partent SANS longueur ni corps posé (le
745
+ * patron de `SseStream`), et le cadrage revient à Node : `chunked` pour un
746
+ * client HTTP/1.1, fermeture de connexion pour un HTTP/1.0 (RFC 9112 §6.1 —
747
+ * pas de `Transfer-Encoding` à qui ne le connaît pas), trames DATA en HTTP/2
748
+ * où l'en-tête est interdit (RFC 9113 §8.2.2). Ne JAMAIS le poser à la main :
749
+ * une fois `flushing` levé, `setHeader` range en trailer, et un
750
+ * `Transfer-Encoding` en trailer est un message malformé (RFC 9110 §6.5.1).
751
+ *
752
+ * @param chunk - le morceau à écrire.
753
+ * @param encoding - l'encodage d'un morceau texte.
754
+ * @returns la promesse réglée quand le flux accepte l'écriture suivante.
755
+ */
383
756
  flush(chunk, encoding) {
384
- this.flushing = true;
385
- this.setHeader("Transfer-Encoding", "chunked");
757
+ if (!this.flushing) {
758
+ if (!this.isHeaderSent()) this.writeHead();
759
+ this.flushing = true;
760
+ }
386
761
  return this.send(chunk, encoding, true);
387
762
  }
388
763
  /**
@@ -445,15 +820,62 @@ var HttpResponse = class {
445
820
  if (!this.response) throw new Error(`response not found`);
446
821
  return this.response.end(chunk, encoding || this.encoding);
447
822
  }
823
+ /**
824
+ * Règle le sort d'un corps de requête resté NON LU au moment de répondre —
825
+ * route inconnue (404, 405), refus avant l'action, 408, 413. À appeler AVANT
826
+ * l'écriture des en-têtes ; sans effet si le corps est entièrement reçu.
827
+ *
828
+ * HTTP/1.1 : Node vide lui-même un corps non consommé une fois la réponse
829
+ * écrite, pour réutiliser la connexion — sans borne. On ne le laisse faire
830
+ * que pour un corps DÉCLARÉ d'au plus {@link UNREAD_BODY_DRAIN_LIMIT}
831
+ * octets ; au-delà, ou sans longueur déclarée (`transfer-encoding`), la
832
+ * réponse part avec `connection: close` (RFC 9112 §9.6) et la connexion
833
+ * n'est pas réutilisée — fermée par étapes ({@link lingerBeforeClose}),
834
+ * pour que le client qui écrit encore lise la réponse. La décision se prend
835
+ * sur la longueur ANNONCÉE, sans attendre un octet : la réponse n'est jamais
836
+ * retardée par le corps.
837
+ *
838
+ * HTTP/2 surcharge cette méthode (`Http2Response`) : fermeture du flux en
839
+ * `NO_ERROR` une fois la réponse écrite. Une seule entrée pour les deux
840
+ * protocoles, partagée par tout refus qui laisse un corps non lu (ADR-0026,
841
+ * D5).
842
+ */
843
+ closeUnfinishedRequest() {
844
+ const response = this.response;
845
+ if (response === null || response.headersSent) return;
846
+ if (this.context.request.request.complete) return;
847
+ const headers = this.context.request.headers;
848
+ if (headers["transfer-encoding"] === void 0) {
849
+ if (Number(headers["content-length"] ?? 0) <= 262144) return;
850
+ }
851
+ this.setHeader("connection", "close");
852
+ const socket = response.socket;
853
+ if (socket !== null) lingerBeforeClose(socket);
854
+ }
855
+ /**
856
+ * Valeur d'un en-tête posé — dans la réserve, sinon sur la réponse Node (ce
857
+ * qu'un tiers y a écrit directement : `send`, quota).
858
+ *
859
+ * @param name - nom de l'en-tête (casse indifférente).
860
+ */
448
861
  getHeader(name) {
862
+ const at = this.#indexOf(name.toLowerCase());
863
+ if (at !== -1) return this.#headers?.[at + 1];
449
864
  return this.response?.getHeader(name);
450
865
  }
451
866
  hasHeader(name) {
452
- if (this.response) return this.response.hasHeader(name);
867
+ if (this.response) return this.#indexOf(name.toLowerCase()) !== -1 || this.response.hasHeader(name);
453
868
  throw new Error(`Respose not foud`);
454
869
  }
870
+ /** Tous les en-têtes posés, noms en minuscules — une copie, à chaque appel. */
455
871
  getHeaders() {
456
- return this.response?.getHeaders();
872
+ const out = { ...this.response?.getHeaders() };
873
+ const list = this.#headers;
874
+ if (list !== null) for (let i = 0; i + 1 < list.length; i += 2) {
875
+ const name = list[i];
876
+ if (typeof name === "string") out[name] = list[i + 1];
877
+ }
878
+ return out;
457
879
  }
458
880
  redirect(url, status, headers) {
459
881
  this.context.isRedirect = true;
@@ -481,4 +903,4 @@ var HttpResponse = class {
481
903
  return this.context.log(pci, severity, msgid, msg);
482
904
  }
483
905
  };
484
- export { HttpResponse as default };
906
+ export { UNREAD_BODY_DRAIN_LIMIT, UNREAD_BODY_LINGER_MS, HttpResponse as default, isContentlessStatus };