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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.157.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
  2. package/dist/_virtual/{_@oxc-project_runtime@0.157.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorateParam.js +1 -1
  3. package/dist/index.js +3 -2
  4. package/dist/nodefony/config/config.js +25 -2
  5. package/dist/nodefony/grpc/GrpcCall.js +298 -0
  6. package/dist/nodefony/grpc/GrpcEndpoint.js +528 -0
  7. package/dist/nodefony/grpc/index.js +4 -0
  8. package/dist/nodefony/interfaces/IRpcCall.js +1 -0
  9. package/dist/nodefony/service/audit-logger.js +52 -6
  10. package/dist/nodefony/service/error-renderer.js +41 -9
  11. package/dist/nodefony/service/http-kernel.js +198 -19
  12. package/dist/nodefony/service/pretty-request-logger.js +5 -3
  13. package/dist/nodefony/service/request-logger.js +21 -6
  14. package/dist/nodefony/service/reverse-proxy.js +1 -1
  15. package/dist/nodefony/service/servers/clientError.js +26 -1
  16. package/dist/nodefony/service/servers/server-h2c.js +3 -3
  17. package/dist/nodefony/service/servers/server-http.js +3 -3
  18. package/dist/nodefony/service/servers/server-https.js +4 -4
  19. package/dist/nodefony/service/servers/server-websocket-secure.js +3 -3
  20. package/dist/nodefony/service/servers/server-websocket.js +3 -3
  21. package/dist/nodefony/service/servers/serverShutdown.js +14 -36
  22. package/dist/nodefony/service/sessions/sessions-service.js +6 -6
  23. package/dist/nodefony/service/upload/upload-service.js +2 -2
  24. package/dist/nodefony/src/context/Context.js +12 -1
  25. package/dist/nodefony/src/context/http/ByteStream.js +9 -4
  26. package/dist/nodefony/src/context/http/HttpContext.js +86 -8
  27. package/dist/nodefony/src/context/http/Request.js +96 -16
  28. package/dist/nodefony/src/context/http/Response.js +120 -17
  29. package/dist/nodefony/src/context/http/SseStream.js +4 -4
  30. package/dist/nodefony/src/context/http/StreamLifecycle.js +328 -19
  31. package/dist/nodefony/src/context/http/parser.js +29 -11
  32. package/dist/nodefony/src/context/http/webResponse.js +56 -0
  33. package/dist/nodefony/src/context/http2/Response.js +47 -8
  34. package/dist/nodefony/src/context/requestTrace.js +6 -2
  35. package/dist/nodefony/src/context/rpcOutcome.js +20 -0
  36. package/dist/nodefony/src/context/websocket/Response.js +11 -1
  37. package/dist/nodefony/src/context/websocket/WebsocketContext.js +15 -4
  38. package/dist/nodefony/src/cookies/cookie.js +3 -1
  39. package/dist/nodefony/src/errors/httpError.js +58 -25
  40. package/dist/nodefony/src/grpc/cors.js +59 -0
  41. package/dist/nodefony/src/grpc/limits.js +16 -0
  42. package/dist/nodefony/src/grpc/refusal.js +271 -0
  43. package/dist/nodefony/src/profiler/aggregate.js +2 -2
  44. package/dist/nodefony/src/proxy/forward.js +1 -1
  45. package/dist/nodefony/src/proxy/generateProxyConfig.js +91 -15
  46. package/dist/nodefony/src/servers/http2Goaway.js +76 -0
  47. package/dist/nodefony/src/servers/http2Limits.js +41 -15
  48. package/dist/nodefony/src/servers/http2StreamGrace.js +100 -0
  49. package/dist/nodefony/src/servers/receiveDeadline.js +177 -0
  50. package/dist/types/index.d.ts +5 -2
  51. package/dist/types/nodefony/config/config.d.ts +10 -0
  52. package/dist/types/nodefony/grpc/GrpcCall.d.ts +142 -0
  53. package/dist/types/nodefony/grpc/GrpcEndpoint.d.ts +98 -0
  54. package/dist/types/nodefony/grpc/index.d.ts +15 -0
  55. package/dist/types/nodefony/interfaces/IContext.d.ts +9 -0
  56. package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +14 -0
  57. package/dist/types/nodefony/interfaces/IRequest.d.ts +2 -1
  58. package/dist/types/nodefony/interfaces/IRequestTrace.d.ts +10 -0
  59. package/dist/types/nodefony/interfaces/IRouting.d.ts +19 -0
  60. package/dist/types/nodefony/interfaces/IRpcCall.d.ts +83 -0
  61. package/dist/types/nodefony/interfaces/IUpload.d.ts +51 -4
  62. package/dist/types/nodefony/interfaces/index.d.ts +2 -1
  63. package/dist/types/nodefony/service/audit-logger.d.ts +33 -3
  64. package/dist/types/nodefony/service/error-renderer.d.ts +22 -1
  65. package/dist/types/nodefony/service/http-kernel.d.ts +21 -1
  66. package/dist/types/nodefony/service/request-logger.d.ts +10 -0
  67. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +2 -2
  68. package/dist/types/nodefony/src/context/Context.d.ts +9 -1
  69. package/dist/types/nodefony/src/context/http/ByteStream.d.ts +1 -1
  70. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +56 -2
  71. package/dist/types/nodefony/src/context/http/Request.d.ts +38 -8
  72. package/dist/types/nodefony/src/context/http/Response.d.ts +61 -7
  73. package/dist/types/nodefony/src/context/http/SseStream.d.ts +1 -1
  74. package/dist/types/nodefony/src/context/http/StreamLifecycle.d.ts +53 -0
  75. package/dist/types/nodefony/src/context/http/parser.d.ts +17 -3
  76. package/dist/types/nodefony/src/context/http/webResponse.d.ts +31 -0
  77. package/dist/types/nodefony/src/context/http2/Response.d.ts +35 -2
  78. package/dist/types/nodefony/src/context/requestTrace.d.ts +3 -1
  79. package/dist/types/nodefony/src/context/rpcOutcome.d.ts +11 -0
  80. package/dist/types/nodefony/src/context/websocket/Response.d.ts +6 -1
  81. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +8 -0
  82. package/dist/types/nodefony/src/errors/httpError.d.ts +29 -4
  83. package/dist/types/nodefony/src/grpc/cors.d.ts +32 -0
  84. package/dist/types/nodefony/src/grpc/limits.d.ts +14 -0
  85. package/dist/types/nodefony/src/grpc/refusal.d.ts +136 -0
  86. package/dist/types/nodefony/src/proxy/forward.d.ts +7 -0
  87. package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +15 -2
  88. package/dist/types/nodefony/src/servers/http2Goaway.d.ts +41 -0
  89. package/dist/types/nodefony/src/servers/http2Limits.d.ts +27 -14
  90. package/dist/types/nodefony/src/servers/http2StreamGrace.d.ts +34 -0
  91. package/dist/types/nodefony/src/servers/receiveDeadline.d.ts +77 -0
  92. package/docs/byte-stream.md +52 -17
  93. package/docs/diagnostics-channel.md +45 -1
  94. package/docs/servers.md +57 -44
  95. package/docs/upload.md +43 -17
  96. package/package.json +26 -6
@@ -1,8 +1,8 @@
1
1
  import { asServersConfig, configuredServerPort } from "../../src/servers/kernelServers.js";
2
2
  import { startHeartbeat } from "./wsHeartbeat.js";
3
3
  import { attachUpgradeDispatch, createNodefonyWebSocketServer } from "../../src/servers/upgradeDispatch.js";
4
- import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorateParam.js";
5
- import __decorate from "../../../_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorate.js";
4
+ import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorateParam.js";
5
+ import __decorate from "../../../_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorate.js";
6
6
  import { Inject, Service } from "nodefony";
7
7
  import Ws from "ws";
8
8
  // nodefony/service/servers/server-websocket.ts
@@ -50,7 +50,7 @@ let Websocket = class Websocket extends Service {
50
50
  this.httpKernel.onWebsocketUpgrade(req, admission, this.type).catch(() => {});
51
51
  });
52
52
  this.heartbeatTimer = startHeartbeat(this.server, this.options);
53
- this.kernel?.prependOnceListener("onTerminate", this.terminate.bind(this));
53
+ this.httpKernel.addShutdownStep("dismiss", () => this.terminate());
54
54
  this.ready = true;
55
55
  this.module.fire("onServersReady", this.type, this);
56
56
  return resolve(this.server);
@@ -1,5 +1,5 @@
1
+ import { ANNOUNCE_CAP_MS, announceSessionClose } from "../../src/servers/http2Goaway.js";
1
2
  import http from "node:http";
2
- import { constants } from "node:http2";
3
3
  import https from "node:https";
4
4
  import tls from "node:tls";
5
5
  // nodefony/service/servers/serverShutdown.ts
@@ -21,8 +21,8 @@ import tls from "node:tls";
21
21
  *
22
22
  * ⚠️ Les sockets WebSocket upgradées (sans réponse HTTP en cours) sont détruites
23
23
  * SANS frame Close → les serveurs WS doivent fermer leurs clients (close 1001)
24
- * AVANT ce drain. Garanti par l'ordre des listeners `onTerminate` : WS/WSS
25
- * s'attachent en `prependOnceListener`, les serveurs HTTP en `once`.
24
+ * AVANT ce drain. Garanti par `HttpKernel.drainTraffic` (ADR-0027 D2) : l'étape
25
+ * `dismiss` (1001) est attendue avant l'étape `drain`.
26
26
  *
27
27
  * @param server - serveur Node à drainer, créé AVANT son `listen` (le suivi des
28
28
  * connexions commence ici)
@@ -84,27 +84,19 @@ function createDrainTerminator(server, shutdownTimeout) {
84
84
  return terminating;
85
85
  } };
86
86
  }
87
- /** Plus long délai d'annonce (ms) : un pair qui ne répond pas au PING ne retient pas l'arrêt. */
88
- const ANNOUNCE_CAP_MS = 1e3;
89
- /** Plus grand identifiant de flux (RFC 9113 §6.8) : « aucun flux n'est encore écarté ». */
90
- const LAST_POSSIBLE_STREAM = 2 ** 31 - 1;
91
87
  /**
92
- * Annonce l'arrêt à chaque session HTTP/2 SANS écarter un flux déjà en route :
93
- * un PING mesure l'aller-retour, puis GOAWAY à `2^31-1`, puis l'attente de cet
94
- * aller-retour — après elle, tout flux ouvert par le client avant l'annonce est
95
- * arrivé. Le GOAWAY définitif (`session.close()`, dernier flux traité) suit
96
- * dans `done`. Un pair qui ne répond pas au PING est réglé au délai `cap`.
97
- * RFC 9113 §6.8 : « SHOULD send an initial GOAWAY frame with the last stream
98
- * identifier set to 2^31-1 […] After allowing time for any in-flight stream
99
- * creation (at least one round-trip time) ». Sans annonce, un flux croisé avec
100
- * le GOAWAY est refusé alors qu'il aurait pu être servi.
88
+ * Annonce l'arrêt à chaque session HTTP/2 SANS écarter un flux déjà en route —
89
+ * l'annonce en deux temps de RFC 9113 §6.8, session par session
90
+ * ({@link announceSessionClose}, la même règle que la fermeture d'une session
91
+ * inactive). Le GOAWAY définitif (`session.close()`) suit dans `done`. Un pair
92
+ * qui ne répond pas au PING est réglé au délai `cap`.
101
93
  *
102
94
  * @param sessions - sessions HTTP/2 ouvertes, `null` pour un serveur HTTP/1.1
103
95
  * @param cap - délai maximal d'attente des échos (ms)
104
96
  * @param done - appelé UNE fois, quand tous les échos sont revenus ou au délai
105
97
  */
106
98
  function announceShutdown(sessions, cap, done) {
107
- let pending = 0;
99
+ let pending = 1;
108
100
  let finished = false;
109
101
  const finish = () => {
110
102
  if (finished) return;
@@ -112,30 +104,16 @@ function announceShutdown(sessions, cap, done) {
112
104
  clearTimeout(timer);
113
105
  done();
114
106
  };
107
+ const settle = () => {
108
+ if (--pending === 0) finish();
109
+ };
115
110
  const timer = setTimeout(finish, cap);
116
111
  if (sessions !== null) for (const session of sessions) {
117
112
  if (session.closed || session.destroyed) continue;
118
- let settled = false;
119
- let wait = null;
120
- const settle = () => {
121
- if (settled) return;
122
- settled = true;
123
- if (wait !== null) clearTimeout(wait);
124
- session.removeListener("close", settle);
125
- if (--pending === 0) finish();
126
- };
127
113
  pending++;
128
- session.once("close", settle);
129
- if (!session.ping((error, rttMs) => {
130
- if (error !== null || settled || session.closed) return settle();
131
- session.goaway(constants.NGHTTP2_NO_ERROR, LAST_POSSIBLE_STREAM);
132
- wait = setTimeout(settle, Math.max(1, Math.ceil(rttMs)));
133
- })) {
134
- session.goaway(constants.NGHTTP2_NO_ERROR, LAST_POSSIBLE_STREAM);
135
- settle();
136
- }
114
+ announceSessionClose(session, settle);
137
115
  }
138
- if (pending === 0) finish();
116
+ settle();
139
117
  }
140
118
  function track(set, socket) {
141
119
  set.add(socket);
@@ -2,8 +2,8 @@ import Session from "../../src/session/session.js";
2
2
  import MemorySessionStorage from "../../src/session/storage/MemorySessionStorage.js";
3
3
  import RevocationGuardStorage from "../../src/session/storage/RevocationGuardStorage.js";
4
4
  import { SESSION_FACETS } from "../../src/session/storage/sessionFilters.js";
5
- import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorateParam.js";
6
- import __decorate from "../../../_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorate.js";
5
+ import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorateParam.js";
6
+ import __decorate from "../../../_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorate.js";
7
7
  import { AUTO_STORE, EMPTY_INFRA, GcScheduler, Inject, Nodefony, RequestContext, Service, countFacets, extend, injectable, readStoreConnector, readStoreLocation, resolveAutoStore, storeConnectivity } from "nodefony";
8
8
  import { createHash, createHmac } from "node:crypto";
9
9
  // nodefony/service/sessions/sessions-service.ts
@@ -125,10 +125,6 @@ let SessionsService = class SessionsService extends Service {
125
125
  this.module = module;
126
126
  this.certificates = this.get("certificates");
127
127
  this.defaultSessionName = this.options.name;
128
- this.once("onTerminate", () => {
129
- this.gcScheduler?.stop();
130
- if (this.storage) this.storage.close();
131
- });
132
128
  }
133
129
  async init() {
134
130
  this.secret = this.createSecret();
@@ -184,6 +180,10 @@ let SessionsService = class SessionsService extends Service {
184
180
  onError: (e) => this.log(e, "WARNING", "SESSION-GC")
185
181
  });
186
182
  this.gcScheduler.start();
183
+ this.kernel?.once("onTerminate", () => {
184
+ this.gcScheduler?.stop();
185
+ this.storage?.close();
186
+ });
187
187
  });
188
188
  return this.storage;
189
189
  }
@@ -1,5 +1,5 @@
1
- import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorateParam.js";
2
- import __decorate from "../../../_virtual/_@oxc-project_runtime@0.157.0/helpers/esm/decorate.js";
1
+ import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorateParam.js";
2
+ import __decorate from "../../../_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorate.js";
3
3
  import { Cli, FileClass, Inject, Service } from "nodefony";
4
4
  import fs from "node:fs";
5
5
  import fsp from "node:fs/promises";
@@ -3,9 +3,9 @@ import { protocolNameOf } from "./transport.js";
3
3
  import { resolveTimingMode } from "./timingMode.js";
4
4
  import Cookie, { cookiesParser } from "../cookies/cookie.js";
5
5
  import { RequestContext, Service, logColor } from "nodefony";
6
+ import { AsyncResource } from "node:async_hooks";
6
7
  import { randomFillSync, randomUUID } from "node:crypto";
7
8
  import { performance } from "node:perf_hooks";
8
- import { AsyncResource } from "node:async_hooks";
9
9
  // nodefony/src/context/Context.ts
10
10
  const colorLogEvent = () => logColor.cyanBgBlack("EVENT CONTEXT");
11
11
  const PERF_PROBE_SUB = process.env.NF_PERF_PROBE === "1";
@@ -356,6 +356,17 @@ var Context = class extends Service {
356
356
  holdRequestStore(store) {
357
357
  this.requestStore = store;
358
358
  }
359
+ /**
360
+ * L'utilisateur authentifié de l'unité de travail, lu sur son magasin et
361
+ * non sur l'ALS : valable là où la bulle n'est plus active (rendu d'une
362
+ * erreur, démontage). Même règle que `RequestContext.getAuthenticatedUser`.
363
+ *
364
+ * @returns l'utilisateur connecté, ou `null` (anonyme, hors zone, nettoyé).
365
+ */
366
+ authenticatedUser() {
367
+ const store = this.requestStore;
368
+ return store === null ? null : RequestContext.authenticatedUserOf(store);
369
+ }
359
370
  clean() {
360
371
  this.cleaned = true;
361
372
  this.httpKernel = null;
@@ -1,5 +1,5 @@
1
1
  import HttpError from "../../errors/httpError.js";
2
- import "./SseStream.js";
2
+ import { SSE_STALL_TIMEOUT_MS } from "./SseStream.js";
3
3
  import { MAX_TIMER_MS, checkStreamTimer, openStreamLifecycle } from "./StreamLifecycle.js";
4
4
  import http from "node:http";
5
5
  import { AsyncResource } from "node:async_hooks";
@@ -330,8 +330,9 @@ function openByteStream(context, source, options = {}) {
330
330
  settings = settingsOf(context, options);
331
331
  opened = openStreamLifecycle(context, {
332
332
  label: "Flux",
333
- stallTimeout: options.stallTimeout ?? 3e4,
334
- maxDuration: options.maxDuration ?? false
333
+ stallTimeout: options.stallTimeout ?? SSE_STALL_TIMEOUT_MS,
334
+ maxDuration: options.maxDuration ?? false,
335
+ endsAtShutdown: settings.live
335
336
  });
336
337
  } catch (error) {
337
338
  readable.on("error", IGNORE_LATE_ERROR);
@@ -343,7 +344,11 @@ function openByteStream(context, source, options = {}) {
343
344
  stream.start();
344
345
  return stream.done;
345
346
  };
346
- return opened instanceof Promise ? opened.then(start) : start(opened);
347
+ if (opened instanceof Promise) {
348
+ readable.on("error", IGNORE_LATE_ERROR);
349
+ return opened.then(start);
350
+ }
351
+ return start(opened);
347
352
  }
348
353
  /** Les réglages du flux, défauts résolus sur le contexte. */
349
354
  function settingsOf(context, options) {
@@ -11,6 +11,7 @@ import { responseEnded } from "../responseEnded.js";
11
11
  import { alpnProtocolOf } from "../transport.js";
12
12
  import { openSseStream } from "./SseStream.js";
13
13
  import { openByteStream } from "./ByteStream.js";
14
+ import { webResponseParts } from "./webResponse.js";
14
15
  import { finallyMaybe, isPromise, thenMaybe, typeOf } from "nodefony";
15
16
  import net from "node:net";
16
17
  import url, { URL } from "node:url";
@@ -33,7 +34,7 @@ function switchUrlScheme(from, scheme, port) {
33
34
  const socketActiveContext = /* @__PURE__ */ new WeakMap();
34
35
  /**
35
36
  * Route le délai d'inactivité d'une socket HTTP/1.1 vers la requête qu'elle
36
- * porte : 408/504 si une requête est en cours, fermeture si la connexion
37
+ * porte : 503 si une requête est en cours, fermeture si la connexion
37
38
  * persistante est inactive. Écouteur `timeout` du SERVEUR — un seul pour
38
39
  * toutes ses connexions —, que Node appelle avec la socket en argument
39
40
  * (`socketOnTimeout` → `server.emit("timeout", socket)`).
@@ -49,7 +50,8 @@ const socketActiveContext = /* @__PURE__ */ new WeakMap();
49
50
  * serveur supprime aussi une fermeture et une entrée de registre par connexion.
50
51
  *
51
52
  * @param socket - la socket dont le délai a expiré ; une session HTTP/2 (même
52
- * événement sur un serveur `allowHTTP1`) est laissée à son propre délai
53
+ * événement sur un serveur `allowHTTP1`) est fermée par l'écouteur que pose
54
+ * `setHttp2SessionTimeout` (`src/servers/http2Limits.ts`)
53
55
  */
54
56
  function routeSocketTimeout(socket) {
55
57
  if (!(socket instanceof net.Socket)) return;
@@ -72,6 +74,15 @@ var HttpContext = class extends Context {
72
74
  * son montage. `null` = servie par l'application (cas de toute requête).
73
75
  */
74
76
  relayAccessLog = null;
77
+ /**
78
+ * L'appel RPC servi sur cette requête (gRPC, gRPC-Web, Connect), `null`
79
+ * hors appel RPC (ADR-0026 D10) : un champ, aucune allocation. Posé par le
80
+ * point d'entrée gRPC ; à l'échéance du délai de réponse, {@link _onTimeout}
81
+ * le termine au lieu d'écrire un 503.
82
+ *
83
+ * @experimental Surface gRPC, ADR-0026 D11.
84
+ */
85
+ rpc = null;
75
86
  request;
76
87
  response;
77
88
  uploadService;
@@ -142,11 +153,34 @@ var HttpContext = class extends Context {
142
153
  _onTimeout() {
143
154
  if (this.listenerCount("onTimeout")) this.fire("onTimeout", this);
144
155
  this._abortIfPending("Request timeout");
156
+ if (this.rpc !== null) {
157
+ this.rpc.expire();
158
+ return;
159
+ }
145
160
  this.httpKernel?.warnUnsentResponse(this);
146
- let error;
147
- if (this.response.stream) error = new HttpError("Gateway Timeout", 504, this);
148
- else error = new HttpError("Request Timeout", 408, this);
149
- this.httpKernel?.onError(error, this);
161
+ this.httpKernel?.onError(new HttpError("Service Unavailable", 503, this), this);
162
+ }
163
+ /**
164
+ * Ré-arme le délai de réponse de la requête à la valeur propre de sa route
165
+ * (`responseTimeout` d'une option de route), une fois celle-ci résolue.
166
+ *
167
+ * Même mécanisme que {@link setTimeout} — délai d'inactivité de la socket
168
+ * (HTTP/1.1) ou du flux (HTTP/2) —, sans second écouteur : sous HTTP/2,
169
+ * `setTimeout(ms)` sans rappel remplace le minuteur et garde celui posé par
170
+ * {@link setTimeout}. `0` désactive le délai pour la requête.
171
+ *
172
+ * @param ms - délai de la route, en millisecondes.
173
+ */
174
+ rearmTimeout(ms) {
175
+ this.response.setTimeout(ms);
176
+ const res = this.response.response;
177
+ if (!res) return;
178
+ if (this.response.stream) {
179
+ res.setTimeout(ms);
180
+ return;
181
+ }
182
+ const socket = this.activeSocket;
183
+ if (socket && socket.timeout !== ms) socket.setTimeout(ms);
150
184
  }
151
185
  setScheme() {
152
186
  const req = this.request;
@@ -173,6 +207,8 @@ var HttpContext = class extends Context {
173
207
  if (kernel && (kernel.debug || kernel.listenerCount("onRequest") > 0)) kernel.fire("onRequest", this);
174
208
  if (!this.resolver && this.router) this.resolver = this.router.resolve(this);
175
209
  if (this.resolver?.resolve) {
210
+ const routeTimeout = this.resolver.route?.responseTimeout;
211
+ if (routeTimeout !== void 0 && routeTimeout !== null) this.rearmTimeout(routeTimeout);
176
212
  this.setMetaData();
177
213
  return this.resolver.callController();
178
214
  }
@@ -199,17 +235,32 @@ var HttpContext = class extends Context {
199
235
  * réellement rendu (le relais écrit la réponse brute, pas celle du
200
236
  * contexte) ; un montage `accessLog: "errors"` ne l'écrit qu'en échec.
201
237
  *
238
+ * Le statut natif n'est recopié que si la réponse a été TERMINÉE
239
+ * ({@link responseEnded}) : sinon il n'a jamais atteint le client (200 par
240
+ * défaut de Node), et le statut du contexte — 499 d'un client parti — dit
241
+ * vrai. Règle unique pour toute réponse écrite par un tiers (#695, critère
242
+ * 38).
243
+ *
202
244
  * @param httpError - l'erreur de la requête, s'il y en a une
203
245
  */
204
246
  logRequest(httpError) {
247
+ if (this.rpc !== null) this.adoptNativeStatus();
205
248
  if (this.relayAccessLog !== null) {
206
- const raw = this.response.response;
207
- if (raw !== null) this.response.statusCode = raw.statusCode;
249
+ this.adoptNativeStatus();
208
250
  if (this.relayAccessLog === "errors" && this.response.statusCode < 400 && !httpError && !this.error) return;
209
251
  }
210
252
  return super.logRequest(httpError);
211
253
  }
212
254
  /**
255
+ * Reprend le statut de la réponse native, écrite par un tiers (relais,
256
+ * adaptateur RPC) — si la réponse est TERMINÉE seulement : sinon ce statut
257
+ * n'a jamais atteint le client, et celui du contexte (499) dit vrai.
258
+ */
259
+ adoptNativeStatus() {
260
+ const raw = this.response.response;
261
+ if (raw !== null && responseEnded(raw)) this.response.statusCode = raw.statusCode;
262
+ }
263
+ /**
213
264
  * Nettoie le contexte, et le DÉTACHE de sa socket h1 : la connexion
214
265
  * persistante survit à la requête, et l'entrée de `socketActiveContext`
215
266
  * retiendrait sinon tout le contexte jusqu'à sa fermeture. Seule l'entrée qui
@@ -382,6 +433,33 @@ var HttpContext = class extends Context {
382
433
  openStream(source, options) {
383
434
  return openByteStream(this, source, options);
384
435
  }
436
+ /**
437
+ * Répond par une `Response` Web standard — celle que rend `fetch()`, ou une
438
+ * réponse construite (`new Response(…)`, `Response.json(…)`) : son statut,
439
+ * ses en-têtes, et son corps EN FLUX, jamais lu d'avance.
440
+ *
441
+ * C'est ce que fait le framework quand une action renvoie une `Response`.
442
+ * Une réponse relayée (issue de `fetch`) perd `content-encoding`,
443
+ * `content-length` et `set-cookie` de l'amont ; une réponse construite garde
444
+ * les siens. Les en-têtes de connexion ne partent jamais. Le reste — arrêt au
445
+ * départ du client, contre-pression, délais, HEAD sans lire le corps — est
446
+ * celui de {@link HttpContext.openStream}.
447
+ *
448
+ * @param response - une `Response` dont le corps n'a pas été consommé.
449
+ * @param options - délais et écouteur de fin ; statut et en-têtes viennent de la réponse.
450
+ * @returns la raison de la fin et le nombre d'octets ; ne rejette jamais.
451
+ * @throws HttpError 502 pour `Response.error()` (statut 0, aucune réponse) ;
452
+ * les cas de {@link HttpContext.openStream} sinon.
453
+ */
454
+ openWebResponse(response, options) {
455
+ if (response.type === "error") throw new HttpError("Réponse Web en erreur : Response.error() ne porte aucune réponse", 502, this);
456
+ const parts = webResponseParts(response);
457
+ return openByteStream(this, parts.source, {
458
+ ...options,
459
+ status: parts.status,
460
+ headers: parts.headers
461
+ });
462
+ }
385
463
  flush(chunk, encoding) {
386
464
  return this.response.flush(chunk, encoding);
387
465
  }
@@ -23,6 +23,18 @@ const reg = /(.*)[[][\]]$/u;
23
23
  const unlinkQuiet = (p) => fsp.unlink(p).catch(() => void 0);
24
24
  var BodyHandled = class {};
25
25
  const BODY_DONE = new BodyHandled();
26
+ /**
27
+ * Une réception multipart EN COURS, interruptible de l'extérieur : `fail` rompt
28
+ * la lecture comme le ferait un dépassement de limite (temporaires nettoyés).
29
+ * Posée dans `request.parser` le temps de la lecture seulement — l'allocation
30
+ * est réservée au multipart, déjà lourd.
31
+ */
32
+ var MultipartReception = class {
33
+ fail;
34
+ constructor(fail) {
35
+ this.fail = fail;
36
+ }
37
+ };
26
38
  const CHARSET_ALIASES = {
27
39
  "utf-8": "utf8",
28
40
  utf8: "utf8",
@@ -128,6 +140,9 @@ var HttpRequest = class {
128
140
  * temporaire — lus par `@UploadedFile()` / `@UploadedFiles()`. Les champs
129
141
  * texte du même corps sont dans {@link body}.
130
142
  *
143
+ * Ces temporaires sont EFFACÉS à la fin de la requête : pour garder un
144
+ * fichier, l'action le déplace (`moveAsync`) avant de répondre.
145
+ *
131
146
  * Lecture seule : une requête sans upload rend un vide partagé et gelé,
132
147
  * aucune allocation.
133
148
  */
@@ -271,6 +286,25 @@ var HttpRequest = class {
271
286
  }
272
287
  }
273
288
  /**
289
+ * Lit le plafond du corps et les limites d'envoi tels que les voit CETTE
290
+ * requête : la configuration http, surchargée par le calque que la requête a
291
+ * pu poser avant la lecture du corps (`overlayConfig`, #494), puis par les
292
+ * bornes que la route déclare pour elle-même (option de route `upload`). La
293
+ * route est déjà résolue à ce moment : une route sans bornes propres
294
+ * n'alloue rien. Hors pipeline (aucun scope ouvert), la configuration du
295
+ * module telle quelle.
296
+ */
297
+ readBodyLimits() {
298
+ const options = RequestContext.getScope() !== void 0 ? useConfig("@nodefony/http") : this.context.httpKernel?.module.options;
299
+ const base = options?.upload ?? {};
300
+ const route = this.context.resolver?.route?.upload;
301
+ this.uploadOption = route ? {
302
+ ...base,
303
+ ...route
304
+ } : base;
305
+ this.maxBodySize = options?.maxBodySize ?? 0;
306
+ }
307
+ /**
274
308
  * B1 — Pré-contrôle de taille du corps via l'en-tête `Content-Length` :
275
309
  * rejette en 413 (RFC 9110 §15.5.14 « Content Too Large ») AVANT toute lecture
276
310
  * quand le client annonce une taille supérieure à `maxBodySize`. Rideau de
@@ -279,17 +313,6 @@ var HttpRequest = class {
279
313
  *
280
314
  * @throws {HttpError} 413 si `Content-Length` dépasse `maxBodySize`.
281
315
  */
282
- /**
283
- * Lit le plafond du corps et les limites d'envoi tels que les voit CETTE
284
- * requête : la configuration http, surchargée par le calque que la requête a
285
- * pu poser avant la lecture du corps (`overlayConfig`, #494). Hors pipeline
286
- * (aucun scope ouvert), la configuration du module telle quelle.
287
- */
288
- readBodyLimits() {
289
- const options = RequestContext.getScope() !== void 0 ? useConfig("@nodefony/http") : this.context.httpKernel?.module.options;
290
- this.uploadOption = options?.upload ?? {};
291
- this.maxBodySize = options?.maxBodySize ?? 0;
292
- }
293
316
  enforceBodyLimit() {
294
317
  const max = this.maxBodySize;
295
318
  if (max <= 0) return;
@@ -366,6 +389,7 @@ var HttpRequest = class {
366
389
  return this.parser;
367
390
  }
368
391
  const { fields, files } = await this.streamMultipart(bb);
392
+ this.parser = null;
369
393
  this.body = fields;
370
394
  for (const pf of files) await this.createFileUpload(pf.field, pf.file);
371
395
  this.context.requestEnded = true;
@@ -385,18 +409,33 @@ var HttpRequest = class {
385
409
  return new Promise((resolve, reject) => {
386
410
  const fields = {};
387
411
  const files = [];
412
+ let order = 0;
388
413
  const pending = [];
389
414
  const tempPaths = [];
390
415
  const openStreams = /* @__PURE__ */ new Set();
391
416
  let totalBytes = 0;
392
417
  let aborted = false;
418
+ let received = false;
393
419
  const abort = (err) => {
394
420
  if (aborted) return;
395
421
  aborted = true;
396
422
  this.request.unpipe(bb);
397
423
  this.request.resume();
398
- for (const ws of openStreams) ws.destroy();
399
- Promise.all(tempPaths.map(unlinkQuiet)).finally(() => reject(err));
424
+ const purge = () => {
425
+ Promise.all(tempPaths.map(unlinkQuiet)).finally(() => reject(err));
426
+ };
427
+ let closing = openStreams.size;
428
+ if (closing === 0) {
429
+ purge();
430
+ return;
431
+ }
432
+ for (const ws of openStreams) {
433
+ ws.once("close", () => {
434
+ closing -= 1;
435
+ if (closing === 0) purge();
436
+ });
437
+ ws.destroy();
438
+ }
400
439
  };
401
440
  bb.on("field", (name, value) => {
402
441
  if (aborted) return;
@@ -407,6 +446,8 @@ var HttpRequest = class {
407
446
  stream.resume();
408
447
  return;
409
448
  }
449
+ const slot = order;
450
+ order += 1;
410
451
  const field = reg.exec(fieldname)?.[1] ?? fieldname;
411
452
  const ext = filename ? path.extname(filename) : "";
412
453
  const newFilename = `${randomUUID()}${ext}`;
@@ -431,7 +472,7 @@ var HttpRequest = class {
431
472
  ws.on("close", () => {
432
473
  openStreams.delete(ws);
433
474
  if (aborted) return res();
434
- files.push({
475
+ files[slot] = {
435
476
  field,
436
477
  file: {
437
478
  filepath,
@@ -443,7 +484,7 @@ var HttpRequest = class {
443
484
  hashAlgorithm: algo,
444
485
  hash: hasher ? hasher.digest("hex") : null
445
486
  }
446
- });
487
+ };
447
488
  res();
448
489
  });
449
490
  });
@@ -455,6 +496,7 @@ var HttpRequest = class {
455
496
  bb.on("partsLimit", () => abort(new HttpError("too many parts", 413, this.context)));
456
497
  bb.on("error", (e) => abort(e instanceof Error ? e : new Error(String(e))));
457
498
  bb.on("finish", () => {
499
+ received = true;
458
500
  if (aborted) return;
459
501
  Promise.all(pending).then(() => {
460
502
  if (!aborted) resolve({
@@ -463,10 +505,32 @@ var HttpRequest = class {
463
505
  });
464
506
  }).catch(abort);
465
507
  });
508
+ this.parser = new MultipartReception((error) => {
509
+ if (aborted || received) return false;
510
+ abort(error);
511
+ return true;
512
+ });
466
513
  this.request.pipe(bb);
467
514
  });
468
515
  }
469
516
  /**
517
+ * Interrompt la réception du corps en cours : la promesse de `initialize()`
518
+ * rejette avec `error`, rendue par le chemin d'erreur habituel — comme un
519
+ * corps qui dépasse sa limite (413).
520
+ *
521
+ * Sert le délai de réception HTTP/2 (`requestTimeout`), que `node:http2` ne
522
+ * borne pas. Sans effet hors réception : corps déjà reçu, lu en flux par
523
+ * l'action (`bodyStream`), ou méthode sans corps.
524
+ *
525
+ * @param error - la cause rendue au client (408 au délai dépassé)
526
+ * @returns vrai si une réception en cours a été interrompue
527
+ */
528
+ abortBody(error) {
529
+ const parser = this.parser;
530
+ if (parser instanceof Parser || parser instanceof MultipartReception) return parser.fail(error);
531
+ return false;
532
+ }
533
+ /**
470
534
  * Accumule un champ texte multipart. `foo[]` (ou répétition de `foo`) →
471
535
  * tableau ; sinon valeur scalaire. (Anciennement délégué à formidable.)
472
536
  */
@@ -499,9 +563,25 @@ var HttpRequest = class {
499
563
  }
500
564
  async createFileUpload(name, file) {
501
565
  const fileUpload = await this.context.uploadService?.createUploadFile(file, name);
502
- if (fileUpload) (this.#uploads ??= []).push(fileUpload);
566
+ if (fileUpload) {
567
+ if (this.#uploads === null) {
568
+ this.#uploads = [];
569
+ this.context.onAfterResponse(() => this.#purgeUploads());
570
+ }
571
+ this.#uploads.push(fileUpload);
572
+ }
503
573
  return fileUpload;
504
574
  }
575
+ /**
576
+ * Efface, une fois la requête terminée (réponse envoyée ou client parti),
577
+ * les temporaires que l'action n'a pas déplacés — la règle de PHP. Un
578
+ * fichier déplacé (`move`/`moveAsync`) n'est plus à son chemin temporaire :
579
+ * l'effacement n'y trouve rien, et l'erreur est ignorée.
580
+ */
581
+ #purgeUploads() {
582
+ const uploads = this.#uploads ?? NO_FILES;
583
+ return Promise.all(uploads.map((upload) => unlinkQuiet(upload.parsedFile.filepath))).then(() => void 0);
584
+ }
505
585
  getMethod() {
506
586
  return this.request.method;
507
587
  }