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

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 (103) hide show
  1. package/README.md +24 -0
  2. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorateParam.js +1 -1
  4. package/dist/index.js +29 -25
  5. package/dist/nodefony/command/proxyGenerateCommand.js +2 -2
  6. package/dist/nodefony/config/config.js +6 -3
  7. package/dist/nodefony/interfaces/IRequestTrace.js +1 -0
  8. package/dist/nodefony/service/ProfilerAdminApi.js +17 -3
  9. package/dist/nodefony/service/audit-logger.js +3 -17
  10. package/dist/nodefony/service/error-renderer.js +6 -4
  11. package/dist/nodefony/service/http-kernel.js +191 -40
  12. package/dist/nodefony/service/pretty-request-logger.js +11 -10
  13. package/dist/nodefony/service/request-logger.js +1 -1
  14. package/dist/nodefony/service/reverse-proxy.js +7 -2
  15. package/dist/nodefony/service/servers/clientError.js +28 -4
  16. package/dist/nodefony/service/servers/server-h2c.js +5 -5
  17. package/dist/nodefony/service/servers/server-http.js +5 -3
  18. package/dist/nodefony/service/servers/server-https.js +12 -5
  19. package/dist/nodefony/service/servers/server-static.js +1 -6
  20. package/dist/nodefony/service/servers/server-websocket-secure.js +6 -10
  21. package/dist/nodefony/service/servers/server-websocket.js +6 -10
  22. package/dist/nodefony/service/servers/serverShutdown.js +63 -4
  23. package/dist/nodefony/service/sessions/sessions-service.js +3 -3
  24. package/dist/nodefony/service/upload/upload-service.js +2 -2
  25. package/dist/nodefony/src/assets/prebuiltUi.js +23 -5
  26. package/dist/nodefony/src/context/Context.js +56 -13
  27. package/dist/nodefony/src/context/http/ByteStream.js +359 -0
  28. package/dist/nodefony/src/context/http/HttpContext.js +75 -25
  29. package/dist/nodefony/src/context/http/Request.js +15 -6
  30. package/dist/nodefony/src/context/http/Response.js +354 -35
  31. package/dist/nodefony/src/context/http/SseStream.js +81 -146
  32. package/dist/nodefony/src/context/http/StreamLifecycle.js +273 -0
  33. package/dist/nodefony/src/context/http/urlFastPath.js +80 -1
  34. package/dist/nodefony/src/context/http2/Request.js +1 -1
  35. package/dist/nodefony/src/context/http2/Response.js +28 -5
  36. package/dist/nodefony/src/context/requestTrace.js +104 -0
  37. package/dist/nodefony/src/context/timingMode.js +21 -0
  38. package/dist/nodefony/src/context/transport.js +20 -1
  39. package/dist/nodefony/src/context/unsentResponse.js +30 -0
  40. package/dist/nodefony/src/context/websocket/Response.js +4 -2
  41. package/dist/nodefony/src/context/websocket/WebsocketContext.js +81 -20
  42. package/dist/nodefony/src/context/websocket/websocketUrl.js +7 -0
  43. package/dist/nodefony/src/context/websocket/wsBackpressure.js +15 -15
  44. package/dist/nodefony/src/context/websocket/wsFrameRate.js +71 -0
  45. package/dist/nodefony/src/context/websocket/wsLogContent.js +55 -17
  46. package/dist/nodefony/src/cookies/cookie.js +54 -13
  47. package/dist/nodefony/src/profiler/FrameProfile.js +22 -0
  48. package/dist/nodefony/src/profiler/Profiler.js +25 -22
  49. package/dist/nodefony/src/profiler/aggregate.js +26 -6
  50. package/dist/nodefony/src/proxy/forward.js +109 -9
  51. package/dist/nodefony/src/proxy/generateProxyConfig.js +6 -2
  52. package/dist/nodefony/src/proxy/rules.js +29 -14
  53. package/dist/nodefony/src/servers/upgradeDispatch.js +62 -4
  54. package/dist/types/index.d.ts +8 -0
  55. package/dist/types/nodefony/config/config.d.ts +8 -0
  56. package/dist/types/nodefony/interfaces/IContext.d.ts +9 -0
  57. package/dist/types/nodefony/interfaces/IRequestTrace.d.ts +56 -0
  58. package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +8 -0
  59. package/dist/types/nodefony/interfaces/IRouting.d.ts +16 -0
  60. package/dist/types/nodefony/interfaces/ISecurity.d.ts +27 -2
  61. package/dist/types/nodefony/interfaces/index.d.ts +1 -0
  62. package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +5 -1
  63. package/dist/types/nodefony/service/audit-logger.d.ts +3 -11
  64. package/dist/types/nodefony/service/error-renderer.d.ts +5 -4
  65. package/dist/types/nodefony/service/http-kernel.d.ts +65 -2
  66. package/dist/types/nodefony/service/servers/clientError.d.ts +3 -2
  67. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +5 -3
  68. package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +9 -4
  69. package/dist/types/nodefony/src/context/Context.d.ts +25 -1
  70. package/dist/types/nodefony/src/context/http/ByteStream.d.ts +109 -0
  71. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +60 -4
  72. package/dist/types/nodefony/src/context/http/Request.d.ts +8 -1
  73. package/dist/types/nodefony/src/context/http/Response.d.ts +134 -8
  74. package/dist/types/nodefony/src/context/http/SseStream.d.ts +32 -7
  75. package/dist/types/nodefony/src/context/http/StreamLifecycle.d.ts +53 -0
  76. package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +40 -0
  77. package/dist/types/nodefony/src/context/http2/Response.d.ts +17 -3
  78. package/dist/types/nodefony/src/context/requestTrace.d.ts +64 -0
  79. package/dist/types/nodefony/src/context/timingMode.d.ts +23 -0
  80. package/dist/types/nodefony/src/context/transport.d.ts +17 -0
  81. package/dist/types/nodefony/src/context/unsentResponse.d.ts +16 -0
  82. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +41 -1
  83. package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +4 -0
  84. package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +12 -2
  85. package/dist/types/nodefony/src/context/websocket/wsFrameRate.d.ts +69 -0
  86. package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +16 -8
  87. package/dist/types/nodefony/src/cookies/cookie.d.ts +20 -2
  88. package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +15 -0
  89. package/dist/types/nodefony/src/profiler/Profiler.d.ts +27 -102
  90. package/dist/types/nodefony/src/profiler/aggregate.d.ts +23 -106
  91. package/dist/types/nodefony/src/proxy/forward.d.ts +30 -2
  92. package/dist/types/nodefony/src/proxy/rules.d.ts +20 -0
  93. package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +41 -1
  94. package/docs/byte-stream.md +371 -0
  95. package/docs/cookies.md +13 -3
  96. package/docs/diagnostics-channel.md +303 -0
  97. package/docs/index.md +14 -1
  98. package/docs/observabilite.md +94 -16
  99. package/docs/reverse-proxy.md +33 -19
  100. package/docs/servers.md +26 -15
  101. package/docs/session.md +3 -3
  102. package/docs/sse.md +27 -20
  103. package/package.json +16 -16
@@ -1,6 +1,6 @@
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
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");
@@ -15,6 +15,40 @@ const REDIRECT_STATUS_CODES = /* @__PURE__ */ new Set([
15
15
  308
16
16
  ]);
17
17
  const NO_CONTENT_LENGTH_STATUS = /* @__PURE__ */ new Set([204, 304]);
18
+ const NO_CONTENT_STATUS = /* @__PURE__ */ new Set([
19
+ 204,
20
+ 205,
21
+ 304
22
+ ]);
23
+ /**
24
+ * Vrai pour un statut dont la réponse n'a, par définition, aucun contenu —
25
+ * 204, 205 et 304 (RFC 9110 §15.3.5, §15.3.6, §15.4.5).
26
+ *
27
+ * Seule source de cette liste : la réponse s'en sert pour ne jamais écrire de
28
+ * corps, le résolveur pour terminer une action qui ne rend rien.
29
+ *
30
+ * @param status - le code de statut de la réponse.
31
+ * @returns `true` quand aucun corps ne doit partir.
32
+ */
33
+ function isContentlessStatus(status) {
34
+ return NO_CONTENT_STATUS.has(status);
35
+ }
36
+ /** Espace ou tabulation : l'OWS de RFC 9110 §5.6.3. */
37
+ const isOws = (code) => code === 32 || code === 9;
38
+ /**
39
+ * Valeur d'en-tête sans espace de bord (RFC 9110 §5.5 ; RFC 9113 §8.2.1 en fait
40
+ * un MUST NOT en HTTP/2). Ne copie la chaîne que dans le cas rare où il y a
41
+ * quelque chose à rogner.
42
+ */
43
+ function trimFieldValue(value) {
44
+ const last = value.length - 1;
45
+ if (last < 0 || !isOws(value.charCodeAt(0)) && !isOws(value.charCodeAt(last))) return value;
46
+ let start = 0;
47
+ let end = last;
48
+ while (start <= end && isOws(value.charCodeAt(start))) start++;
49
+ while (end >= start && isOws(value.charCodeAt(end))) end--;
50
+ return value.slice(start, end + 1);
51
+ }
18
52
  const CONTENT_TYPE_CACHE_MAX = 64;
19
53
  const contentTypeCache = /* @__PURE__ */ new Map();
20
54
  /**
@@ -38,6 +72,26 @@ function resolveContentType(type) {
38
72
  if (contentTypeCache.size < CONTENT_TYPE_CACHE_MAX) contentTypeCache.set(type, resolved);
39
73
  return resolved;
40
74
  }
75
+ /**
76
+ * Valeur d'en-tête sous la forme que Node accepte (une liste figée est copiée),
77
+ * rognée de ses espaces de bord.
78
+ */
79
+ function toHeaderValue(value) {
80
+ if (typeof value === "number") return value;
81
+ if (typeof value === "string") return trimFieldValue(value);
82
+ return value.map(trimFieldValue);
83
+ }
84
+ /**
85
+ * Contrôle d'un en-tête comme le fait `setHeader` natif — nom, puis chaque
86
+ * valeur (un nombre est toujours valide).
87
+ *
88
+ * @throws TypeError `ERR_INVALID_HTTP_TOKEN` / `ERR_INVALID_CHAR`.
89
+ */
90
+ function assertHeader(name, value) {
91
+ validateHeaderName(name);
92
+ if (typeof value === "string") validateHeaderValue(name, value);
93
+ else if (typeof value !== "number") for (const item of value) validateHeaderValue(name, item);
94
+ }
41
95
  var HttpResponse = class {
42
96
  context;
43
97
  response;
@@ -52,6 +106,27 @@ var HttpResponse = class {
52
106
  headers = {};
53
107
  timeout;
54
108
  cookies = {};
109
+ /**
110
+ * En-têtes posés, remis TELS QUELS en un seul `writeHead(statut, liste)` :
111
+ * un `setHeader` natif par en-tête coûtait validation, rangement dans la
112
+ * table de Node puis relecture au `writeHead` — 41 % de plus pour le même
113
+ * bloc (`micro-write-head.mjs`). Et un seul `setHeader` natif avant le
114
+ * `writeHead` suffit à faire retomber Node sur son chemin lent.
115
+ * `null` tant que rien n'est posé ; amorcée avec les en-têtes du transport.
116
+ */
117
+ #headers = null;
118
+ /**
119
+ * Pare-feu qui posera les en-têtes de sécurité applicatifs juste avant
120
+ * l'émission des en-têtes, une fois le type de la réponse fixé : la politique
121
+ * suit ce type (ADR-0024 §C11). Armé par le noyau HTTP au routage, consommé
122
+ * une fois ; `null` sans module de sécurité.
123
+ */
124
+ headerPolicy = null;
125
+ /**
126
+ * Vrai une fois la réserve recopiée sur la réponse Node ({@link commitHeaders}) :
127
+ * le transport y est déjà, il ne s'amorce plus.
128
+ */
129
+ #committed = false;
55
130
  constructor(response, context) {
56
131
  this.context = context;
57
132
  this.response = response;
@@ -68,16 +143,16 @@ var HttpResponse = class {
68
143
  * contenu (RFC 9110 §8.3), et il n'y en a pas.
69
144
  */
70
145
  ensureContentTypeHeader() {
71
- if (this.response && !this.response.hasHeader("content-type") && !this.#isBodyless()) this.response.setHeader("content-type", this.contentType);
146
+ if (this.response && !this.hasHeader("content-type") && !this.#isBodyless()) this.setHeader("content-type", this.contentType);
72
147
  }
73
148
  /**
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.
149
+ * Vrai quand la réponse n'a pas de corps : 204, 205 et 304 n'en ont jamais
150
+ * (RFC 9110 §15.3.5, §15.3.6, §15.4.5) ; ailleurs, un corps posé et VIDE. Un
151
+ * corps jamais posé ne dit rien — un flux peut suivre l'en-tête — et `HEAD`
152
+ * rend les en-têtes du `GET` (§9.3.2) : tous deux gardent le défaut.
78
153
  */
79
154
  #isBodyless() {
80
- if (NO_CONTENT_LENGTH_STATUS.has(this.statusCode)) return true;
155
+ if (NO_CONTENT_STATUS.has(this.statusCode)) return true;
81
156
  if (this.context.method === "HEAD") return false;
82
157
  if (this.#text !== null) return this.#text === "" && !this.flushing;
83
158
  return this.#buffer?.length === 0 && !this.flushing;
@@ -100,9 +175,12 @@ var HttpResponse = class {
100
175
  /**
101
176
  * Corps tel qu'il part sur le fil : le texte s'il n'a jamais été encodé, sinon
102
177
  * les octets — vide légal si rien n'a été posé (`write(null)` jetterait
103
- * ERR_STREAM_NULL_VALUES).
178
+ * ERR_STREAM_NULL_VALUES). Vide aussi sous un statut sans contenu : un corps
179
+ * posé par l'application sous un 205 ne part pas (RFC 9110 §15.3.6) — Node
180
+ * ne l'écarte de lui-même que pour 204 et 304.
104
181
  */
105
182
  get payload() {
183
+ if (NO_CONTENT_STATUS.has(this.statusCode)) return "";
106
184
  return this.#text ?? this.#buffer ?? "";
107
185
  }
108
186
  /** Encodage à passer avec {@link payload} (ignoré par Node pour des octets). */
@@ -113,6 +191,7 @@ var HttpResponse = class {
113
191
  this.response = null;
114
192
  this.body = null;
115
193
  this.cookies = {};
194
+ this.#headers = null;
116
195
  }
117
196
  isHeaderSent() {
118
197
  if (this.response) return this.response.headersSent;
@@ -126,7 +205,10 @@ var HttpResponse = class {
126
205
  this.timeout = ms;
127
206
  }
128
207
  addCookie(cookie) {
129
- if (cookie instanceof Cookie) return this.cookies[cookie.name] = cookie;
208
+ if (cookie instanceof Cookie) {
209
+ cookie.assertGrammar();
210
+ return this.cookies[cookie.name] = cookie;
211
+ }
130
212
  throw new Error("Response addCookies not valid cookies");
131
213
  }
132
214
  deleteCookie(cookie) {
@@ -150,19 +232,22 @@ var HttpResponse = class {
150
232
  const cookies = Object.values(this.cookies);
151
233
  const first = cookies.at(0);
152
234
  if (first === void 0) return;
153
- if (cookies.length === 1) return this.setCookie(first);
235
+ if (cookies.length === 1) {
236
+ this.setCookie(first);
237
+ return;
238
+ }
154
239
  const serialized = [];
155
240
  for (const cookie of cookies) {
156
241
  const s = cookie.serialize();
157
242
  this.logCookie(cookie, s);
158
243
  serialized.push(s);
159
244
  }
160
- return this.setHeader("Set-Cookie", serialized);
245
+ this.setHeader("Set-Cookie", serialized);
161
246
  }
162
247
  setCookie(cookie) {
163
248
  const serialize = cookie.serialize();
164
249
  this.logCookie(cookie, serialize);
165
- return this.setHeader("Set-Cookie", serialize);
250
+ this.setHeader("Set-Cookie", serialize);
166
251
  }
167
252
  /**
168
253
  * Journalise en DEBUG un cookie posé, sa VALEUR MASQUÉE : nom et attributs
@@ -181,22 +266,189 @@ var HttpResponse = class {
181
266
  const attrs = serialized.indexOf("; ", cookie.name.length + 1);
182
267
  this.log(`ADD COOKIE ==> ${cookie.name}=<masqué>${attrs === -1 ? "" : serialized.slice(attrs)}`, "DEBUG", msgid);
183
268
  }
269
+ /**
270
+ * Pose un en-tête de la réponse ; en flux chunké déjà ouvert, le range en
271
+ * trailer. Le nom et la valeur sont VALIDÉS ici, comme le ferait Node : une
272
+ * valeur invalide (saut de ligne…) lève tout de suite et n'entre jamais dans
273
+ * la réserve — sinon elle lèverait au `writeHead`, et la page d'erreur
274
+ * rejouerait le même en-tête.
275
+ *
276
+ * @param name - nom de l'en-tête (casse indifférente).
277
+ * @param value - valeur ; une liste émet une ligne par élément (`Set-Cookie`).
278
+ * @throws TypeError `ERR_INVALID_HTTP_TOKEN` / `ERR_INVALID_CHAR` sur un nom
279
+ * ou une valeur qu'un en-tête HTTP ne peut pas porter.
280
+ */
184
281
  setHeader(name, value) {
185
282
  if (this.response) {
186
283
  if (this.flushing) {
187
284
  const obj = {};
188
- obj[name] = value;
285
+ obj[name] = toHeaderValue(value);
189
286
  this.addTrailers(obj);
190
287
  return;
191
288
  }
192
289
  if (!this.response.headersSent) {
290
+ assertHeader(name, value);
193
291
  const lower = name.toLowerCase();
194
- if (lower === "vary") return this.response.setHeader(lower, this.#mergeVary(value));
195
- return this.response.setHeader(lower, value);
292
+ this.#put(lower, lower === "vary" ? this.#mergeVary(value) : toHeaderValue(value));
196
293
  }
197
294
  }
198
295
  }
199
296
  /**
297
+ * Pose un en-tête dont la valeur a DÉJÀ été validée par l'appelant — une
298
+ * table figée et contrôlée au démarrage (en-têtes de sécurité). Node la
299
+ * validera une fois, au `writeHead` ; la revalider ici effaçait le gain de
300
+ * l'écriture groupée (`micro-write-head.mjs`, ligne « plat validé »).
301
+ *
302
+ * ⚠️ Jamais pour une valeur venue d'une requête : elle passe par {@link setHeader}.
303
+ *
304
+ * @param name - nom de l'en-tête (casse indifférente).
305
+ * @param value - valeur contrôlée au démarrage.
306
+ */
307
+ setValidatedHeader(name, value) {
308
+ if (this.response === null || this.response.headersSent) return;
309
+ if (this.flushing) {
310
+ this.setHeader(name, value);
311
+ return;
312
+ }
313
+ this.#put(name.toLowerCase(), value);
314
+ }
315
+ /**
316
+ * Pose un en-tête validé au démarrage SAUF si la réponse en porte déjà un :
317
+ * la valeur du framework est un défaut, celle que l'application a écrite
318
+ * gagne. Même coût que {@link setValidatedHeader} — le rangement cherche
319
+ * l'en-tête de toute façon.
320
+ *
321
+ * @param name - nom de l'en-tête (casse indifférente).
322
+ * @param value - valeur contrôlée au démarrage.
323
+ */
324
+ setDefaultHeader(name, value) {
325
+ if (this.response === null || this.response.headersSent) return;
326
+ const lower = name.toLowerCase();
327
+ if (this.flushing) {
328
+ if (!this.hasHeader(lower)) this.setHeader(name, value);
329
+ return;
330
+ }
331
+ if (this.#indexOf(lower) === -1) this.#store().push(lower, value);
332
+ }
333
+ /**
334
+ * Pose les en-têtes de la politique armée ({@link headerPolicy}) pour une
335
+ * réponse de type `contentType` — une fois, juste avant l'émission.
336
+ *
337
+ * Un `304` n'en reçoit aucun : ses en-têtes REMPLACENT ceux de la
338
+ * représentation en cache (RFC 9111 §4.3.4). Une politique courte y
339
+ * réduirait la page gardée, un nonce neuf la désaccorderait de ses balises.
340
+ *
341
+ * @param contentType - type de la réponse ; `null` = sans contenu ; `undefined`
342
+ * = inconnu (politique complète).
343
+ * @param servedAsFile - corps écrit par un tiers (fichier statique, relais),
344
+ * qui ne peut pas porter de nonce.
345
+ */
346
+ applyHeaderPolicy(contentType, servedAsFile = false) {
347
+ const policy = this.headerPolicy;
348
+ if (policy === null) return;
349
+ this.headerPolicy = null;
350
+ if (this.statusCode === 304) return;
351
+ policy.applySecurityHeaders(this.context, contentType, servedAsFile);
352
+ }
353
+ /**
354
+ * Type qui choisit la politique : `null` pour une réponse sans contenu (204,
355
+ * corps vide) — même si un `Content-Type` y a été posé, rien n'y sera rendu ;
356
+ * sinon le `Content-Type` posé.
357
+ */
358
+ contentTypeForPolicy() {
359
+ if (this.#isBodyless()) return null;
360
+ const value = this.getHeader("content-type");
361
+ return typeof value === "string" ? value : null;
362
+ }
363
+ /**
364
+ * La réserve, amorcée au premier usage avec les en-têtes du transport
365
+ * (`Server`, `X-Content-Type-Options`, `X-Frame-Options`, HSTS) que le
366
+ * noyau a figés au démarrage — posés EN TÊTE, comme à l'entrée de la requête.
367
+ */
368
+ #store() {
369
+ if (this.#headers === null) {
370
+ const transport = this.#committed ? null : this.context.httpKernel?.transportHeaders(this.context.serverType);
371
+ this.#headers = transport ? [...transport] : [];
372
+ }
373
+ return this.#headers;
374
+ }
375
+ /** Position du nom (minuscules) dans la réserve, ou -1. */
376
+ #indexOf(lower) {
377
+ const list = this.#headers;
378
+ if (list === null) return -1;
379
+ for (let i = 0; i < list.length; i += 2) if (list[i] === lower) return i;
380
+ return -1;
381
+ }
382
+ /** Range un en-tête, en remplaçant sa valeur s'il est déjà posé. */
383
+ #put(lower, value) {
384
+ const list = this.#store();
385
+ const at = this.#indexOf(lower);
386
+ if (at === -1) list.push(lower, value);
387
+ else list[at + 1] = value;
388
+ }
389
+ /**
390
+ * Recopie la réserve sur la réponse Node, pour un émetteur qui écrit sans
391
+ * passer par {@link writeHead} — fichier statique, relais vers un amont. La
392
+ * réserve se vide ; ce qui se pose ensuite y retourne, et Node fusionne les
393
+ * deux au `writeHead`.
394
+ */
395
+ commitHeaders() {
396
+ const res = this.response;
397
+ if (res === null || res.headersSent) return;
398
+ if (this.headerPolicy !== null) {
399
+ const pathname = this.context.request.pathname;
400
+ const type = pathname.endsWith("/") ? false : mime.lookup(pathname);
401
+ this.applyHeaderPolicy(type === false ? void 0 : type, true);
402
+ }
403
+ const list = this.#store();
404
+ for (let i = 0; i + 1 < list.length; i += 2) {
405
+ const name = list[i];
406
+ const value = list[i + 1];
407
+ if (typeof name === "string" && value !== void 0) res.setHeader(name, value);
408
+ }
409
+ this.#headers = null;
410
+ this.#committed = true;
411
+ }
412
+ /**
413
+ * Recopie la réserve sur la réponse Node au PREMIER `setHeader` qu'un tiers
414
+ * y fera — fichier statique servi, redirection de dossier — et seulement
415
+ * alors : un raté du repli statique (la 404 d'une route inconnue) ne recopie
416
+ * rien, sa réponse part ensuite en un seul `writeHead`. À désarmer par
417
+ * {@link disarmNativeWrite} quand le tiers rend la main sans avoir écrit.
418
+ *
419
+ * L'interception est une propriété PROPRE de la réponse, ajoutée puis
420
+ * retirée : V8 annule alors la dernière transition de forme de l'objet.
421
+ */
422
+ commitBeforeNativeWrite() {
423
+ const res = this.response;
424
+ if (res === null || res.headersSent || Object.hasOwn(res, "setHeader")) return;
425
+ const native = res.setHeader;
426
+ Object.defineProperty(res, "setHeader", {
427
+ configurable: true,
428
+ writable: true,
429
+ value: (name, value) => {
430
+ this.disarmNativeWrite();
431
+ this.commitHeaders();
432
+ return Reflect.apply(native, res, [name, value]);
433
+ }
434
+ });
435
+ }
436
+ /** Retire l'interception posée par {@link commitBeforeNativeWrite}. */
437
+ disarmNativeWrite() {
438
+ const res = this.response;
439
+ if (res !== null && Object.hasOwn(res, "setHeader")) Reflect.deleteProperty(res, "setHeader");
440
+ }
441
+ /**
442
+ * Retire un en-tête, de la réserve comme de la réponse Node.
443
+ *
444
+ * @param name - nom de l'en-tête (casse indifférente).
445
+ */
446
+ removeHeader(name) {
447
+ const at = this.#indexOf(name.toLowerCase());
448
+ if (at !== -1) this.#headers?.splice(at, 2);
449
+ if (this.response && !this.response.headersSent) this.response.removeHeader(name);
450
+ }
451
+ /**
200
452
  * Union des tokens `Vary` déjà posés et de ceux qu'on ajoute, sans doublon.
201
453
  *
202
454
  * `*` absorbe tout (RFC 9110 : « la réponse varie sur des paramètres non
@@ -207,7 +459,7 @@ var HttpResponse = class {
207
459
  */
208
460
  #mergeVary(value) {
209
461
  const incoming = (Array.isArray(value) ? value.join(",") : String(value)).split(",").map((t) => t.trim()).filter(Boolean);
210
- const current = this.response?.getHeader("vary");
462
+ const current = this.getHeader("vary");
211
463
  const all = [...(Array.isArray(current) ? current.join(",") : String(current ?? "")).split(",").map((t) => t.trim()).filter(Boolean), ...incoming];
212
464
  if (all.some((t) => t === "*")) return "*";
213
465
  const seen = /* @__PURE__ */ new Set();
@@ -226,10 +478,10 @@ var HttpResponse = class {
226
478
  const value = obj[head];
227
479
  if (value !== void 0) this.setHeader(head, value);
228
480
  }
229
- return this.headers = this.response?.getHeaders();
481
+ return this.headers = this.getHeaders();
230
482
  }
231
483
  this.log("headers already sended ", "WARNING");
232
- return this.headers = this.response.getHeaders();
484
+ return this.headers = this.getHeaders();
233
485
  }
234
486
  setContentType(type, encoding) {
235
487
  if (type && encoding) {
@@ -238,8 +490,12 @@ var HttpResponse = class {
238
490
  const mytype = resolved.bare;
239
491
  this.contentType = mytype;
240
492
  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}`);
493
+ if (mytype === "application/json" || mytype.endsWith("+json")) {
494
+ this.setHeader("Content-Type", mytype);
495
+ return;
496
+ }
497
+ this.setHeader("Content-Type", `${mytype}; charset=${encoding}`);
498
+ return;
243
499
  }
244
500
  }
245
501
  if (type && !encoding) {
@@ -249,10 +505,11 @@ var HttpResponse = class {
249
505
  this.contentType = mytype;
250
506
  const charset = resolved.charset;
251
507
  if (charset) this.encoding = charset;
252
- return this.setHeader("Content-Type", mytype);
508
+ this.setHeader("Content-Type", mytype);
509
+ return;
253
510
  }
254
511
  }
255
- return this.setHeader("Content-Type", `${this.contentType}; charset=${this.encoding}`);
512
+ this.setHeader("Content-Type", `${this.contentType}; charset=${this.encoding}`);
256
513
  }
257
514
  setFileMimeType(type, encoding) {
258
515
  let myType = this.getMimeType(type);
@@ -261,7 +518,7 @@ var HttpResponse = class {
261
518
  myType = "application/octet-stream";
262
519
  }
263
520
  this.contentType = myType;
264
- return this.setContentType(myType, encoding || this.encoding);
521
+ this.setContentType(myType, encoding || this.encoding);
265
522
  }
266
523
  setContentTypeByExtension(extention) {
267
524
  const ismime = mime.contentType(extention);
@@ -269,7 +526,8 @@ var HttpResponse = class {
269
526
  this.contentType = ismime;
270
527
  let charset = mime.charset(this.contentType);
271
528
  if (charset) this.encoding = charset;
272
- return this.setHeader("Content-Type", ismime);
529
+ this.setHeader("Content-Type", ismime);
530
+ return;
273
531
  }
274
532
  this.log(`setContentTypeByExtension: ${extention} not found`, "WARNING");
275
533
  }
@@ -342,14 +600,18 @@ var HttpResponse = class {
342
600
  setLength(body) {
343
601
  if (this.response?.headersSent) throw new Error("Headers already sended");
344
602
  if (this.getHeader("Transfer-Encoding") === "chunked") {
345
- this.response?.removeHeader("Content-Length");
603
+ this.removeHeader("Content-Length");
346
604
  return 0;
347
605
  }
348
606
  if (NO_CONTENT_LENGTH_STATUS.has(this.statusCode)) return 0;
607
+ if (this.statusCode === 205) {
608
+ this.#put("content-length", "0");
609
+ return 0;
610
+ }
349
611
  const actualBody = body || this.#text || this.#buffer;
350
612
  if (actualBody) {
351
613
  const length = typeof actualBody === "string" && actualBody === this.#text ? Buffer.byteLength(actualBody, this.#textEncoding) : Buffer.byteLength(actualBody);
352
- this.setHeader("Content-Length", String(length));
614
+ this.#put("content-length", String(length));
353
615
  return length;
354
616
  }
355
617
  return 0;
@@ -362,27 +624,69 @@ var HttpResponse = class {
362
624
  if (this.statusCode > 599) this.statusCode = 500;
363
625
  }
364
626
  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);
627
+ if (headers) this.#apply(headers);
628
+ if (this.context.requestId) this.#put("x-request-id", this.context.requestId);
629
+ if (this.context.traceparent) this.#put("traceparent", this.context.traceparent);
367
630
  this.setLength();
368
631
  this.ensureContentTypeHeader();
632
+ this.applyHeaderPolicy(this.contentTypeForPolicy());
633
+ const list = this.#store();
369
634
  const std = http.STATUS_CODES[this.statusCode];
370
- if (this.statusMessage === std) this.response.writeHead(this.statusCode, headers);
635
+ if (this.statusMessage === std) this.response.writeHead(this.statusCode, list);
371
636
  else {
372
637
  const safeMsg = this.statusMessage.replace(/[^\x20-\x7E]/g, "").trim() || (std ?? "Unknown Error");
373
- this.response.writeHead(this.statusCode, safeMsg, headers);
638
+ this.response.writeHead(this.statusCode, safeMsg, list);
374
639
  }
375
640
  } else {
376
641
  this.log("Headers already sent !!", "WARNING");
377
642
  throw new Error(`Headers already sent !!`);
378
643
  }
379
644
  }
645
+ /**
646
+ * Range dans la réserve les en-têtes passés à `writeHead` — ils l'emportent
647
+ * sur ceux déjà posés, comme chez Node.
648
+ */
649
+ #apply(headers) {
650
+ if (Array.isArray(headers)) {
651
+ for (let i = 0; i + 1 < headers.length; i += 2) {
652
+ const value = headers[i + 1];
653
+ if (value !== void 0) this.setHeader(String(headers[i]), value);
654
+ }
655
+ return;
656
+ }
657
+ for (const name in headers) {
658
+ const value = headers[name];
659
+ if (value !== void 0) this.setHeader(name, value);
660
+ }
661
+ }
662
+ /** La réserve amorcée — pour une remise sous une autre forme (HTTP/2). */
663
+ headerList() {
664
+ return this.#store();
665
+ }
380
666
  addTrailers(headers) {
381
667
  return this.response?.addTrailers(headers);
382
668
  }
669
+ /**
670
+ * Écrit un morceau d'une réponse en flux, sans la terminer — c'est `end()`
671
+ * qui la clôt après le dernier.
672
+ *
673
+ * Au premier morceau, les en-têtes partent SANS longueur ni corps posé (le
674
+ * patron de `SseStream`), et le cadrage revient à Node : `chunked` pour un
675
+ * client HTTP/1.1, fermeture de connexion pour un HTTP/1.0 (RFC 9112 §6.1 —
676
+ * pas de `Transfer-Encoding` à qui ne le connaît pas), trames DATA en HTTP/2
677
+ * où l'en-tête est interdit (RFC 9113 §8.2.2). Ne JAMAIS le poser à la main :
678
+ * une fois `flushing` levé, `setHeader` range en trailer, et un
679
+ * `Transfer-Encoding` en trailer est un message malformé (RFC 9110 §6.5.1).
680
+ *
681
+ * @param chunk - le morceau à écrire.
682
+ * @param encoding - l'encodage d'un morceau texte.
683
+ * @returns la promesse réglée quand le flux accepte l'écriture suivante.
684
+ */
383
685
  flush(chunk, encoding) {
384
- this.flushing = true;
385
- this.setHeader("Transfer-Encoding", "chunked");
686
+ if (!this.flushing) {
687
+ if (!this.isHeaderSent()) this.writeHead();
688
+ this.flushing = true;
689
+ }
386
690
  return this.send(chunk, encoding, true);
387
691
  }
388
692
  /**
@@ -445,15 +749,30 @@ var HttpResponse = class {
445
749
  if (!this.response) throw new Error(`response not found`);
446
750
  return this.response.end(chunk, encoding || this.encoding);
447
751
  }
752
+ /**
753
+ * Valeur d'un en-tête posé — dans la réserve, sinon sur la réponse Node (ce
754
+ * qu'un tiers y a écrit directement : `send`, quota).
755
+ *
756
+ * @param name - nom de l'en-tête (casse indifférente).
757
+ */
448
758
  getHeader(name) {
759
+ const at = this.#indexOf(name.toLowerCase());
760
+ if (at !== -1) return this.#headers?.[at + 1];
449
761
  return this.response?.getHeader(name);
450
762
  }
451
763
  hasHeader(name) {
452
- if (this.response) return this.response.hasHeader(name);
764
+ if (this.response) return this.#indexOf(name.toLowerCase()) !== -1 || this.response.hasHeader(name);
453
765
  throw new Error(`Respose not foud`);
454
766
  }
767
+ /** Tous les en-têtes posés, noms en minuscules — une copie, à chaque appel. */
455
768
  getHeaders() {
456
- return this.response?.getHeaders();
769
+ const out = { ...this.response?.getHeaders() };
770
+ const list = this.#headers;
771
+ if (list !== null) for (let i = 0; i + 1 < list.length; i += 2) {
772
+ const name = list[i];
773
+ if (typeof name === "string") out[name] = list[i + 1];
774
+ }
775
+ return out;
457
776
  }
458
777
  redirect(url, status, headers) {
459
778
  this.context.isRedirect = true;
@@ -481,4 +800,4 @@ var HttpResponse = class {
481
800
  return this.context.log(pci, severity, msgid, msg);
482
801
  }
483
802
  };
484
- export { HttpResponse as default };
803
+ export { HttpResponse as default, isContentlessStatus };