@nodefony/http 10.0.0-alpha.9 → 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 (94) hide show
  1. package/README.md +7 -0
  2. package/dist/index.js +4 -2
  3. package/dist/nodefony/command/assetsPublishCommand.js +1 -1
  4. package/dist/nodefony/command/certificatesCommand.js +1 -1
  5. package/dist/nodefony/command/networkCommand.js +1 -1
  6. package/dist/nodefony/command/proxyGenerateCommand.js +1 -1
  7. package/dist/nodefony/config/config.js +22 -1
  8. package/dist/nodefony/interfaces/IRouting.js +1 -0
  9. package/dist/nodefony/interfaces/ISecurity.js +1 -0
  10. package/dist/nodefony/service/ProfilerAdminApi.js +2 -1
  11. package/dist/nodefony/service/audit-logger.js +16 -13
  12. package/dist/nodefony/service/certificates.js +25 -13
  13. package/dist/nodefony/service/error-renderer.js +11 -8
  14. package/dist/nodefony/service/http-kernel.js +340 -163
  15. package/dist/nodefony/service/pretty-request-logger.js +3 -10
  16. package/dist/nodefony/service/servers/clientError.js +2 -2
  17. package/dist/nodefony/service/servers/server-http.js +20 -20
  18. package/dist/nodefony/service/servers/server-https.js +38 -39
  19. package/dist/nodefony/service/servers/server-static.js +13 -17
  20. package/dist/nodefony/service/servers/server-websocket-secure.js +8 -7
  21. package/dist/nodefony/service/servers/server-websocket.js +8 -7
  22. package/dist/nodefony/service/servers/wsHeartbeat.js +2 -2
  23. package/dist/nodefony/service/sessions/sessions-service.js +19 -12
  24. package/dist/nodefony/service/trace.js +1 -1
  25. package/dist/nodefony/service/upload/upload-service.js +8 -7
  26. package/dist/nodefony/src/assets/prebuiltUi.js +1 -1
  27. package/dist/nodefony/src/context/Context.js +34 -19
  28. package/dist/nodefony/src/context/forwarded.js +4 -4
  29. package/dist/nodefony/src/context/http/HttpContext.js +133 -88
  30. package/dist/nodefony/src/context/http/Request.js +93 -44
  31. package/dist/nodefony/src/context/http/Response.js +107 -51
  32. package/dist/nodefony/src/context/http/parser.js +9 -10
  33. package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
  34. package/dist/nodefony/src/context/http2/Response.js +53 -25
  35. package/dist/nodefony/src/context/metaData.js +1 -1
  36. package/dist/nodefony/src/context/trustProxy.js +4 -3
  37. package/dist/nodefony/src/context/websocket/Response.js +13 -6
  38. package/dist/nodefony/src/context/websocket/WebsocketContext.js +59 -26
  39. package/dist/nodefony/src/context/websocket/wsCloseReason.js +41 -0
  40. package/dist/nodefony/src/cookies/cookie.js +3 -3
  41. package/dist/nodefony/src/errors/httpError.js +6 -6
  42. package/dist/nodefony/src/errors/thrownMessage.js +27 -0
  43. package/dist/nodefony/src/profiler/FrameProfile.js +4 -3
  44. package/dist/nodefony/src/profiler/Profiler.js +4 -4
  45. package/dist/nodefony/src/servers/kernelServers.js +29 -0
  46. package/dist/nodefony/src/servers/portBinder.js +8 -21
  47. package/dist/nodefony/src/session/session.js +18 -10
  48. package/dist/nodefony/src/session/sessionStoreFailure.js +2 -1
  49. package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +24 -9
  50. package/dist/types/index.d.ts +16 -0
  51. package/dist/types/nodefony/config/config.d.ts +22 -0
  52. package/dist/types/nodefony/interfaces/IContext.d.ts +24 -7
  53. package/dist/types/nodefony/interfaces/ICookie.d.ts +18 -18
  54. package/dist/types/nodefony/interfaces/IHttpKernel.d.ts +10 -4
  55. package/dist/types/nodefony/interfaces/IResponse.d.ts +1 -1
  56. package/dist/types/nodefony/interfaces/IRouting.d.ts +87 -0
  57. package/dist/types/nodefony/interfaces/ISecurity.d.ts +45 -0
  58. package/dist/types/nodefony/interfaces/ISession.d.ts +8 -8
  59. package/dist/types/nodefony/interfaces/IUpload.d.ts +9 -9
  60. package/dist/types/nodefony/interfaces/index.d.ts +2 -0
  61. package/dist/types/nodefony/service/audit-logger.d.ts +14 -10
  62. package/dist/types/nodefony/service/certificates.d.ts +10 -10
  63. package/dist/types/nodefony/service/http-kernel.d.ts +192 -42
  64. package/dist/types/nodefony/service/servers/server-http.d.ts +4 -1
  65. package/dist/types/nodefony/service/servers/server-https.d.ts +4 -1
  66. package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +4 -1
  67. package/dist/types/nodefony/service/servers/server-websocket.d.ts +4 -1
  68. package/dist/types/nodefony/service/sessions/sessions-service.d.ts +6 -2
  69. package/dist/types/nodefony/service/upload/upload-service.d.ts +1 -1
  70. package/dist/types/nodefony/src/context/Context.d.ts +21 -10
  71. package/dist/types/nodefony/src/context/forwarded.d.ts +3 -3
  72. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +75 -18
  73. package/dist/types/nodefony/src/context/http/Request.d.ts +39 -2
  74. package/dist/types/nodefony/src/context/http/Response.d.ts +47 -7
  75. package/dist/types/nodefony/src/context/http/parser.d.ts +1 -1
  76. package/dist/types/nodefony/src/context/http2/Response.d.ts +32 -2
  77. package/dist/types/nodefony/src/context/websocket/Response.d.ts +4 -4
  78. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +11 -2
  79. package/dist/types/nodefony/src/context/websocket/wsCloseReason.d.ts +21 -0
  80. package/dist/types/nodefony/src/cookies/cookie.d.ts +9 -9
  81. package/dist/types/nodefony/src/errors/httpError.d.ts +7 -7
  82. package/dist/types/nodefony/src/errors/thrownMessage.d.ts +12 -0
  83. package/dist/types/nodefony/src/profiler/Profiler.d.ts +28 -28
  84. package/dist/types/nodefony/src/servers/kernelServers.d.ts +20 -0
  85. package/dist/types/nodefony/src/servers/portBinder.d.ts +6 -19
  86. package/dist/types/nodefony/src/session/session.d.ts +12 -12
  87. package/dist/types/nodefony/src/session/storage/RevocationGuardStorage.d.ts +1 -1
  88. package/docs/cookies.md +20 -20
  89. package/docs/observabilite.md +25 -25
  90. package/docs/rate-limit.md +18 -18
  91. package/docs/servers.md +52 -52
  92. package/docs/session.md +65 -60
  93. package/docs/upload.md +36 -30
  94. package/package.json +7 -7
@@ -141,7 +141,7 @@ var Context = class extends Service {
141
141
  scheme;
142
142
  webSocketState = null;
143
143
  constructor(container, type) {
144
- super(`${type}`, container);
144
+ super(type, container);
145
145
  if (PERF_PROBE_SUB) {
146
146
  const p = globalThis.__nfPerfProbe;
147
147
  if (p && p.t0 !== 0n) p.svcNs += Number(process.hrtime.bigint() - p.t0);
@@ -227,7 +227,7 @@ var Context = class extends Service {
227
227
  }
228
228
  phaseStart(name) {
229
229
  if (!this._timingEnabled) return;
230
- if (this._phaseIndex === null) this._phaseIndex = /* @__PURE__ */ new Map();
230
+ this._phaseIndex ??= /* @__PURE__ */ new Map();
231
231
  const idx = this.phases.length;
232
232
  this.phases.push({
233
233
  name,
@@ -239,8 +239,8 @@ var Context = class extends Service {
239
239
  if (!this._timingEnabled || this._phaseIndex === null) return;
240
240
  const idx = this._phaseIndex.get(name);
241
241
  if (idx === void 0) return;
242
- const p = this.phases[idx];
243
- if (p.endMs !== void 0) return;
242
+ const p = this.phases.at(idx);
243
+ if (p === void 0 || p.endMs !== void 0) return;
244
244
  p.endMs = performance.now();
245
245
  p.durationMs = p.endMs - p.startMs;
246
246
  }
@@ -250,15 +250,28 @@ var Context = class extends Service {
250
250
  Promise.resolve().then(() => boundFn(this)).catch((e) => this.log(e, "ERROR", "onAfterResponse(late)"));
251
251
  return;
252
252
  }
253
- if (this._afterResponseFns === null) this._afterResponseFns = [];
253
+ this._afterResponseFns ??= [];
254
254
  this._afterResponseFns.push(boundFn);
255
255
  }
256
- async _runAfterResponse() {
257
- if (this._afterResponseFired) return;
256
+ /**
257
+ * Exécute les hooks `onAfterResponse`, une seule fois, dans l'ordre
258
+ * d'enregistrement ; un hook qui lève est journalisé sans arrêter les suivants.
259
+ *
260
+ * @returns `undefined` quand aucun hook n'est enregistré (le cas nominal) ou
261
+ * qu'ils ont déjà tourné : rien à attendre, le démontage continue sans
262
+ * suspension. Une `Promise` sinon, réglée quand le dernier hook a fini — elle
263
+ * ne rejette jamais.
264
+ */
265
+ _runAfterResponse() {
266
+ if (this._afterResponseFired) return void 0;
258
267
  this._afterResponseFired = true;
259
268
  const fns = this._afterResponseFns;
260
- if (fns === null || fns.length === 0) return;
269
+ if (fns === null || fns.length === 0) return void 0;
261
270
  this._afterResponseFns = null;
271
+ return this.runAfterResponseHooks(fns);
272
+ }
273
+ /** Boucle des hooks `onAfterResponse` — séquentielle, erreurs journalisées. */
274
+ async runAfterResponseHooks(fns) {
262
275
  for (const fn of fns) try {
263
276
  await fn(this);
264
277
  } catch (e) {
@@ -298,7 +311,7 @@ var Context = class extends Service {
298
311
  * notable → INFO (EVENT_SEVERITY), event technique → DEBUG.
299
312
  */
300
313
  logEvent(event) {
301
- if (lifecycleEventLogging === null) lifecycleEventLogging = this.kernel?.environment !== "production";
314
+ lifecycleEventLogging ??= this.kernel?.environment !== "production";
302
315
  if (!lifecycleEventLogging) return;
303
316
  this.log(`${colorLogEvent()} ${event}`, EVENT_SEVERITY[event] || "DEBUG");
304
317
  }
@@ -327,7 +340,9 @@ var Context = class extends Service {
327
340
  if (logger.shouldSample && !logger.shouldSample(this, err)) return;
328
341
  const entry = logger.renderHttp(this, err);
329
342
  return this.log(entry.text, entry.severity, entry.msgid);
330
- } catch {}
343
+ } catch {
344
+ return;
345
+ }
331
346
  }
332
347
  /**
333
348
  * P3.7 — Émet au teardown un log DEBUG détaillant la durée de chaque phase du
@@ -339,18 +354,18 @@ var Context = class extends Service {
339
354
  logPhasesVerbose() {
340
355
  if (!this._timingVerbose) return;
341
356
  const phases = this.phases;
342
- const n = phases.length;
343
- if (n === 0) return;
357
+ if (phases.length === 0) return;
344
358
  let line = "";
345
359
  let total = 0;
346
- for (let i = 0; i < n; i++) {
347
- const p = phases[i];
360
+ let i = 0;
361
+ for (const p of phases) {
348
362
  const d = p.durationMs ?? (p.endMs !== void 0 ? p.endMs - p.startMs : void 0);
349
363
  if (i > 0) line += " · ";
350
364
  if (d !== void 0) {
351
365
  line += `${p.name}=${d.toFixed(2)}ms`;
352
366
  total += d;
353
367
  } else line += `${p.name}=…`;
368
+ i++;
354
369
  }
355
370
  this.log(`TRACE phases [Σ ${total.toFixed(2)}ms] ${line}`, "DEBUG", `${this.type} TIMING`);
356
371
  }
@@ -363,11 +378,12 @@ var Context = class extends Service {
363
378
  }
364
379
  }
365
380
  getRequestCookies(name) {
366
- if (name) return this.cookies[name] || null;
381
+ if (name) return Object.hasOwn(this.cookies, name) ? this.cookies[name] ?? null : null;
367
382
  return this.cookies;
368
383
  }
369
384
  setCookie(cookie) {
370
- if (cookie) return this.response?.addCookie(cookie);
385
+ const received = cookie;
386
+ if (received) return this.response?.addCookie(received);
371
387
  }
372
388
  getRequest() {
373
389
  return this.request;
@@ -387,8 +403,7 @@ var Context = class extends Service {
387
403
  return Boolean(this.cookieSession);
388
404
  }
389
405
  getCookieSession(name) {
390
- if (this.cookies[name]) return this.cookies[name];
391
- return null;
406
+ return Object.hasOwn(this.cookies, name) ? this.cookies[name] ?? null : null;
392
407
  }
393
408
  /**
394
409
  * Nom effectif du cookie de session pour CE transport. Sur TLS (https/wss) on
@@ -401,7 +416,7 @@ var Context = class extends Service {
401
416
  */
402
417
  getSessionCookieName() {
403
418
  const base = this.sessionService?.defaultSessionName ?? "nodefony";
404
- const mode = (this.sessionService?.options?.cookie)?.hostPrefix ?? "auto";
419
+ const mode = (this.sessionService?.options.cookie)?.hostPrefix ?? "auto";
405
420
  const tls = this.scheme === "https" || this.scheme === "wss";
406
421
  return mode === true || mode === "auto" && tls ? `__Host-${base}` : base;
407
422
  }
@@ -8,7 +8,7 @@ import { isIPv4 } from "node:net";
8
8
  * un point de coupe. Les `\"` échappés à l'intérieur des guillemets sont préservés.
9
9
  */
10
10
  function splitTopLevel(input, sep) {
11
- if (input.indexOf("\"") === -1) return input.split(sep);
11
+ if (!input.includes("\"")) return input.split(sep);
12
12
  const out = [];
13
13
  let buf = "";
14
14
  let inQuote = false;
@@ -43,7 +43,7 @@ function splitTopLevel(input, sep) {
43
43
  /** Retire les guillemets d'une `quoted-string` et déséchappe `\x` (RFC 7230 §3.2.6). */
44
44
  function unquote(value) {
45
45
  const v = value.trim();
46
- if (v.length >= 2 && v[0] === "\"" && v[v.length - 1] === "\"") return v.slice(1, -1).replace(/\\(.)/gu, "$1");
46
+ if (v.length >= 2 && v.startsWith("\"") && v.endsWith("\"")) return v.slice(1, -1).replace(/\\(.)/gu, "$1");
47
47
  return v;
48
48
  }
49
49
  /**
@@ -104,11 +104,11 @@ function forwardedNodeIp(node) {
104
104
  if (!node) return null;
105
105
  const n = node.trim();
106
106
  if (!n) return null;
107
- if (n[0] === "[") {
107
+ if (n.startsWith("[")) {
108
108
  const end = n.indexOf("]");
109
109
  return end > 1 ? n.slice(1, end) : null;
110
110
  }
111
- if (n === "unknown" || n[0] === "_") return null;
111
+ if (n === "unknown" || n.startsWith("_")) return null;
112
112
  const colon = n.indexOf(":");
113
113
  const host = colon === -1 ? n : n.slice(0, colon);
114
114
  return isIPv4(host) ? host : host || null;
@@ -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");