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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +7 -0
  2. package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorateMetadata.js +1 -1
  4. package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorateParam.js +1 -1
  5. package/dist/index.js +6 -4
  6. package/dist/nodefony/command/assetsPublishCommand.js +1 -1
  7. package/dist/nodefony/command/certificatesCommand.js +1 -1
  8. package/dist/nodefony/command/networkCommand.js +1 -1
  9. package/dist/nodefony/command/proxyGenerateCommand.js +1 -1
  10. package/dist/nodefony/config/config.js +22 -1
  11. package/dist/nodefony/interfaces/IRouting.js +1 -0
  12. package/dist/nodefony/interfaces/ISecurity.js +1 -0
  13. package/dist/nodefony/service/ProfilerAdminApi.js +2 -1
  14. package/dist/nodefony/service/audit-logger.js +16 -13
  15. package/dist/nodefony/service/certificates.js +25 -13
  16. package/dist/nodefony/service/error-renderer.js +118 -5
  17. package/dist/nodefony/service/http-kernel.js +342 -165
  18. package/dist/nodefony/service/pretty-request-logger.js +3 -10
  19. package/dist/nodefony/service/servers/clientError.js +2 -2
  20. package/dist/nodefony/service/servers/server-http.js +23 -23
  21. package/dist/nodefony/service/servers/server-https.js +41 -42
  22. package/dist/nodefony/service/servers/server-static.js +13 -17
  23. package/dist/nodefony/service/servers/server-websocket-secure.js +11 -10
  24. package/dist/nodefony/service/servers/server-websocket.js +11 -10
  25. package/dist/nodefony/service/servers/wsHeartbeat.js +2 -2
  26. package/dist/nodefony/service/sessions/sessions-service.js +25 -17
  27. package/dist/nodefony/service/trace.js +1 -1
  28. package/dist/nodefony/service/upload/upload-service.js +11 -10
  29. package/dist/nodefony/src/assets/prebuiltUi.js +1 -1
  30. package/dist/nodefony/src/context/Context.js +34 -19
  31. package/dist/nodefony/src/context/forwarded.js +4 -4
  32. package/dist/nodefony/src/context/http/HttpContext.js +133 -88
  33. package/dist/nodefony/src/context/http/Request.js +93 -44
  34. package/dist/nodefony/src/context/http/Response.js +107 -51
  35. package/dist/nodefony/src/context/http/parser.js +9 -10
  36. package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
  37. package/dist/nodefony/src/context/http2/Response.js +53 -25
  38. package/dist/nodefony/src/context/metaData.js +1 -1
  39. package/dist/nodefony/src/context/trustProxy.js +4 -3
  40. package/dist/nodefony/src/context/websocket/Response.js +13 -6
  41. package/dist/nodefony/src/context/websocket/WebsocketContext.js +59 -26
  42. package/dist/nodefony/src/context/websocket/wsCloseReason.js +41 -0
  43. package/dist/nodefony/src/cookies/cookie.js +3 -3
  44. package/dist/nodefony/src/errors/httpError.js +6 -6
  45. package/dist/nodefony/src/errors/thrownMessage.js +27 -0
  46. package/dist/nodefony/src/profiler/FrameProfile.js +4 -3
  47. package/dist/nodefony/src/profiler/Profiler.js +4 -4
  48. package/dist/nodefony/src/servers/kernelServers.js +29 -0
  49. package/dist/nodefony/src/servers/portBinder.js +8 -21
  50. package/dist/nodefony/src/session/session.js +18 -10
  51. package/dist/nodefony/src/session/sessionStoreFailure.js +2 -1
  52. package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +24 -9
  53. package/dist/types/index.d.ts +16 -0
  54. package/dist/types/nodefony/config/config.d.ts +22 -0
  55. package/dist/types/nodefony/interfaces/IContext.d.ts +24 -7
  56. package/dist/types/nodefony/interfaces/ICookie.d.ts +18 -18
  57. package/dist/types/nodefony/interfaces/IHttpKernel.d.ts +10 -4
  58. package/dist/types/nodefony/interfaces/IResponse.d.ts +1 -1
  59. package/dist/types/nodefony/interfaces/IRouting.d.ts +87 -0
  60. package/dist/types/nodefony/interfaces/ISecurity.d.ts +45 -0
  61. package/dist/types/nodefony/interfaces/ISession.d.ts +8 -8
  62. package/dist/types/nodefony/interfaces/IUpload.d.ts +9 -9
  63. package/dist/types/nodefony/interfaces/index.d.ts +2 -0
  64. package/dist/types/nodefony/service/audit-logger.d.ts +14 -10
  65. package/dist/types/nodefony/service/certificates.d.ts +10 -10
  66. package/dist/types/nodefony/service/error-renderer.d.ts +14 -0
  67. package/dist/types/nodefony/service/http-kernel.d.ts +192 -42
  68. package/dist/types/nodefony/service/servers/server-http.d.ts +4 -1
  69. package/dist/types/nodefony/service/servers/server-https.d.ts +4 -1
  70. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +4 -1
  71. package/dist/types/nodefony/service/servers/server-websocket.d.ts +4 -1
  72. package/dist/types/nodefony/service/sessions/sessions-service.d.ts +6 -2
  73. package/dist/types/nodefony/service/upload/upload-service.d.ts +1 -1
  74. package/dist/types/nodefony/src/context/Context.d.ts +21 -10
  75. package/dist/types/nodefony/src/context/forwarded.d.ts +3 -3
  76. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +75 -18
  77. package/dist/types/nodefony/src/context/http/Request.d.ts +39 -2
  78. package/dist/types/nodefony/src/context/http/Response.d.ts +47 -7
  79. package/dist/types/nodefony/src/context/http/parser.d.ts +1 -1
  80. package/dist/types/nodefony/src/context/http2/Response.d.ts +32 -2
  81. package/dist/types/nodefony/src/context/websocket/Response.d.ts +4 -4
  82. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +11 -2
  83. package/dist/types/nodefony/src/context/websocket/wsCloseReason.d.ts +21 -0
  84. package/dist/types/nodefony/src/cookies/cookie.d.ts +9 -9
  85. package/dist/types/nodefony/src/errors/httpError.d.ts +7 -7
  86. package/dist/types/nodefony/src/errors/thrownMessage.d.ts +12 -0
  87. package/dist/types/nodefony/src/profiler/Profiler.d.ts +28 -28
  88. package/dist/types/nodefony/src/servers/kernelServers.d.ts +20 -0
  89. package/dist/types/nodefony/src/servers/portBinder.d.ts +6 -19
  90. package/dist/types/nodefony/src/session/session.d.ts +12 -12
  91. package/dist/types/nodefony/src/session/storage/RevocationGuardStorage.d.ts +1 -1
  92. package/docs/cookies.md +25 -25
  93. package/docs/observabilite.md +30 -30
  94. package/docs/rate-limit.md +22 -22
  95. package/docs/servers.md +61 -61
  96. package/docs/session.md +82 -77
  97. package/docs/upload.md +43 -37
  98. package/package.json +11 -11
@@ -2,7 +2,7 @@ import HttpError from "../../errors/httpError.js";
2
2
  import { hasForwardingHeaders, resolveForwarded } from "../forwarded.js";
3
3
  import { Parser, ParserJson, ParserQs, ParserXml, acceptParser } from "./parser.js";
4
4
  import { isCanonicalAuthority, splitTarget } from "./urlFastPath.js";
5
- import { extend } from "nodefony";
5
+ import { RequestContext, extend, useConfig } from "nodefony";
6
6
  import { createHash, randomUUID } from "node:crypto";
7
7
  import { URL } from "node:url";
8
8
  import fs from "node:fs";
@@ -39,6 +39,14 @@ const parse = {
39
39
  PATCH: true,
40
40
  DELETE: true
41
41
  };
42
+ /**
43
+ * La méthode porte-t-elle un corps à lire ? La règle a UNE implémentation : le
44
+ * choix « lire le corps » (`parseRequest`) et le choix « rien à attendre »
45
+ * (`initialize`) la lisent tous deux ici.
46
+ */
47
+ function hasRequestBody(method) {
48
+ return method in parse;
49
+ }
42
50
  const PERF_PROBE_SUB = process.env.NF_PERF_PROBE === "1";
43
51
  function perfMark(field) {
44
52
  const p = globalThis.__nfPerfProbe;
@@ -129,12 +137,12 @@ var HttpRequest = class {
129
137
  #acceptHtml = null;
130
138
  /** Media-ranges de l'en-tête `Accept`, parsés au premier accès (mémoïsé). */
131
139
  get accept() {
132
- if (this.#accept === null) this.#accept = acceptParser(this.headers?.accept);
140
+ this.#accept ??= acceptParser(this.headers.accept);
133
141
  return this.#accept;
134
142
  }
135
143
  /** `true` si le client accepte `text/html` — résolu au premier accès (mémoïsé). */
136
144
  get acceptHtml() {
137
- if (this.#acceptHtml === null) this.#acceptHtml = this.accepts("html");
145
+ this.#acceptHtml ??= this.accepts("html");
138
146
  return this.#acceptHtml;
139
147
  }
140
148
  origin;
@@ -147,8 +155,8 @@ var HttpRequest = class {
147
155
  this.request.body = null;
148
156
  this.headers = request.headers;
149
157
  this.origin = this.headers.origin;
150
- const checker = this.context?.httpKernel?.getTrustProxyChecker();
151
- const socketAddress = this.request.socket?.remoteAddress;
158
+ const checker = this.context.httpKernel?.getTrustProxyChecker();
159
+ const socketAddress = this.request.socket.remoteAddress;
152
160
  this.trustedProxy = !!checker?.isTrusted(socketAddress);
153
161
  if (this.trustedProxy && checker && hasForwardingHeaders(this.headers)) this.forwarded = resolveForwarded(this.headers, socketAddress, checker);
154
162
  if (PERF_PROBE_SUB) perfMark("reqProxyNs");
@@ -167,11 +175,9 @@ var HttpRequest = class {
167
175
  this.search = u.search;
168
176
  }
169
177
  if (PERF_PROBE_SUB) perfMark("reqUrlNs");
170
- this.queryStringOptions = this.context?.httpKernel?.module.options.queryString || {};
171
- this.uploadOption = this.context?.httpKernel?.module.options.upload || {};
172
- this.maxBodySize = this.context?.httpKernel?.module.options.maxBodySize ?? 0;
178
+ this.queryStringOptions = this.context.httpKernel?.module.options.queryString || {};
173
179
  let query;
174
- if (this.search) query = QS.parse(this.search.slice(1), this.queryStringOptions || {});
180
+ if (this.search) query = QS.parse(this.search.slice(1), this.queryStringOptions ?? {});
175
181
  else query = {};
176
182
  if (this.#url !== null) this.#url.query = query;
177
183
  this.queryGet = query;
@@ -198,32 +204,64 @@ var HttpRequest = class {
198
204
  if (this.context.listenerCount("onRequestEnd") === 0) return false;
199
205
  return this.context.fireAsync("onRequestEnd", this);
200
206
  }
201
- async initialize() {
207
+ /**
208
+ * Lit le corps de la requête, s'il y en a un, puis émet `onRequestEnd`.
209
+ *
210
+ * Le contrat de retour dit ce qui reste à ATTENDRE :
211
+ * - `undefined` — la méthode ne porte pas de corps (GET, HEAD, OPTIONS…) et
212
+ * aucun écouteur `onRequestEnd` ne rend de promesse : tout est fait, de
213
+ * façon SYNCHRONE ;
214
+ * - une `Promise` — le corps arrive du socket (POST, PUT, PATCH, DELETE), ou
215
+ * un écouteur `onRequestEnd` est à attendre. Sa valeur n'est consommée par
216
+ * personne : elle ne sert qu'au séquençage.
217
+ *
218
+ * L'appelant n'attend donc que ce qui attend vraiment
219
+ * (`const pending = request.initialize(); if (pending) await pending;`) : un
220
+ * `await` inconditionnel reportait le pipeline d'une micro-tâche, et la
221
+ * chaîne de promesses coûtait ~6 évènements asynchrones à un GET qui n'avait
222
+ * rien à lire (#505).
223
+ *
224
+ * Ne lève jamais de façon synchrone : un corps refusé (413) ou illisible rompt
225
+ * la promesse rendue, comme avant.
226
+ *
227
+ * @returns `undefined` quand tout est fait, sinon la promesse à attendre.
228
+ */
229
+ initialize() {
230
+ if (!hasRequestBody(this.method)) return this.endWithoutBody();
202
231
  return this.parseRequest().then(async (parser) => {
203
- switch (true) {
204
- case parser instanceof ParserXml:
205
- case parser instanceof ParserQs:
206
- case parser instanceof Parser: try {
232
+ if (parser instanceof ParserXml || parser instanceof ParserQs || parser instanceof Parser) {
233
+ let requestEnd;
234
+ try {
207
235
  if (this.context.finished) return;
208
236
  await parser.parse();
209
- return this.fireRequestEnd();
210
- } catch (error) {
211
- return this.context?.httpKernel?.onError(error, this.context);
212
- }
213
- default: if (!parser) try {
214
- if (this.context.finished) return;
215
- this.context.requestEnded = true;
216
- return this.fireRequestEnd();
237
+ requestEnd = this.fireRequestEnd();
217
238
  } catch (error) {
218
239
  return this.context.httpKernel?.onError(error, this.context);
219
240
  }
241
+ return requestEnd;
220
242
  }
243
+ if (!parser) return this.endWithoutBody();
221
244
  return parser;
222
- }).catch((e) => {
223
- throw e;
224
245
  });
225
246
  }
226
247
  /**
248
+ * Fin d'une requête SANS corps à lire : la marque terminée et émet
249
+ * `onRequestEnd`. Synchrone — ne rend une promesse que lorsqu'un écouteur
250
+ * `onRequestEnd` est à attendre, ou quand l'émission lève (celle d'`onError`).
251
+ *
252
+ * @returns `undefined` quand tout est fait, sinon la promesse à attendre.
253
+ */
254
+ endWithoutBody() {
255
+ if (this.context.finished) return;
256
+ try {
257
+ this.context.requestEnded = true;
258
+ const requestEnd = this.fireRequestEnd();
259
+ return requestEnd === false ? void 0 : requestEnd;
260
+ } catch (error) {
261
+ return this.context.httpKernel?.onError(error, this.context);
262
+ }
263
+ }
264
+ /**
227
265
  * B1 — Pré-contrôle de taille du corps via l'en-tête `Content-Length` :
228
266
  * rejette en 413 (RFC 9110 §15.5.14 « Content Too Large ») AVANT toute lecture
229
267
  * quand le client annonce une taille supérieure à `maxBodySize`. Rideau de
@@ -232,16 +270,28 @@ var HttpRequest = class {
232
270
  *
233
271
  * @throws {HttpError} 413 si `Content-Length` dépasse `maxBodySize`.
234
272
  */
273
+ /**
274
+ * Lit le plafond du corps et les limites d'envoi tels que les voit CETTE
275
+ * requête : la configuration http, surchargée par le calque que la requête a
276
+ * pu poser avant la lecture du corps (`overlayConfig`, #494). Hors pipeline
277
+ * (aucun scope ouvert), la configuration du module telle quelle.
278
+ */
279
+ readBodyLimits() {
280
+ const options = RequestContext.getScope() !== void 0 ? useConfig("@nodefony/http") : this.context.httpKernel?.module.options;
281
+ this.uploadOption = options?.upload ?? {};
282
+ this.maxBodySize = options?.maxBodySize ?? 0;
283
+ }
235
284
  enforceBodyLimit() {
236
285
  const max = this.maxBodySize;
237
286
  if (max <= 0) return;
238
287
  const cl = this.headers["content-length"];
239
288
  if (cl === void 0) return;
240
- const declared = parseInt(Array.isArray(cl) ? cl[0] : cl, 10);
289
+ const declared = parseInt(typeof cl === "string" ? cl : cl[0], 10);
241
290
  if (Number.isFinite(declared) && declared > max) throw new HttpError(`Request body too large: ${declared} bytes > maxBodySize ${max}`, 413, this.context);
242
291
  }
243
292
  async parseRequest() {
244
- if (!(this.method in parse)) return this.parser;
293
+ if (!hasRequestBody(this.method)) return this.parser;
294
+ this.readBodyLimits();
245
295
  if (this.contentType !== "multipart/form-data") this.enforceBodyLimit();
246
296
  switch (this.contentType) {
247
297
  case "application/xml":
@@ -349,8 +399,7 @@ var HttpRequest = class {
349
399
  stream.resume();
350
400
  return;
351
401
  }
352
- const match = reg.exec(fieldname);
353
- const field = match ? match[1] : fieldname;
402
+ const field = reg.exec(fieldname)?.[1] ?? fieldname;
354
403
  const ext = filename ? path.extname(filename) : "";
355
404
  const newFilename = `${randomUUID()}${ext}`;
356
405
  const filepath = path.join(dir, newFilename);
@@ -415,7 +464,7 @@ var HttpRequest = class {
415
464
  */
416
465
  assignField(fields, name, value) {
417
466
  const match = reg.exec(name);
418
- const key = match ? match[1] : name;
467
+ const key = match?.[1] ?? name;
419
468
  if (key in fields) {
420
469
  const cur = fields[key];
421
470
  if (Array.isArray(cur)) cur.push(value);
@@ -427,7 +476,7 @@ var HttpRequest = class {
427
476
  let subtype = "*";
428
477
  let type = "*";
429
478
  if (Type) parts = Type.split("/");
430
- if (parts) switch (parts.length) {
479
+ switch (parts.length) {
431
480
  case 1:
432
481
  subtype = parts.shift();
433
482
  break;
@@ -437,10 +486,7 @@ var HttpRequest = class {
437
486
  break;
438
487
  default: throw new Error("request accepts method bad type format");
439
488
  }
440
- for (let i = 0; i < this.accept.length; i++) {
441
- const line = this.accept[i];
442
- if ((type === "*" || line.type.test(type)) && (subtype === "*" || line.subtype.test(subtype))) return true;
443
- }
489
+ for (const line of this.accept) if ((type === "*" || line.type.test(type)) && (subtype === "*" || line.subtype.test(subtype))) return true;
444
490
  return false;
445
491
  }
446
492
  async createFileUpload(name, file) {
@@ -454,12 +500,15 @@ var HttpRequest = class {
454
500
  getContentType(request) {
455
501
  if (request.headers["content-type"]) {
456
502
  const tab = request.headers["content-type"].split(";");
457
- if (tab.length > 1) for (let i = 1; i < tab.length; i++) if (typeof tab[i] === "string") {
458
- const ele = tab[i].split("=");
459
- const key = ele[0].replace(/\s+/g, "").toLowerCase();
460
- this.rawContentType[key] = ele[1];
461
- } else continue;
462
- return tab[0];
503
+ if (tab.length > 1) for (let i = 1; i < tab.length; i++) {
504
+ const part = tab[i];
505
+ if (typeof part === "string") {
506
+ const [rawKey = "", value] = part.split("=");
507
+ const key = rawKey.replace(/\s+/g, "").toLowerCase();
508
+ this.rawContentType[key] = value;
509
+ } else continue;
510
+ }
511
+ return tab[0] ?? null;
463
512
  }
464
513
  return null;
465
514
  }
@@ -485,8 +534,8 @@ var HttpRequest = class {
485
534
  }
486
535
  getHostName(host) {
487
536
  if (this.#url?.hostname) return this.#url.hostname;
488
- if (host) return host.split(":")[0];
489
- if (host = this.getHost()) return host.split(":")[0];
537
+ if (host) return host.split(":")[0] ?? "";
538
+ if (host = this.getHost()) return host.split(":")[0] ?? "";
490
539
  return "";
491
540
  }
492
541
  getHost() {
@@ -494,7 +543,7 @@ var HttpRequest = class {
494
543
  }
495
544
  getRemoteAddress() {
496
545
  if (this.forwarded) return this.forwarded.clientIp;
497
- return this.request.socket?.remoteAddress ?? null;
546
+ return this.request.socket.remoteAddress ?? null;
498
547
  }
499
548
  /**
500
549
  * Request-target brut (origin-form) tel que reçu du transport — SOURCE
@@ -511,7 +560,7 @@ var HttpRequest = class {
511
560
  */
512
561
  resolveScheme(request) {
513
562
  if (this.forwarded?.proto) return this.forwarded.proto;
514
- if ("encrypted" in request.socket && request.socket.encrypted) return "https";
563
+ if (request.socket.encrypted) return "https";
515
564
  return "http";
516
565
  }
517
566
  getFullUrl(request = this.request) {
@@ -1,5 +1,5 @@
1
1
  import Cookie from "../../cookies/cookie.js";
2
- import { typeOf } from "nodefony";
2
+ import { thenMaybe, typeOf } from "nodefony";
3
3
  import http from "node:http";
4
4
  import mime from "mime-types";
5
5
  //#region nodefony/src/context/http/Response.ts
@@ -14,11 +14,6 @@ const REDIRECT_STATUS_CODES = /* @__PURE__ */ new Set([
14
14
  307,
15
15
  308
16
16
  ]);
17
- const NO_CONTENT_LENGTH_METHODS = /* @__PURE__ */ new Set([
18
- "HEAD",
19
- "OPTIONS",
20
- "TRACE"
21
- ]);
22
17
  const NO_CONTENT_LENGTH_STATUS = /* @__PURE__ */ new Set([204, 304]);
23
18
  var HttpResponse = class {
24
19
  context;
@@ -27,7 +22,9 @@ var HttpResponse = class {
27
22
  statusMessage = "";
28
23
  flushing = false;
29
24
  encoding = "utf-8";
30
- body = null;
25
+ #text = null;
26
+ #textEncoding = "utf-8";
27
+ #buffer = null;
31
28
  contentType = "application/octet-stream";
32
29
  headers = {};
33
30
  timeout;
@@ -35,7 +32,7 @@ var HttpResponse = class {
35
32
  constructor(response, context) {
36
33
  this.context = context;
37
34
  this.response = response;
38
- this.timeout = this.context?.httpKernel?.responseTimeout[this.context.type];
35
+ this.timeout = this.context.httpKernel?.responseTimeout[this.context.type];
39
36
  }
40
37
  /**
41
38
  * Filet posé juste avant l'émission des en-têtes : si aucun `Content-Type`
@@ -46,6 +43,33 @@ var HttpResponse = class {
46
43
  ensureContentTypeHeader() {
47
44
  if (this.response && !this.response.hasHeader("content-type")) this.response.setHeader("content-type", this.contentType);
48
45
  }
46
+ /**
47
+ * Corps de la réponse en octets. Un corps posé en texte n'est encodé qu'à la
48
+ * première lecture de cette propriété — l'envoi, lui, écrit le texte tel quel.
49
+ */
50
+ get body() {
51
+ if (this.#text !== null) {
52
+ this.#buffer = Buffer.from(this.#text, this.#textEncoding);
53
+ this.#text = null;
54
+ }
55
+ return this.#buffer;
56
+ }
57
+ set body(value) {
58
+ this.#text = null;
59
+ this.#buffer = value;
60
+ }
61
+ /**
62
+ * Corps tel qu'il part sur le fil : le texte s'il n'a jamais été encodé, sinon
63
+ * les octets — vide légal si rien n'a été posé (`write(null)` jetterait
64
+ * ERR_STREAM_NULL_VALUES).
65
+ */
66
+ get payload() {
67
+ return this.#text ?? this.#buffer ?? "";
68
+ }
69
+ /** Encodage à passer avec {@link payload} (ignoré par Node pour des octets). */
70
+ get payloadEncoding() {
71
+ return this.#text !== null ? this.#textEncoding : this.encoding;
72
+ }
49
73
  clean() {
50
74
  this.response = null;
51
75
  this.body = null;
@@ -68,7 +92,7 @@ var HttpResponse = class {
68
92
  }
69
93
  deleteCookie(cookie) {
70
94
  if (cookie instanceof Cookie) {
71
- if (this.cookies[cookie.name]) {
95
+ if (Object.hasOwn(this.cookies, cookie.name)) {
72
96
  delete this.cookies[cookie.name];
73
97
  return true;
74
98
  }
@@ -77,19 +101,20 @@ var HttpResponse = class {
77
101
  throw new Error("Response delCookie not valid cookies");
78
102
  }
79
103
  deleteCookieByName(name) {
80
- if (this.cookies[name]) {
104
+ if (Object.hasOwn(this.cookies, name)) {
81
105
  delete this.cookies[name];
82
106
  return true;
83
107
  }
84
108
  return false;
85
109
  }
86
110
  setCookies() {
87
- const names = Object.keys(this.cookies);
88
- if (names.length === 0) return;
89
- if (names.length === 1) return this.setCookie(this.cookies[names[0]]);
111
+ const cookies = Object.values(this.cookies);
112
+ const first = cookies.at(0);
113
+ if (first === void 0) return;
114
+ if (cookies.length === 1) return this.setCookie(first);
90
115
  const serialized = [];
91
- for (const name of names) {
92
- const s = this.cookies[name].serialize();
116
+ for (const cookie of cookies) {
117
+ const s = cookie.serialize();
93
118
  this.log(`ADD COOKIE ==> ${s}`, "DEBUG");
94
119
  serialized.push(s);
95
120
  }
@@ -105,7 +130,8 @@ var HttpResponse = class {
105
130
  if (this.flushing) {
106
131
  const obj = {};
107
132
  obj[name] = value;
108
- return this.addTrailers(obj);
133
+ this.addTrailers(obj);
134
+ return;
109
135
  }
110
136
  if (!this.response.headersSent) {
111
137
  const lower = name.toLowerCase();
@@ -125,7 +151,7 @@ var HttpResponse = class {
125
151
  */
126
152
  #mergeVary(value) {
127
153
  const incoming = (Array.isArray(value) ? value.join(",") : String(value)).split(",").map((t) => t.trim()).filter(Boolean);
128
- const current = this.response?.getHeader?.("vary");
154
+ const current = this.response?.getHeader("vary");
129
155
  const all = [...(Array.isArray(current) ? current.join(",") : String(current ?? "")).split(",").map((t) => t.trim()).filter(Boolean), ...incoming];
130
156
  if (all.some((t) => t === "*")) return "*";
131
157
  const seen = /* @__PURE__ */ new Set();
@@ -151,9 +177,9 @@ var HttpResponse = class {
151
177
  }
152
178
  setContentType(type, encoding) {
153
179
  if (type && encoding) {
154
- let mytype = mime.contentType(type);
155
- if (mytype) {
156
- mytype = mytype.split(";")[0];
180
+ const full = mime.contentType(type);
181
+ if (full) {
182
+ const mytype = full.split(";")[0] ?? full;
157
183
  this.contentType = mytype;
158
184
  this.encoding = encoding;
159
185
  if (mytype === "application/json" || mytype.endsWith("+json")) return this.setHeader("Content-Type", mytype);
@@ -230,30 +256,45 @@ var HttpResponse = class {
230
256
  if (this.response) return this.statusMessage || this.response.statusMessage || http.STATUS_CODES[this.statusCode];
231
257
  return this.statusMessage || http.STATUS_CODES[this.statusCode];
232
258
  }
259
+ /**
260
+ * Pose le corps de la réponse : texte gardé tel quel, octets copiés sur leur
261
+ * seule fenêtre, tout autre valeur sérialisée en JSON.
262
+ *
263
+ * @param ele - corps à envoyer.
264
+ * @param encoding - encodage d'un corps texte (défaut : `this.encoding`).
265
+ */
233
266
  setBody(ele, encoding) {
234
- if (typeof ele === "string") this.body = Buffer.from(ele, encoding || this.encoding);
267
+ if (typeof ele === "string") this.#setText(ele, encoding || this.encoding);
235
268
  else if (ele instanceof ArrayBuffer || ele instanceof SharedArrayBuffer) this.body = Buffer.from(ele);
236
269
  else if (ArrayBuffer.isView(ele) && ele.buffer instanceof ArrayBuffer) this.body = Buffer.from(ele.buffer, ele.byteOffset, ele.byteLength);
237
- else try {
238
- this.body = Buffer.from(JSON.stringify(ele));
239
- } catch (e) {
240
- this.body = Buffer.from(String(ele));
270
+ else {
271
+ let text;
272
+ try {
273
+ text = JSON.stringify(ele);
274
+ } catch {
275
+ text = String(ele);
276
+ }
277
+ this.#setText(text, "utf-8");
241
278
  }
242
- return this.body;
279
+ }
280
+ #setText(text, encoding) {
281
+ this.#buffer = null;
282
+ this.#text = text;
283
+ this.#textEncoding = encoding;
243
284
  }
244
285
  setLength(body) {
245
286
  if (this.response?.headersSent) throw new Error("Headers already sended");
246
- const actualBody = body || this.body;
247
- const isChunked = this.getHeader("Transfer-Encoding") === "chunked";
248
- if (!NO_CONTENT_LENGTH_METHODS.has(this.context?.method) && !NO_CONTENT_LENGTH_STATUS.has(this.statusCode) && !isChunked) {
249
- if (actualBody) {
250
- const length = Buffer.byteLength(actualBody);
251
- this.setHeader("Content-Length", String(length));
252
- return length;
253
- }
287
+ if (this.getHeader("Transfer-Encoding") === "chunked") {
288
+ this.response?.removeHeader("Content-Length");
254
289
  return 0;
255
290
  }
256
- if (!NO_CONTENT_LENGTH_STATUS.has(this.statusCode)) this.setHeader("Content-Length", "0");
291
+ if (NO_CONTENT_LENGTH_STATUS.has(this.statusCode)) return 0;
292
+ const actualBody = body || this.#text || this.#buffer;
293
+ if (actualBody) {
294
+ const length = typeof actualBody === "string" && actualBody === this.#text ? Buffer.byteLength(actualBody, this.#textEncoding) : Buffer.byteLength(actualBody);
295
+ this.setHeader("Content-Length", String(length));
296
+ return length;
297
+ }
257
298
  return 0;
258
299
  }
259
300
  writeHead(statusCode, headers) {
@@ -264,8 +305,8 @@ var HttpResponse = class {
264
305
  if (this.statusCode > 599) this.statusCode = 500;
265
306
  }
266
307
  this.statusMessage = this.getStatusMessage();
267
- if (this.context.requestId && !this.response.headersSent) this.response.setHeader("x-request-id", this.context.requestId);
268
- if (this.context.traceparent && !this.response.headersSent) this.response.setHeader("traceparent", this.context.traceparent);
308
+ if (this.context.requestId) this.response.setHeader("x-request-id", this.context.requestId);
309
+ if (this.context.traceparent) this.response.setHeader("traceparent", this.context.traceparent);
269
310
  this.setLength();
270
311
  this.ensureContentTypeHeader();
271
312
  const std = http.STATUS_CODES[this.statusCode];
@@ -287,18 +328,29 @@ var HttpResponse = class {
287
328
  this.setHeader("Transfer-Encoding", "chunked");
288
329
  return this.send(chunk, encoding, true);
289
330
  }
290
- async send(chunk, encoding, _flush = false) {
331
+ /**
332
+ * Écrit le corps sur le flux et, pour une réponse unique, la termine.
333
+ *
334
+ * Synchrone pour une réponse unique ou une redirection : `end(corps)` est un
335
+ * appel synchrone de Node, rien n'y est à attendre (#505). Une promesse
336
+ * seulement en streaming chunké (`flush()`), réglée quand le flux accepte
337
+ * l'écriture suivante (`drain`) — c'est la contre-pression.
338
+ *
339
+ * @param chunk - le corps (posé par `setBody`) ; absent, le corps déjà posé.
340
+ * @param encoding - l'encodage d'un corps texte.
341
+ * @returns la réponse, ou sa promesse en streaming chunké.
342
+ * @throws de façon SYNCHRONE quand la réponse Node n'existe plus.
343
+ */
344
+ send(chunk, encoding, _flush = false) {
291
345
  if (this.context.isRedirect) {
292
346
  if (!this.response?.headersSent) this.writeHead();
293
- await this.end();
294
- return this;
347
+ return thenMaybe(this.end(), () => this);
295
348
  }
296
- if (chunk) this.setBody(chunk);
349
+ if (chunk) this.setBody(chunk, encoding);
297
350
  if (!this.response) throw new Error(`Http Response not found`);
298
- if (this.body === null) this.body = Buffer.alloc(0);
299
351
  const res = this.response;
300
352
  if (!_flush && !this.flushing && !res.writableEnded) {
301
- res.end(this.body, encoding || this.encoding);
353
+ res.end(this.payload, this.payloadEncoding);
302
354
  return this;
303
355
  }
304
356
  return new Promise((resolve) => {
@@ -310,7 +362,7 @@ var HttpResponse = class {
310
362
  }
311
363
  };
312
364
  const onDrain = () => done();
313
- if (res.write(this.body, encoding || this.encoding, (error) => {
365
+ if (res.write(this.payload, this.payloadEncoding, (error) => {
314
366
  if (error) {
315
367
  this.log(error, "ERROR");
316
368
  res.removeListener("drain", onDrain);
@@ -320,17 +372,21 @@ var HttpResponse = class {
320
372
  else res.once("drain", onDrain);
321
373
  });
322
374
  }
323
- async write(chunk, encoding) {
324
- return await this.send(chunk, encoding || this.encoding);
375
+ write(chunk, encoding) {
376
+ return this.send(chunk, encoding || this.encoding);
325
377
  }
326
378
  writeContinue() {
327
379
  return this.response?.writeContinue();
328
380
  }
329
- async end(chunk, encoding) {
330
- return new Promise((resolve, reject) => {
331
- if (this.response) return resolve(this.response.end(chunk, encoding || this.encoding));
332
- return reject(/* @__PURE__ */ new Error(`response not found`));
333
- });
381
+ /**
382
+ * Termine la réponse Node — un appel synchrone, rien n'y est à attendre (#505).
383
+ *
384
+ * @returns le flux terminé (valeur de `ServerResponse.end`).
385
+ * @throws de façon SYNCHRONE quand la réponse Node n'existe plus.
386
+ */
387
+ end(chunk, encoding) {
388
+ if (!this.response) throw new Error(`response not found`);
389
+ return this.response.end(chunk, encoding || this.encoding);
334
390
  }
335
391
  getHeader(name) {
336
392
  return this.response?.getHeader(name);
@@ -19,7 +19,7 @@ var Parser = class {
19
19
  }
20
20
  write(buffer) {
21
21
  if (this.aborted) return buffer;
22
- const max = this.request.maxBodySize ?? 0;
22
+ const max = this.request.maxBodySize;
23
23
  this.received += buffer.length;
24
24
  if (max > 0 && this.received > max) {
25
25
  this.aborted = true;
@@ -78,7 +78,7 @@ var ParserQs = class extends Parser {
78
78
  charset = "utf8";
79
79
  constructor(request) {
80
80
  super(request);
81
- this.parserOptions = this.request.queryStringOptions || {};
81
+ this.parserOptions = this.request.queryStringOptions ?? {};
82
82
  this.charset = this.request.charset;
83
83
  }
84
84
  async parse() {
@@ -151,13 +151,15 @@ const ACCEPT_ANY = [{
151
151
  * corrige aussi la négociation (match du token ENTIER, pas d'une sous-chaîne).
152
152
  */
153
153
  const acceptMatcher = (token) => !token || token === "*" ? /.*/ : new RegExp(`^${escapeRegExp(token)}$`);
154
+ /** Qualité `q` d'une entrée `Accept` : sa valeur si c'est un nombre fini, 1 sinon. */
155
+ const qualityOf = (entry) => typeof entry.q === "number" && Number.isFinite(entry.q) ? entry.q : 1;
154
156
  const acceptParser = function(acc) {
155
157
  if (!acc) return ACCEPT_ANY;
156
158
  const arr = [];
157
159
  try {
158
160
  const types = acc.split(",");
159
- for (let i = 0; i < types.length; i++) {
160
- const type = types[i].split(";");
161
+ for (const segment of types) {
162
+ const type = segment.split(";");
161
163
  const mine = type.shift();
162
164
  if (!mine) continue;
163
165
  const dec = mine.trim().split("/");
@@ -167,8 +169,8 @@ const acceptParser = function(acc) {
167
169
  type: acceptMatcher(ele1),
168
170
  subtype: acceptMatcher(ele2)
169
171
  };
170
- for (let j = 0; j < type.length; j++) {
171
- const params = type[j].split("=");
172
+ for (const param of type) {
173
+ const params = param.split("=");
172
174
  const name = params.shift();
173
175
  const value = params.shift();
174
176
  obj[name] = name === "q" ? parseFloat(value) : value;
@@ -176,10 +178,7 @@ const acceptParser = function(acc) {
176
178
  arr.push(obj);
177
179
  }
178
180
  if (arr.length === 0) return ACCEPT_ANY;
179
- return arr.sort((a, b) => {
180
- const qA = a.q || 1;
181
- return (b.q || 1) - qA;
182
- });
181
+ return arr.sort((a, b) => qualityOf(b) - qualityOf(a));
183
182
  } catch {
184
183
  return ACCEPT_ANY;
185
184
  }
@@ -81,8 +81,7 @@ function isCanonicalAuthority(host, scheme) {
81
81
  }
82
82
  const labels = name.split(".");
83
83
  for (const label of labels) if (label.length === 0) return false;
84
- const last = labels[labels.length - 1];
85
- if (isDigits(last)) {
84
+ if (isDigits(labels.at(-1) ?? "")) {
86
85
  if (labels.length !== 4) return false;
87
86
  for (const label of labels) {
88
87
  if (!isDigits(label) || label.length > 3) return false;