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

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 (100) hide show
  1. package/README.md +12 -3
  2. package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorateMetadata.js +1 -1
  4. package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.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 -2
  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 +26 -4
  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 +87 -144
  16. package/dist/nodefony/service/error-renderer.js +11 -8
  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 +22 -15
  27. package/dist/nodefony/service/trace.js +3 -2
  28. package/dist/nodefony/service/upload/upload-service.js +11 -10
  29. package/dist/nodefony/service/x509.js +324 -0
  30. package/dist/nodefony/src/assets/prebuiltUi.js +1 -1
  31. package/dist/nodefony/src/context/Context.js +34 -19
  32. package/dist/nodefony/src/context/forwarded.js +4 -4
  33. package/dist/nodefony/src/context/http/HttpContext.js +133 -88
  34. package/dist/nodefony/src/context/http/Request.js +108 -46
  35. package/dist/nodefony/src/context/http/Response.js +134 -54
  36. package/dist/nodefony/src/context/http/parser.js +9 -10
  37. package/dist/nodefony/src/context/http/urlFastPath.js +35 -11
  38. package/dist/nodefony/src/context/http2/Response.js +53 -25
  39. package/dist/nodefony/src/context/metaData.js +1 -1
  40. package/dist/nodefony/src/context/trustProxy.js +4 -3
  41. package/dist/nodefony/src/context/websocket/Response.js +13 -6
  42. package/dist/nodefony/src/context/websocket/WebsocketContext.js +59 -26
  43. package/dist/nodefony/src/context/websocket/wsCloseReason.js +41 -0
  44. package/dist/nodefony/src/cookies/cookie.js +3 -3
  45. package/dist/nodefony/src/errors/httpError.js +6 -6
  46. package/dist/nodefony/src/errors/thrownMessage.js +27 -0
  47. package/dist/nodefony/src/profiler/FrameProfile.js +4 -3
  48. package/dist/nodefony/src/profiler/Profiler.js +4 -4
  49. package/dist/nodefony/src/servers/kernelServers.js +29 -0
  50. package/dist/nodefony/src/servers/portBinder.js +65 -23
  51. package/dist/nodefony/src/session/session.js +18 -10
  52. package/dist/nodefony/src/session/sessionStoreFailure.js +2 -1
  53. package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +24 -9
  54. package/dist/types/index.d.ts +16 -0
  55. package/dist/types/nodefony/config/config.d.ts +23 -0
  56. package/dist/types/nodefony/interfaces/IContext.d.ts +24 -7
  57. package/dist/types/nodefony/interfaces/ICookie.d.ts +18 -18
  58. package/dist/types/nodefony/interfaces/IHttpKernel.d.ts +10 -4
  59. package/dist/types/nodefony/interfaces/IResponse.d.ts +1 -1
  60. package/dist/types/nodefony/interfaces/IRouting.d.ts +87 -0
  61. package/dist/types/nodefony/interfaces/ISecurity.d.ts +45 -0
  62. package/dist/types/nodefony/interfaces/ISession.d.ts +8 -8
  63. package/dist/types/nodefony/interfaces/IUpload.d.ts +9 -9
  64. package/dist/types/nodefony/interfaces/index.d.ts +2 -0
  65. package/dist/types/nodefony/service/audit-logger.d.ts +14 -10
  66. package/dist/types/nodefony/service/certificates.d.ts +42 -57
  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/service/x509.d.ts +86 -0
  75. package/dist/types/nodefony/src/context/Context.d.ts +21 -10
  76. package/dist/types/nodefony/src/context/forwarded.d.ts +3 -3
  77. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +75 -18
  78. package/dist/types/nodefony/src/context/http/Request.d.ts +39 -2
  79. package/dist/types/nodefony/src/context/http/Response.d.ts +47 -7
  80. package/dist/types/nodefony/src/context/http/parser.d.ts +1 -1
  81. package/dist/types/nodefony/src/context/http2/Response.d.ts +32 -2
  82. package/dist/types/nodefony/src/context/websocket/Response.d.ts +4 -4
  83. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +11 -2
  84. package/dist/types/nodefony/src/context/websocket/wsCloseReason.d.ts +21 -0
  85. package/dist/types/nodefony/src/cookies/cookie.d.ts +9 -9
  86. package/dist/types/nodefony/src/errors/httpError.d.ts +7 -7
  87. package/dist/types/nodefony/src/errors/thrownMessage.d.ts +12 -0
  88. package/dist/types/nodefony/src/profiler/Profiler.d.ts +28 -28
  89. package/dist/types/nodefony/src/servers/kernelServers.d.ts +20 -0
  90. package/dist/types/nodefony/src/servers/portBinder.d.ts +16 -65
  91. package/dist/types/nodefony/src/session/session.d.ts +12 -12
  92. package/dist/types/nodefony/src/session/storage/RevocationGuardStorage.d.ts +1 -1
  93. package/docs/cookies.md +25 -25
  94. package/docs/index.md +4 -2
  95. package/docs/observabilite.md +27 -27
  96. package/docs/rate-limit.md +20 -20
  97. package/docs/servers.md +82 -79
  98. package/docs/session.md +74 -69
  99. package/docs/upload.md +49 -43
  100. package/package.json +9 -12
@@ -7,7 +7,7 @@ import HttpResponse from "./Response.js";
7
7
  import Http2Request from "../http2/Request.js";
8
8
  import Http2Response from "../http2/Response.js";
9
9
  import { describeSessionStoreFailure } from "../../session/sessionStoreFailure.js";
10
- import { extend, typeOf } from "nodefony";
10
+ import { extend, finallyMaybe, isPromise, thenMaybe, typeOf } from "nodefony";
11
11
  import url, { URL } from "node:url";
12
12
  //#region nodefony/src/context/http/HttpContext.ts
13
13
  const socketActiveContext = /* @__PURE__ */ new WeakMap();
@@ -20,7 +20,6 @@ var HttpContext = class extends Context {
20
20
  request;
21
21
  response;
22
22
  uploadService;
23
- csrf;
24
23
  constructor(container, request, response, type) {
25
24
  super(container, type);
26
25
  this.uploadService = this.get("upload");
@@ -97,18 +96,28 @@ var HttpContext = class extends Context {
97
96
  if (req.hasParsedUrl) return req.url.protocol.replace(":", "");
98
97
  return req.scheme;
99
98
  }
100
- async handle() {
99
+ /**
100
+ * Sert la requête : délai de réponse, hooks `onRequest`, puis l'action
101
+ * résolue et son rendu (`resolver.callController()`).
102
+ *
103
+ * Pas `async` : synchrone quand l'action et son rendu le sont, rien n'y est
104
+ * alors attendu (#505).
105
+ *
106
+ * @returns ce que rend `callController()` — le résultat du rendu, pas le
107
+ * contexte (comportement historique, conservé) —, ou sa promesse. Sur une
108
+ * redirection déjà posée : le contexte, une fois l'envoi fait.
109
+ * @throws HttpError 404, de façon SYNCHRONE, quand aucune route ne résout.
110
+ */
111
+ handle() {
101
112
  this.setTimeout();
102
- if (this.isRedirect) {
103
- await this.send();
104
- return this;
105
- }
113
+ if (this.isRedirect) return thenMaybe(this.send(), () => this);
106
114
  this.fire("onRequest", this);
107
- this.kernel?.fire("onRequest", this);
115
+ const kernel = this.kernel;
116
+ if (kernel && (kernel.debug || kernel.listenerCount("onRequest") > 0)) kernel.fire("onRequest", this);
108
117
  if (!this.resolver && this.router) this.resolver = this.router.resolve(this);
109
- if (this.resolver && this.resolver.resolve) {
118
+ if (this.resolver?.resolve) {
110
119
  this.setMetaData();
111
- return await this.resolver.callController();
120
+ return this.resolver.callController();
112
121
  }
113
122
  throw new HttpError("", 404, this);
114
123
  }
@@ -117,7 +126,7 @@ var HttpContext = class extends Context {
117
126
  if (!res) return;
118
127
  if (this.response.stream) {
119
128
  res.setTimeout(this.response.timeout, () => {
120
- if (!this.response?.response?.writableEnded) this._onTimeout();
129
+ if (!this.response.response?.writableEnded) this._onTimeout();
121
130
  });
122
131
  return;
123
132
  }
@@ -130,27 +139,28 @@ var HttpContext = class extends Context {
130
139
  socketTimeoutArmed.add(socket);
131
140
  socket.on("timeout", () => {
132
141
  const ctx = socketActiveContext.get(socket);
133
- if (ctx && !ctx.response?.response?.writableEnded) ctx._onTimeout();
142
+ if (ctx && !ctx.response.response?.writableEnded) ctx._onTimeout();
134
143
  });
135
144
  }
136
145
  }
137
- async render(chunk, encoding, status, headers) {
146
+ /**
147
+ * Sérialise la donnée selon son type (JSON, HTML, texte), pose statut et
148
+ * en-têtes, puis l'envoie ({@link send}). Synchrone quand l'envoi l'est (#505).
149
+ *
150
+ * @returns la réponse, ou sa promesse.
151
+ * @throws de façon SYNCHRONE ce que lèvent la sérialisation ou l'envoi.
152
+ */
153
+ render(chunk, encoding, status, headers) {
138
154
  let data = chunk;
139
- switch (true) {
140
- case this.isJson:
155
+ if (this.isJson) data = JSON.stringify(chunk);
156
+ else {
157
+ const type = typeOf(chunk);
158
+ if (type === "object") {
159
+ this.setContextJson();
141
160
  data = JSON.stringify(chunk);
142
- break;
143
- case this.isHtml:
144
- default: switch (typeOf(chunk)) {
145
- case "object":
146
- this.setContextJson();
147
- data = JSON.stringify(chunk);
148
- break;
149
- case "string":
150
- if (this.response.contentType === "application/octet-stream") this.setContextHtml();
151
- break;
152
- default: if (this.response.contentType === "application/octet-stream") this.response.setContentType("text");
153
- }
161
+ } else if (type === "string") {
162
+ if (this.response.contentType === "application/octet-stream") this.setContextHtml();
163
+ } else if (this.response.contentType === "application/octet-stream") this.response.setContentType("text");
154
164
  }
155
165
  if (headers) this.response.setHeaders(headers);
156
166
  if (status) this.response.setStatusCode(status);
@@ -161,83 +171,118 @@ var HttpContext = class extends Context {
161
171
  this.log(describeSessionStoreFailure(e).message, "CRITIC", "SESSION-STORE");
162
172
  return null;
163
173
  }).then(async (_session) => {
164
- return this.close().catch((e) => {
165
- throw e;
166
- });
174
+ return this.close();
167
175
  });
168
176
  }
169
- async send(chunk, encoding) {
177
+ /**
178
+ * Envoie la réponse : session sauvée, hook `onSend`, en-têtes, corps, puis
179
+ * fin de la réponse.
180
+ *
181
+ * Synchrone dans le cas nominal — aucune session ouverte, aucun écouteur
182
+ * `onSend`, réponse unique : rien n'y est à attendre (#505). Une promesse
183
+ * seulement quand une étape attend réellement (écriture du store de session,
184
+ * hook asynchrone, streaming chunké, flux HTTP/2).
185
+ *
186
+ * @param chunk - le corps ; absent, le corps déjà posé sur la réponse.
187
+ * @param encoding - l'encodage d'un corps texte.
188
+ * @returns la réponse, ou sa promesse.
189
+ * @throws de façon SYNCHRONE `Response Already sended` quand la réponse est
190
+ * déjà partie, et ce que lève une étape synchrone de l'envoi.
191
+ */
192
+ send(chunk, encoding) {
170
193
  if (this.finished && !this.sended) {
171
194
  this.log("send() on finished context — client disconnected", "DEBUG");
172
195
  return this.response;
173
196
  }
174
- if (this.sended || this.finished || this.response.isHeaderSent()) return new Promise((_resolve, reject) => {
175
- return reject(/* @__PURE__ */ new Error("Response Already sended"));
176
- });
177
- if (!this.timingEnabled) return this.#doSend(chunk, encoding);
197
+ if (this.sended || this.finished || this.response.isHeaderSent()) throw new Error("Response Already sended");
198
+ if (!this.timingEnabled) return this.doSend(chunk, encoding);
178
199
  this.phaseStart("send");
179
- try {
180
- return await this.#doSend(chunk, encoding);
181
- } finally {
182
- this.phaseEnd("send");
183
- }
200
+ return finallyMaybe(() => this.doSend(chunk, encoding), () => this.phaseEnd("send"));
184
201
  }
185
- async #doSend(chunk, encoding) {
202
+ /**
203
+ * Le travail de {@link send}, journalisé en ERROR quand il échoue — de façon
204
+ * synchrone ou par rejet, selon l'étape qui échoue.
205
+ */
206
+ doSend(chunk, encoding) {
207
+ let sent;
186
208
  try {
187
- let body = chunk;
188
- if (this.session != null) try {
189
- await this.saveSession();
190
- } catch (e) {
191
- const failure = describeSessionStoreFailure(e);
192
- this.log(failure.message, "CRITIC", "SESSION-STORE");
193
- this.response.statusCode = failure.statusCode;
194
- body = failure.body;
195
- }
196
- if (body) this.response?.setBody(body);
197
- if (this.listenerCount("onSend") > 0) await this.fireAsync("onSend", this.response, this);
198
- try {
199
- this.writeHead();
200
- } catch (e) {
201
- this.log(e, "WARNING");
202
- }
203
- if (this.isRedirect) return await this.close();
204
- return await this.write(body, encoding);
209
+ sent = this.sendSteps(chunk, encoding);
205
210
  } catch (error) {
206
211
  this.log(error, "ERROR");
207
212
  throw error;
208
213
  }
214
+ if (!isPromise(sent)) return sent;
215
+ return sent.catch((error) => {
216
+ this.log(error, "ERROR");
217
+ throw error;
218
+ });
219
+ }
220
+ /** Session, puis hook `onSend`, puis en-têtes et corps — chacun attendu seulement s'il attend. */
221
+ sendSteps(chunk, encoding) {
222
+ if (this.session != null) return this.saveSession().then(() => this.emitAndWrite(chunk, encoding), (e) => {
223
+ const failure = describeSessionStoreFailure(e);
224
+ this.log(failure.message, "CRITIC", "SESSION-STORE");
225
+ this.response.statusCode = failure.statusCode;
226
+ return this.emitAndWrite(failure.body, encoding);
227
+ });
228
+ return this.emitAndWrite(chunk, encoding);
229
+ }
230
+ /** Hook `onSend` (attendu seulement s'il a un écouteur), puis en-têtes et corps. */
231
+ emitAndWrite(body, encoding) {
232
+ if (body) this.response.setBody(body);
233
+ if (this.listenerCount("onSend") > 0) return this.fireAsync("onSend", this.response, this).then(() => this.writeHeadAndBody(body, encoding));
234
+ return this.writeHeadAndBody(body, encoding);
235
+ }
236
+ /** En-têtes, puis corps — ou la seule fin de réponse pour une redirection. */
237
+ writeHeadAndBody(body, encoding) {
238
+ try {
239
+ this.writeHead();
240
+ } catch (e) {
241
+ this.log(e, "WARNING");
242
+ }
243
+ if (this.isRedirect) return this.close();
244
+ return this.write(body, encoding);
209
245
  }
210
246
  writeHead(statusCode, headers) {
211
- if (this.response) {
212
- if (this.csrfToken) this.setCookie(new Cookie("csrf-token", this.csrfToken, {
213
- httpOnly: false,
214
- sameSite: "Strict",
215
- secure: this.scheme === "https",
216
- path: "/"
217
- }));
218
- this.response.setCookies();
219
- this.response.writeHead(statusCode, headers);
220
- }
247
+ if (this.csrfToken) this.setCookie(new Cookie("csrf-token", this.csrfToken, {
248
+ httpOnly: false,
249
+ sameSite: "Strict",
250
+ secure: this.scheme === "https",
251
+ path: "/"
252
+ }));
253
+ this.response.setCookies();
254
+ this.response.writeHead(statusCode, headers);
221
255
  }
222
- async write(chunk, encoding, _flush = false) {
223
- await this.response.send(chunk, encoding || this.response.encoding).then(() => {
256
+ /**
257
+ * Écrit le corps, puis termine la requête ({@link close}). Synchrone pour une
258
+ * réponse unique (#505).
259
+ *
260
+ * @returns la réponse, ou sa promesse quand l'écriture attend.
261
+ */
262
+ write(chunk, encoding, _flush = false) {
263
+ return thenMaybe(this.response.send(chunk, encoding || this.response.encoding), () => {
224
264
  this.sended = true;
225
- }).catch((e) => {
226
- throw e;
227
- });
228
- return this.close().catch((e) => {
229
- throw e;
265
+ return this.close();
230
266
  });
231
267
  }
232
268
  flush(chunk, encoding) {
233
269
  return this.response.flush(chunk, encoding);
234
270
  }
235
- async close() {
236
- if (this.listenerCount("onClose") > 0) await this.fireAsync("onClose", this);
237
- if (this.response?.response?.writableEnded) return this.response;
238
- return this.response.end().then(() => this.response).catch((e) => {
239
- throw e;
240
- });
271
+ /**
272
+ * Termine la requête : hook `onClose`, puis fin de la réponse si l'envoi ne
273
+ * l'a pas déjà faite. Synchrone sans écouteur `onClose` (#505).
274
+ *
275
+ * @returns la réponse, ou sa promesse quand le hook attend.
276
+ */
277
+ close() {
278
+ if (this.finished) return this.response;
279
+ if (this.listenerCount("onClose") > 0) return this.fireAsync("onClose", this).then(() => this.endResponse());
280
+ return this.endResponse();
281
+ }
282
+ /** Fin de la réponse, sauf si l'envoi l'a déjà terminée. */
283
+ endResponse() {
284
+ if (this.response.response?.writableEnded) return this.response;
285
+ return thenMaybe(this.response.end(), () => this.response);
241
286
  }
242
287
  redirect(Url, status, headers) {
243
288
  if (typeof Url === "object") return this.response.redirect(url.format(Url), status, headers);
@@ -280,25 +325,25 @@ var HttpContext = class extends Context {
280
325
  return this.redirect(newUrl, status, headers);
281
326
  }
282
327
  getHostName() {
283
- return this.request?.getHostName();
328
+ return this.request.getHostName();
284
329
  }
285
330
  getRemoteAddress() {
286
- return this.request?.getRemoteAddress();
331
+ return this.request.getRemoteAddress();
287
332
  }
288
333
  getHost() {
289
- return this.request?.getHost();
334
+ return this.request.getHost();
290
335
  }
291
336
  getUserAgent() {
292
- return this.request?.getUserAgent();
337
+ return this.request.getUserAgent();
293
338
  }
294
339
  getMethod() {
295
- return this.request?.getMethod();
340
+ return this.request.getMethod();
296
341
  }
297
342
  setContentType(type, encoding) {
298
343
  return this.response.setContentType(type, encoding);
299
344
  }
300
345
  resolveIsHtml() {
301
- return this.request?.acceptHtml ?? false;
346
+ return this.request.acceptHtml;
302
347
  }
303
348
  setDefaultContentType() {
304
349
  if (this.isHtml) this.response.setContentType("html", "utf-8");
@@ -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,15 +39,34 @@ 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;
45
53
  if (p && p.t0 !== 0n) p[field] += Number(process.hrtime.bigint() - p.t0);
46
54
  }
55
+ /**
56
+ * Nom d'hôte d'une autorité brute (`host[:port]`) : tout ce qui précède le
57
+ * premier `:`. Même résultat que `host.split(":")[0]`, sans tableau (#508).
58
+ */
59
+ function hostnameOf(host) {
60
+ if (!host) return "";
61
+ const colon = host.indexOf(":");
62
+ return colon === -1 ? host : host.slice(0, colon);
63
+ }
47
64
  var HttpRequest = class {
48
65
  context;
49
66
  request;
50
67
  #url = null;
68
+ #rawHostname;
69
+ #rawHostSource;
51
70
  headers = {};
52
71
  host = "";
53
72
  method;
@@ -129,12 +148,12 @@ var HttpRequest = class {
129
148
  #acceptHtml = null;
130
149
  /** Media-ranges de l'en-tête `Accept`, parsés au premier accès (mémoïsé). */
131
150
  get accept() {
132
- if (this.#accept === null) this.#accept = acceptParser(this.headers?.accept);
151
+ this.#accept ??= acceptParser(this.headers.accept);
133
152
  return this.#accept;
134
153
  }
135
154
  /** `true` si le client accepte `text/html` — résolu au premier accès (mémoïsé). */
136
155
  get acceptHtml() {
137
- if (this.#acceptHtml === null) this.#acceptHtml = this.accepts("html");
156
+ this.#acceptHtml ??= this.accepts("html");
138
157
  return this.#acceptHtml;
139
158
  }
140
159
  origin;
@@ -147,14 +166,16 @@ var HttpRequest = class {
147
166
  this.request.body = null;
148
167
  this.headers = request.headers;
149
168
  this.origin = this.headers.origin;
150
- const checker = this.context?.httpKernel?.getTrustProxyChecker();
151
- const socketAddress = this.request.socket?.remoteAddress;
169
+ const checker = this.context.httpKernel?.getTrustProxyChecker();
170
+ const socketAddress = this.request.socket.remoteAddress;
152
171
  this.trustedProxy = !!checker?.isTrusted(socketAddress);
153
172
  if (this.trustedProxy && checker && hasForwardingHeaders(this.headers)) this.forwarded = resolveForwarded(this.headers, socketAddress, checker);
154
173
  if (PERF_PROBE_SUB) perfMark("reqProxyNs");
155
174
  this.method = this.getMethod();
156
175
  this.host = this.getHost();
157
- this.hostname = this.getHostName(this.host);
176
+ this.#rawHostSource = this.host;
177
+ this.#rawHostname = hostnameOf(this.host);
178
+ this.hostname = this.#rawHostname;
158
179
  this.sUrl = this.getFullUrl(request);
159
180
  const fast = (this.scheme === "http" || this.scheme === "https") && isCanonicalAuthority(this.host, this.scheme) ? splitTarget(this.getRawTarget()) : null;
160
181
  if (fast !== null) {
@@ -167,11 +188,9 @@ var HttpRequest = class {
167
188
  this.search = u.search;
168
189
  }
169
190
  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;
191
+ this.queryStringOptions = this.context.httpKernel?.module.options.queryString || {};
173
192
  let query;
174
- if (this.search) query = QS.parse(this.search.slice(1), this.queryStringOptions || {});
193
+ if (this.search) query = QS.parse(this.search.slice(1), this.queryStringOptions ?? {});
175
194
  else query = {};
176
195
  if (this.#url !== null) this.#url.query = query;
177
196
  this.queryGet = query;
@@ -198,32 +217,64 @@ var HttpRequest = class {
198
217
  if (this.context.listenerCount("onRequestEnd") === 0) return false;
199
218
  return this.context.fireAsync("onRequestEnd", this);
200
219
  }
201
- async initialize() {
220
+ /**
221
+ * Lit le corps de la requête, s'il y en a un, puis émet `onRequestEnd`.
222
+ *
223
+ * Le contrat de retour dit ce qui reste à ATTENDRE :
224
+ * - `undefined` — la méthode ne porte pas de corps (GET, HEAD, OPTIONS…) et
225
+ * aucun écouteur `onRequestEnd` ne rend de promesse : tout est fait, de
226
+ * façon SYNCHRONE ;
227
+ * - une `Promise` — le corps arrive du socket (POST, PUT, PATCH, DELETE), ou
228
+ * un écouteur `onRequestEnd` est à attendre. Sa valeur n'est consommée par
229
+ * personne : elle ne sert qu'au séquençage.
230
+ *
231
+ * L'appelant n'attend donc que ce qui attend vraiment
232
+ * (`const pending = request.initialize(); if (pending) await pending;`) : un
233
+ * `await` inconditionnel reportait le pipeline d'une micro-tâche, et la
234
+ * chaîne de promesses coûtait ~6 évènements asynchrones à un GET qui n'avait
235
+ * rien à lire (#505).
236
+ *
237
+ * Ne lève jamais de façon synchrone : un corps refusé (413) ou illisible rompt
238
+ * la promesse rendue, comme avant.
239
+ *
240
+ * @returns `undefined` quand tout est fait, sinon la promesse à attendre.
241
+ */
242
+ initialize() {
243
+ if (!hasRequestBody(this.method)) return this.endWithoutBody();
202
244
  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 {
245
+ if (parser instanceof ParserXml || parser instanceof ParserQs || parser instanceof Parser) {
246
+ let requestEnd;
247
+ try {
207
248
  if (this.context.finished) return;
208
249
  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();
250
+ requestEnd = this.fireRequestEnd();
217
251
  } catch (error) {
218
252
  return this.context.httpKernel?.onError(error, this.context);
219
253
  }
254
+ return requestEnd;
220
255
  }
256
+ if (!parser) return this.endWithoutBody();
221
257
  return parser;
222
- }).catch((e) => {
223
- throw e;
224
258
  });
225
259
  }
226
260
  /**
261
+ * Fin d'une requête SANS corps à lire : la marque terminée et émet
262
+ * `onRequestEnd`. Synchrone — ne rend une promesse que lorsqu'un écouteur
263
+ * `onRequestEnd` est à attendre, ou quand l'émission lève (celle d'`onError`).
264
+ *
265
+ * @returns `undefined` quand tout est fait, sinon la promesse à attendre.
266
+ */
267
+ endWithoutBody() {
268
+ if (this.context.finished) return;
269
+ try {
270
+ this.context.requestEnded = true;
271
+ const requestEnd = this.fireRequestEnd();
272
+ return requestEnd === false ? void 0 : requestEnd;
273
+ } catch (error) {
274
+ return this.context.httpKernel?.onError(error, this.context);
275
+ }
276
+ }
277
+ /**
227
278
  * B1 — Pré-contrôle de taille du corps via l'en-tête `Content-Length` :
228
279
  * rejette en 413 (RFC 9110 §15.5.14 « Content Too Large ») AVANT toute lecture
229
280
  * quand le client annonce une taille supérieure à `maxBodySize`. Rideau de
@@ -232,16 +283,28 @@ var HttpRequest = class {
232
283
  *
233
284
  * @throws {HttpError} 413 si `Content-Length` dépasse `maxBodySize`.
234
285
  */
286
+ /**
287
+ * Lit le plafond du corps et les limites d'envoi tels que les voit CETTE
288
+ * requête : la configuration http, surchargée par le calque que la requête a
289
+ * pu poser avant la lecture du corps (`overlayConfig`, #494). Hors pipeline
290
+ * (aucun scope ouvert), la configuration du module telle quelle.
291
+ */
292
+ readBodyLimits() {
293
+ const options = RequestContext.getScope() !== void 0 ? useConfig("@nodefony/http") : this.context.httpKernel?.module.options;
294
+ this.uploadOption = options?.upload ?? {};
295
+ this.maxBodySize = options?.maxBodySize ?? 0;
296
+ }
235
297
  enforceBodyLimit() {
236
298
  const max = this.maxBodySize;
237
299
  if (max <= 0) return;
238
300
  const cl = this.headers["content-length"];
239
301
  if (cl === void 0) return;
240
- const declared = parseInt(Array.isArray(cl) ? cl[0] : cl, 10);
302
+ const declared = parseInt(typeof cl === "string" ? cl : cl[0], 10);
241
303
  if (Number.isFinite(declared) && declared > max) throw new HttpError(`Request body too large: ${declared} bytes > maxBodySize ${max}`, 413, this.context);
242
304
  }
243
305
  async parseRequest() {
244
- if (!(this.method in parse)) return this.parser;
306
+ if (!hasRequestBody(this.method)) return this.parser;
307
+ this.readBodyLimits();
245
308
  if (this.contentType !== "multipart/form-data") this.enforceBodyLimit();
246
309
  switch (this.contentType) {
247
310
  case "application/xml":
@@ -349,8 +412,7 @@ var HttpRequest = class {
349
412
  stream.resume();
350
413
  return;
351
414
  }
352
- const match = reg.exec(fieldname);
353
- const field = match ? match[1] : fieldname;
415
+ const field = reg.exec(fieldname)?.[1] ?? fieldname;
354
416
  const ext = filename ? path.extname(filename) : "";
355
417
  const newFilename = `${randomUUID()}${ext}`;
356
418
  const filepath = path.join(dir, newFilename);
@@ -415,7 +477,7 @@ var HttpRequest = class {
415
477
  */
416
478
  assignField(fields, name, value) {
417
479
  const match = reg.exec(name);
418
- const key = match ? match[1] : name;
480
+ const key = match?.[1] ?? name;
419
481
  if (key in fields) {
420
482
  const cur = fields[key];
421
483
  if (Array.isArray(cur)) cur.push(value);
@@ -427,7 +489,7 @@ var HttpRequest = class {
427
489
  let subtype = "*";
428
490
  let type = "*";
429
491
  if (Type) parts = Type.split("/");
430
- if (parts) switch (parts.length) {
492
+ switch (parts.length) {
431
493
  case 1:
432
494
  subtype = parts.shift();
433
495
  break;
@@ -437,10 +499,7 @@ var HttpRequest = class {
437
499
  break;
438
500
  default: throw new Error("request accepts method bad type format");
439
501
  }
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
- }
502
+ for (const line of this.accept) if ((type === "*" || line.type.test(type)) && (subtype === "*" || line.subtype.test(subtype))) return true;
444
503
  return false;
445
504
  }
446
505
  async createFileUpload(name, file) {
@@ -454,12 +513,15 @@ var HttpRequest = class {
454
513
  getContentType(request) {
455
514
  if (request.headers["content-type"]) {
456
515
  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];
516
+ if (tab.length > 1) for (let i = 1; i < tab.length; i++) {
517
+ const part = tab[i];
518
+ if (typeof part === "string") {
519
+ const [rawKey = "", value] = part.split("=");
520
+ const key = rawKey.replace(/\s+/g, "").toLowerCase();
521
+ this.rawContentType[key] = value;
522
+ } else continue;
523
+ }
524
+ return tab[0] ?? null;
463
525
  }
464
526
  return null;
465
527
  }
@@ -485,16 +547,16 @@ var HttpRequest = class {
485
547
  }
486
548
  getHostName(host) {
487
549
  if (this.#url?.hostname) return this.#url.hostname;
488
- if (host) return host.split(":")[0];
489
- if (host = this.getHost()) return host.split(":")[0];
490
- return "";
550
+ if (host) return hostnameOf(host);
551
+ host = this.getHost();
552
+ return host === this.#rawHostSource ? this.#rawHostname : hostnameOf(host);
491
553
  }
492
554
  getHost() {
493
555
  return this.request.headers.host;
494
556
  }
495
557
  getRemoteAddress() {
496
558
  if (this.forwarded) return this.forwarded.clientIp;
497
- return this.request.socket?.remoteAddress ?? null;
559
+ return this.request.socket.remoteAddress ?? null;
498
560
  }
499
561
  /**
500
562
  * Request-target brut (origin-form) tel que reçu du transport — SOURCE
@@ -511,7 +573,7 @@ var HttpRequest = class {
511
573
  */
512
574
  resolveScheme(request) {
513
575
  if (this.forwarded?.proto) return this.forwarded.proto;
514
- if ("encrypted" in request.socket && request.socket.encrypted) return "https";
576
+ if (request.socket.encrypted) return "https";
515
577
  return "http";
516
578
  }
517
579
  getFullUrl(request = this.request) {