@nodefony/http 10.0.0-beta.5 → 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 (130) hide show
  1. package/README.md +24 -0
  2. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorateParam.js +1 -1
  4. package/dist/index.js +30 -26
  5. package/dist/nodefony/command/proxyGenerateCommand.js +2 -2
  6. package/dist/nodefony/config/config.js +29 -4
  7. package/dist/nodefony/grpc/GrpcCall.js +298 -0
  8. package/dist/nodefony/grpc/GrpcEndpoint.js +528 -0
  9. package/dist/nodefony/grpc/index.js +4 -0
  10. package/dist/nodefony/interfaces/IRpcCall.js +1 -0
  11. package/dist/nodefony/service/ProfilerAdminApi.js +5 -2
  12. package/dist/nodefony/service/audit-logger.js +52 -6
  13. package/dist/nodefony/service/error-renderer.js +47 -13
  14. package/dist/nodefony/service/http-kernel.js +306 -49
  15. package/dist/nodefony/service/pretty-request-logger.js +5 -3
  16. package/dist/nodefony/service/request-logger.js +21 -6
  17. package/dist/nodefony/service/reverse-proxy.js +8 -3
  18. package/dist/nodefony/service/servers/clientError.js +53 -4
  19. package/dist/nodefony/service/servers/server-h2c.js +6 -6
  20. package/dist/nodefony/service/servers/server-http.js +6 -4
  21. package/dist/nodefony/service/servers/server-https.js +14 -7
  22. package/dist/nodefony/service/servers/server-static.js +1 -6
  23. package/dist/nodefony/service/servers/server-websocket-secure.js +7 -11
  24. package/dist/nodefony/service/servers/server-websocket.js +7 -11
  25. package/dist/nodefony/service/servers/serverShutdown.js +43 -6
  26. package/dist/nodefony/service/sessions/sessions-service.js +7 -7
  27. package/dist/nodefony/service/upload/upload-service.js +2 -2
  28. package/dist/nodefony/src/assets/prebuiltUi.js +23 -5
  29. package/dist/nodefony/src/context/Context.js +12 -1
  30. package/dist/nodefony/src/context/http/ByteStream.js +364 -0
  31. package/dist/nodefony/src/context/http/HttpContext.js +139 -32
  32. package/dist/nodefony/src/context/http/Request.js +111 -22
  33. package/dist/nodefony/src/context/http/Response.js +459 -37
  34. package/dist/nodefony/src/context/http/SseStream.js +83 -148
  35. package/dist/nodefony/src/context/http/StreamLifecycle.js +582 -0
  36. package/dist/nodefony/src/context/http/parser.js +29 -11
  37. package/dist/nodefony/src/context/http/urlFastPath.js +80 -1
  38. package/dist/nodefony/src/context/http/webResponse.js +56 -0
  39. package/dist/nodefony/src/context/http2/Request.js +1 -1
  40. package/dist/nodefony/src/context/http2/Response.js +55 -12
  41. package/dist/nodefony/src/context/requestTrace.js +6 -2
  42. package/dist/nodefony/src/context/rpcOutcome.js +20 -0
  43. package/dist/nodefony/src/context/transport.js +20 -1
  44. package/dist/nodefony/src/context/websocket/Response.js +15 -3
  45. package/dist/nodefony/src/context/websocket/WebsocketContext.js +86 -18
  46. package/dist/nodefony/src/context/websocket/websocketUrl.js +7 -0
  47. package/dist/nodefony/src/context/websocket/wsBackpressure.js +15 -15
  48. package/dist/nodefony/src/context/websocket/wsFrameRate.js +71 -0
  49. package/dist/nodefony/src/context/websocket/wsLogContent.js +55 -17
  50. package/dist/nodefony/src/cookies/cookie.js +57 -14
  51. package/dist/nodefony/src/errors/httpError.js +58 -25
  52. package/dist/nodefony/src/grpc/cors.js +59 -0
  53. package/dist/nodefony/src/grpc/limits.js +16 -0
  54. package/dist/nodefony/src/grpc/refusal.js +271 -0
  55. package/dist/nodefony/src/profiler/Profiler.js +14 -1
  56. package/dist/nodefony/src/profiler/aggregate.js +2 -2
  57. package/dist/nodefony/src/proxy/forward.js +109 -9
  58. package/dist/nodefony/src/proxy/generateProxyConfig.js +97 -17
  59. package/dist/nodefony/src/proxy/rules.js +29 -14
  60. package/dist/nodefony/src/servers/http2Goaway.js +76 -0
  61. package/dist/nodefony/src/servers/http2Limits.js +41 -15
  62. package/dist/nodefony/src/servers/http2StreamGrace.js +100 -0
  63. package/dist/nodefony/src/servers/receiveDeadline.js +177 -0
  64. package/dist/nodefony/src/servers/upgradeDispatch.js +62 -4
  65. package/dist/types/index.d.ts +11 -2
  66. package/dist/types/nodefony/config/config.d.ts +14 -0
  67. package/dist/types/nodefony/grpc/GrpcCall.d.ts +142 -0
  68. package/dist/types/nodefony/grpc/GrpcEndpoint.d.ts +98 -0
  69. package/dist/types/nodefony/grpc/index.d.ts +15 -0
  70. package/dist/types/nodefony/interfaces/IContext.d.ts +9 -0
  71. package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +14 -0
  72. package/dist/types/nodefony/interfaces/IRequest.d.ts +2 -1
  73. package/dist/types/nodefony/interfaces/IRequestTrace.d.ts +10 -0
  74. package/dist/types/nodefony/interfaces/IRouting.d.ts +19 -0
  75. package/dist/types/nodefony/interfaces/IRpcCall.d.ts +83 -0
  76. package/dist/types/nodefony/interfaces/ISecurity.d.ts +27 -2
  77. package/dist/types/nodefony/interfaces/IUpload.d.ts +51 -4
  78. package/dist/types/nodefony/interfaces/index.d.ts +2 -1
  79. package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +2 -1
  80. package/dist/types/nodefony/service/audit-logger.d.ts +33 -3
  81. package/dist/types/nodefony/service/error-renderer.d.ts +27 -5
  82. package/dist/types/nodefony/service/http-kernel.d.ts +36 -3
  83. package/dist/types/nodefony/service/request-logger.d.ts +10 -0
  84. package/dist/types/nodefony/service/servers/clientError.d.ts +3 -2
  85. package/dist/types/nodefony/service/servers/serverShutdown.d.ts +7 -5
  86. package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +9 -4
  87. package/dist/types/nodefony/src/context/Context.d.ts +9 -1
  88. package/dist/types/nodefony/src/context/http/ByteStream.d.ts +109 -0
  89. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +99 -3
  90. package/dist/types/nodefony/src/context/http/Request.d.ts +46 -9
  91. package/dist/types/nodefony/src/context/http/Response.d.ts +188 -8
  92. package/dist/types/nodefony/src/context/http/SseStream.d.ts +33 -8
  93. package/dist/types/nodefony/src/context/http/StreamLifecycle.d.ts +106 -0
  94. package/dist/types/nodefony/src/context/http/parser.d.ts +17 -3
  95. package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +40 -0
  96. package/dist/types/nodefony/src/context/http/webResponse.d.ts +31 -0
  97. package/dist/types/nodefony/src/context/http2/Response.d.ts +40 -5
  98. package/dist/types/nodefony/src/context/requestTrace.d.ts +3 -1
  99. package/dist/types/nodefony/src/context/rpcOutcome.d.ts +11 -0
  100. package/dist/types/nodefony/src/context/transport.d.ts +17 -0
  101. package/dist/types/nodefony/src/context/websocket/Response.d.ts +6 -1
  102. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +48 -0
  103. package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +4 -0
  104. package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +12 -2
  105. package/dist/types/nodefony/src/context/websocket/wsFrameRate.d.ts +69 -0
  106. package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +16 -8
  107. package/dist/types/nodefony/src/cookies/cookie.d.ts +20 -2
  108. package/dist/types/nodefony/src/errors/httpError.d.ts +29 -4
  109. package/dist/types/nodefony/src/grpc/cors.d.ts +32 -0
  110. package/dist/types/nodefony/src/grpc/limits.d.ts +14 -0
  111. package/dist/types/nodefony/src/grpc/refusal.d.ts +136 -0
  112. package/dist/types/nodefony/src/profiler/Profiler.d.ts +11 -1
  113. package/dist/types/nodefony/src/proxy/forward.d.ts +37 -2
  114. package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +15 -2
  115. package/dist/types/nodefony/src/proxy/rules.d.ts +20 -0
  116. package/dist/types/nodefony/src/servers/http2Goaway.d.ts +41 -0
  117. package/dist/types/nodefony/src/servers/http2Limits.d.ts +27 -14
  118. package/dist/types/nodefony/src/servers/http2StreamGrace.d.ts +34 -0
  119. package/dist/types/nodefony/src/servers/receiveDeadline.d.ts +77 -0
  120. package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +41 -1
  121. package/docs/byte-stream.md +406 -0
  122. package/docs/cookies.md +13 -3
  123. package/docs/diagnostics-channel.md +45 -1
  124. package/docs/index.md +8 -0
  125. package/docs/reverse-proxy.md +15 -3
  126. package/docs/servers.md +67 -43
  127. package/docs/session.md +3 -3
  128. package/docs/sse.md +27 -20
  129. package/docs/upload.md +43 -17
  130. package/package.json +38 -18
@@ -0,0 +1,364 @@
1
+ import HttpError from "../../errors/httpError.js";
2
+ import { SSE_STALL_TIMEOUT_MS } from "./SseStream.js";
3
+ import { MAX_TIMER_MS, checkStreamTimer, openStreamLifecycle } from "./StreamLifecycle.js";
4
+ import http from "node:http";
5
+ import { AsyncResource } from "node:async_hooks";
6
+ import { Readable } from "node:stream";
7
+ // nodefony/src/context/http/ByteStream.ts
8
+ /** Erreur d'une source après la fin de son flux : plus personne à prévenir. */
9
+ const IGNORE_LATE_ERROR = () => void 0;
10
+ /**
11
+ * Un flux d'octets en cours sur une réponse — interne : on l'obtient par
12
+ * {@link openByteStream}, qui ne rend que sa promesse de fin.
13
+ */
14
+ var ByteStream = class {
15
+ #lifecycle;
16
+ #source;
17
+ #settings;
18
+ done;
19
+ #resolve;
20
+ #bytes = 0;
21
+ /** Les en-têtes sont-ils partis ? Posé au premier morceau, ou à la fin. */
22
+ #headed = false;
23
+ /** Raison posée par le flux lui-même ; `null` : le cycle de vie tranchera. */
24
+ #reason = null;
25
+ /** La résolution attend la fin de la requête (fin propre, erreur rendue). */
26
+ #settleLater = false;
27
+ #waitingDrain = false;
28
+ /** Octets vus au dernier tour de garde où la source avait avancé. */
29
+ #seenBytes = 0;
30
+ /** Heure de ce tour (`Date.now()`) — une lecture par tour, jamais par morceau. */
31
+ #seenAt = 0;
32
+ #watchdog = null;
33
+ /** `Content-Length` annoncé, lu une fois aux en-têtes ; `-1` : aucun. */
34
+ #declared = -1;
35
+ /** Requête HEAD : aucun corps, la longueur annoncée est celle du GET. */
36
+ #head = false;
37
+ #onData = (chunk) => this.#write(chunk);
38
+ #onEnd = () => this.#finishClean();
39
+ #onSourceError = (error) => this.#fail(error, "error");
40
+ /** Une source détruite sans erreur ni fin : on la tient pour finie. */
41
+ #onSourceClose = () => {
42
+ if (this.#source.errored === null) this.#finishClean();
43
+ };
44
+ /**
45
+ * Écouteur `drain` PERMANENT, posé une fois (comme `pipe`) : un `once` par
46
+ * épisode de contre-pression allouait un enrobage par morceau sur une socket
47
+ * rapide, où presque chaque écriture de 64 Kio rend `false`.
48
+ */
49
+ #onDrain = () => {
50
+ if (!this.#waitingDrain) return;
51
+ this.#waitingDrain = false;
52
+ this.#lifecycle.clearStall();
53
+ this.#source.resume();
54
+ };
55
+ #onIdle = () => this.#checkIdle();
56
+ constructor(lifecycle, source, settings) {
57
+ this.#lifecycle = lifecycle;
58
+ this.#source = source;
59
+ this.#settings = settings;
60
+ this.done = new Promise((resolve) => {
61
+ this.#resolve = resolve;
62
+ });
63
+ lifecycle.onRelease(() => this.#release());
64
+ }
65
+ /** Démarre le flux : HEAD, source déjà finie, ou lecture de la source. */
66
+ start() {
67
+ const lifecycle = this.#lifecycle;
68
+ if (lifecycle.closed) return;
69
+ lifecycle.disarmIdleTimeout();
70
+ const source = this.#source;
71
+ source.on("error", this.#onSourceError);
72
+ if (lifecycle.context.getMethod() === "HEAD") {
73
+ this.#head = true;
74
+ this.#finishClean();
75
+ return;
76
+ }
77
+ if (source.errored !== null) {
78
+ this.#fail(source.errored, "error");
79
+ return;
80
+ }
81
+ if (source.destroyed || source.readableEnded) {
82
+ this.#finishClean();
83
+ return;
84
+ }
85
+ lifecycle.target.on("drain", this.#onDrain);
86
+ const idle = this.#settings.idleTimeout;
87
+ if (idle > 0) {
88
+ this.#seenAt = Date.now();
89
+ this.#watchdog = setInterval(this.#onIdle, Math.max(1, Math.floor(idle / 4)));
90
+ this.#watchdog.unref();
91
+ }
92
+ source.once("end", this.#onEnd);
93
+ source.once("close", this.#onSourceClose);
94
+ source.on("data", this.#onData);
95
+ }
96
+ /**
97
+ * En-têtes au premier morceau (ou à la fin d'une source vide).
98
+ *
99
+ * @param first - octets du premier morceau (0 : source vide).
100
+ * @throws Error quand ce morceau contredit déjà `Content-Length` — levé
101
+ * AVANT `writeHead` : rien n'est parti, la page d'erreur le peut.
102
+ */
103
+ #writeHead(first) {
104
+ const context = this.#lifecycle.context;
105
+ const { headers, live, status } = this.#settings;
106
+ const response = context.response;
107
+ if (headers !== null) response.setHeaders(headers);
108
+ response.setDefaultHeader("Content-Type", "application/octet-stream");
109
+ if (live) {
110
+ response.setDefaultHeader("Cache-Control", "no-cache, no-transform");
111
+ response.setDefaultHeader("X-Accel-Buffering", "no");
112
+ }
113
+ const length = response.getHeader("content-length");
114
+ if (length !== void 0) {
115
+ const declared = Number(length);
116
+ if (Number.isSafeInteger(declared) && declared >= 0) this.#declared = declared;
117
+ }
118
+ if (this.#declared !== -1 && !this.#head && (first === 0 ? this.#declared !== 0 : first > this.#declared)) throw this.#lengthMismatch(first > this.#declared ? "plus" : "moins");
119
+ context.writeHead(status);
120
+ }
121
+ /**
122
+ * Les en-têtes, sans jamais lever : appelé depuis un écouteur `data` ou
123
+ * `end`, une exception (valeur d'en-tête refusée) n'aurait personne pour la
124
+ * rattraper et abattrait le processus. Un échec passe par le chemin d'erreur
125
+ * d'avant le premier octet — rien n'est parti, la page d'erreur peut l'être.
126
+ *
127
+ * @returns `false` quand les en-têtes n'ont pas pu partir.
128
+ */
129
+ #tryWriteHead(first) {
130
+ try {
131
+ this.#writeHead(first);
132
+ this.#headed = true;
133
+ return true;
134
+ } catch (e) {
135
+ this.#fail(e instanceof Error ? e : new Error(String(e)), "error");
136
+ return false;
137
+ }
138
+ }
139
+ #write(chunk) {
140
+ if (this.#reason !== null || this.#lifecycle.closed) return;
141
+ const size = typeof chunk === "string" ? Buffer.byteLength(chunk) : chunk.byteLength;
142
+ if (!this.#headed && !this.#tryWriteHead(size)) return;
143
+ if (this.#declared !== -1 && this.#bytes + size > this.#declared) {
144
+ this.#fail(this.#lengthMismatch("plus"), "error");
145
+ return;
146
+ }
147
+ this.#bytes += size;
148
+ if (this.#lifecycle.target.write(chunk)) return;
149
+ this.#source.pause();
150
+ this.#waitingDrain = true;
151
+ this.#lifecycle.armStall();
152
+ }
153
+ /** Fin propre : en-têtes s'ils manquent, puis fin de la requête. */
154
+ #finishClean() {
155
+ const lifecycle = this.#lifecycle;
156
+ if (this.#reason !== null || lifecycle.closed) return;
157
+ if (!this.#headed && !this.#tryWriteHead(0)) return;
158
+ if (!this.#head && this.#declared !== -1 && this.#bytes !== this.#declared) {
159
+ this.#fail(this.#lengthMismatch("moins"), "error");
160
+ return;
161
+ }
162
+ this.#reason = "end";
163
+ this.#settleLater = true;
164
+ const closed = lifecycle.close();
165
+ if (closed === void 0) this.#settle();
166
+ else closed.then(this.#settleBound, this.#settleBound);
167
+ }
168
+ #settleBound = () => this.#settle();
169
+ /** L'erreur d'un corps qui ne fait pas la longueur annoncée. */
170
+ #lengthMismatch(direction) {
171
+ return /* @__PURE__ */ new Error(`Flux : la source rend ${direction} d'octets que Content-Length n'en annonce (${this.#declared})`);
172
+ }
173
+ /**
174
+ * Échec de la source, ou source muette. Avant le premier octet, la réponse
175
+ * est ROUVERTE et l'erreur confiée au moteur d'erreurs du noyau — même
176
+ * statut, même page qu'une action ordinaire. Après, le client voit une
177
+ * coupure, jamais une fin propre.
178
+ */
179
+ #fail(failure, reason) {
180
+ const lifecycle = this.#lifecycle;
181
+ if (this.#reason !== null || lifecycle.closed) return;
182
+ this.#reason = reason;
183
+ const context = lifecycle.context;
184
+ if (!this.#headed && !context.response.isHeaderSent()) {
185
+ this.#settleLater = true;
186
+ lifecycle.finish();
187
+ const kernel = context.httpKernel;
188
+ if (kernel === null) {
189
+ this.#settle();
190
+ return;
191
+ }
192
+ context.sended = false;
193
+ this.#renderError(kernel, failure);
194
+ return;
195
+ }
196
+ if (reason === "error") context.log(failure, "ERROR", "STREAM");
197
+ context.response.statusCode = "code" in failure && typeof failure.code === "number" && failure.code >= 400 && failure.code <= 599 ? failure.code : 500;
198
+ const target = lifecycle.target;
199
+ if (target instanceof http.ServerResponse) target.destroy();
200
+ else target.destroy(failure);
201
+ lifecycle.finish();
202
+ }
203
+ /**
204
+ * Rend l'erreur d'avant le premier octet par le moteur d'erreurs du noyau,
205
+ * puis règle la promesse — qui ne rejette jamais, même si ce rendu échoue.
206
+ */
207
+ async #renderError(kernel, failure) {
208
+ const context = this.#lifecycle.context;
209
+ try {
210
+ await kernel.onError(failure, context);
211
+ } catch (e) {
212
+ context.log(e, "ERROR", "STREAM");
213
+ }
214
+ this.#settle();
215
+ }
216
+ /**
217
+ * Un tour du chien de garde, tous les quarts de `idleTimeout`. La source a
218
+ * avancé depuis le tour précédent : on note l'heure. Sinon, et depuis au
219
+ * moins `idleTimeout` : la source est muette.
220
+ */
221
+ #checkIdle() {
222
+ if (this.#reason !== null || this.#lifecycle.closed) return;
223
+ const now = Date.now();
224
+ if (this.#bytes !== this.#seenBytes || this.#waitingDrain) {
225
+ this.#seenBytes = this.#bytes;
226
+ this.#seenAt = now;
227
+ return;
228
+ }
229
+ const idle = this.#settings.idleTimeout;
230
+ if (now - this.#seenAt < idle) return;
231
+ this.#fail(new HttpError(`Flux : source muette depuis ${idle} ms`, 504), "timeout");
232
+ }
233
+ /** Nettoyage, appelé UNE fois par le cycle de vie, quelle qu'en soit la cause. */
234
+ #release() {
235
+ const lifecycle = this.#lifecycle;
236
+ const reason = this.#reason ?? (lifecycle.cause === "stall" ? "stall" : lifecycle.cause === "server" ? "end" : "client");
237
+ this.#reason = reason;
238
+ const source = this.#source;
239
+ source.removeListener("data", this.#onData);
240
+ source.removeListener("end", this.#onEnd);
241
+ source.removeListener("close", this.#onSourceClose);
242
+ source.on("error", IGNORE_LATE_ERROR);
243
+ source.removeListener("error", this.#onSourceError);
244
+ source.destroy();
245
+ lifecycle.target.removeListener("drain", this.#onDrain);
246
+ if (this.#watchdog !== null) {
247
+ clearInterval(this.#watchdog);
248
+ this.#watchdog = null;
249
+ }
250
+ const context = lifecycle.context;
251
+ if (reason !== "end") context._abortIfPending(`Flux terminé : ${reason}`);
252
+ context.log(`Flux d'octets terminé (${reason}, ${this.#bytes} octets)`, "DEBUG", "STREAM");
253
+ const onClose = this.#settings.onClose;
254
+ if (onClose !== null) try {
255
+ onClose(reason);
256
+ } catch (e) {
257
+ context.log(e, "ERROR", "STREAM");
258
+ }
259
+ if (!this.#settleLater) this.#settle();
260
+ }
261
+ #settle() {
262
+ this.#resolve({
263
+ reason: this.#reason ?? "end",
264
+ bytes: this.#bytes
265
+ });
266
+ }
267
+ };
268
+ /**
269
+ * Un flux web (`fetch().body`) lu comme un flux Node.
270
+ *
271
+ * Pas `Readable.fromWeb` : il attend le `ReadableStream` de `node:stream/web`,
272
+ * que le type global d'une application compilée avec la lib DOM ne satisfait
273
+ * pas. La lecture tient en un lecteur, et la destruction ANNULE le flux web —
274
+ * c'est elle qui ferme la connexion amont d'un `fetch`.
275
+ */
276
+ function fromWebStream(stream) {
277
+ const reader = stream.getReader();
278
+ const onRead = (result) => {
279
+ if (result.done) readable.push(null);
280
+ else readable.push(result.value);
281
+ };
282
+ const onFail = (error) => {
283
+ readable.destroy(error instanceof Error ? error : new Error(String(error)));
284
+ };
285
+ const readable = new Readable({
286
+ read() {
287
+ reader.read().then(onRead, onFail);
288
+ },
289
+ destroy(failure, callback) {
290
+ reader.cancel(failure ?? void 0).catch(IGNORE_LATE_ERROR);
291
+ callback(failure);
292
+ }
293
+ });
294
+ return readable;
295
+ }
296
+ /** Refuse une option de flux d'octets hors bornes. */
297
+ function checkOptions(options) {
298
+ const { status, idleTimeout } = options;
299
+ if (status !== void 0 && (!Number.isInteger(status) || status < 200 || status > 599)) throw new RangeError("Flux : status entier entre 200 et 599 attendu");
300
+ if (idleTimeout !== void 0 && (!Number.isInteger(idleTimeout) || idleTimeout < 0 || idleTimeout > 2147483647)) throw new RangeError(`Flux : idleTimeout entier entre 0 et ${MAX_TIMER_MS} ms attendu`);
301
+ checkStreamTimer("Flux", "stallTimeout", options.stallTimeout);
302
+ checkStreamTimer("Flux", "maxDuration", options.maxDuration);
303
+ }
304
+ /**
305
+ * Répond par un flux d'octets sur la réponse d'un contexte HTTP.
306
+ *
307
+ * Prend la réponse à l'appel (le noyau ne peut plus répondre par-dessus) ;
308
+ * les en-têtes partent au premier morceau de la source. La promesse se règle à
309
+ * la fin du flux, quelle qu'en soit la raison, et ne rejette JAMAIS.
310
+ *
311
+ * @param context - le contexte de la requête, pas encore répondu.
312
+ * @param source - flux Node, ou flux web (`fetch().body`) ; jamais en mode objet.
313
+ * @param options - statut, en-têtes, délais, écouteur de fin.
314
+ * @returns la raison de la fin et le nombre d'octets envoyés.
315
+ * @throws TypeError quand la source est en mode objet.
316
+ * @throws RangeError quand une option est hors bornes.
317
+ * @throws HttpError 500 quand la réponse est déjà partie ; 429 quand l'IP a
318
+ * déjà tous ses flux et sockets (`wsMaxConnectionsPerIp`).
319
+ *
320
+ * La source appartient au flux dès l'appel : quand l'appel lève, elle est
321
+ * DÉTRUITE (un flux web est annulé) avant que l'erreur ne remonte.
322
+ */
323
+ function openByteStream(context, source, options = {}) {
324
+ const readable = source instanceof Readable ? source : fromWebStream(source);
325
+ let opened;
326
+ let settings;
327
+ try {
328
+ checkOptions(options);
329
+ if (readable.readableObjectMode) throw new TypeError("Flux : une source en mode objet ne porte pas d'octets — pour Readable.from(), passer { objectMode: false }.");
330
+ settings = settingsOf(context, options);
331
+ opened = openStreamLifecycle(context, {
332
+ label: "Flux",
333
+ stallTimeout: options.stallTimeout ?? SSE_STALL_TIMEOUT_MS,
334
+ maxDuration: options.maxDuration ?? false,
335
+ endsAtShutdown: settings.live
336
+ });
337
+ } catch (error) {
338
+ readable.on("error", IGNORE_LATE_ERROR);
339
+ readable.destroy();
340
+ throw error;
341
+ }
342
+ const start = (lifecycle) => {
343
+ const stream = new ByteStream(lifecycle, readable, settings);
344
+ stream.start();
345
+ return stream.done;
346
+ };
347
+ if (opened instanceof Promise) {
348
+ readable.on("error", IGNORE_LATE_ERROR);
349
+ return opened.then(start);
350
+ }
351
+ return start(opened);
352
+ }
353
+ /** Les réglages du flux, défauts résolus sur le contexte. */
354
+ function settingsOf(context, options) {
355
+ const timeout = context.response.timeout;
356
+ return {
357
+ status: options.status ?? 200,
358
+ headers: options.headers ?? null,
359
+ live: options.live ?? true,
360
+ idleTimeout: options.idleTimeout ?? (typeof timeout === "number" ? timeout : 3e4),
361
+ onClose: options.onClose === void 0 ? null : AsyncResource.bind(options.onClose)
362
+ };
363
+ }
364
+ export { openByteStream };
@@ -1,16 +1,19 @@
1
1
  import HttpError from "../../errors/httpError.js";
2
- import { responseEnded } from "../responseEnded.js";
3
- import { alpnProtocolOf } from "../transport.js";
4
- import Cookie from "../../cookies/cookie.js";
5
- import Context from "../Context.js";
6
2
  import { sanitizeRequestId } from "../requestId.js";
3
+ import Context from "../Context.js";
7
4
  import HttpRequest from "./Request.js";
8
5
  import HttpResponse from "./Response.js";
9
6
  import Http2Request from "../http2/Request.js";
10
7
  import Http2Response from "../http2/Response.js";
8
+ import Cookie from "../../cookies/cookie.js";
11
9
  import { describeSessionStoreFailure } from "../../session/sessionStoreFailure.js";
10
+ import { responseEnded } from "../responseEnded.js";
11
+ import { alpnProtocolOf } from "../transport.js";
12
12
  import { openSseStream } from "./SseStream.js";
13
+ import { openByteStream } from "./ByteStream.js";
14
+ import { webResponseParts } from "./webResponse.js";
13
15
  import { finallyMaybe, isPromise, thenMaybe, typeOf } from "nodefony";
16
+ import net from "node:net";
14
17
  import url, { URL } from "node:url";
15
18
  // nodefony/src/context/http/HttpContext.ts
16
19
  /**
@@ -29,25 +32,35 @@ function switchUrlScheme(from, scheme, port) {
29
32
  return target.href;
30
33
  }
31
34
  const socketActiveContext = /* @__PURE__ */ new WeakMap();
32
- const socketTimeoutArmed = /* @__PURE__ */ new WeakSet();
33
35
  /**
34
- * Gestionnaire `timeout` PERMANENT d'une socket h1, fabriqué hors de toute
35
- * méthode : une fermeture créée dans `HttpContext.setTimeout()` partageait
36
- * l'environnement d'une autre qui lit `this`, et retenait ainsi le contexte de
37
- * la PREMIÈRE requête de la connexion tant que celle-ci vivait (#561).
36
+ * Route le délai d'inactivité d'une socket HTTP/1.1 vers la requête qu'elle
37
+ * porte : 503 si une requête est en cours, fermeture si la connexion
38
+ * persistante est inactive. Écouteur `timeout` du SERVEUR — un seul pour
39
+ * toutes ses connexions —, que Node appelle avec la socket en argument
40
+ * (`socketOnTimeout` → `server.emit("timeout", socket)`).
41
+ *
42
+ * Pourquoi au serveur et pas sur la socket : Node y pose déjà son propre
43
+ * écouteur `timeout`, et un second faisait créer à `EventEmitter` un tableau
44
+ * d'écouteurs par CONNEXION. Ce site d'allocation, partagé avec tout le
45
+ * processus et toujours survivant (une connexion vit longtemps), finissait
46
+ * par être basculé dans le vieux tas par V8 (pretenuring) — dans un démarrage
47
+ * sur trois environ, mesuré sous charge. Le graphe de chaque requête y
48
+ * restait alors accroché jusqu'à la collecte complète suivante : +45 % de CPU
49
+ * par requête, 14 à 19 collectes complètes au lieu de 3. Un écouteur par
50
+ * serveur supprime aussi une fermeture et une entrée de registre par connexion.
38
51
  *
39
- * @param socket - la socket h1 dont on route le délai d'inactivité
40
- * @returns l'écouteur à attacher une fois par socket
52
+ * @param socket - la socket dont le délai a expiré ; une session HTTP/2 (même
53
+ * événement sur un serveur `allowHTTP1`) est fermée par l'écouteur que pose
54
+ * `setHttp2SessionTimeout` (`src/servers/http2Limits.ts`)
41
55
  */
42
- function idleSocketTimeoutHandler(socket) {
43
- return () => {
44
- const ctx = socketActiveContext.get(socket);
45
- if (ctx !== void 0 && !ctx.cleaned && !ctx.response.response?.writableEnded) {
46
- ctx._onTimeout();
47
- return;
48
- }
49
- socket.destroy();
50
- };
56
+ function routeSocketTimeout(socket) {
57
+ if (!(socket instanceof net.Socket)) return;
58
+ const ctx = socketActiveContext.get(socket);
59
+ if (ctx !== void 0 && !ctx.cleaned && !ctx.response.response?.writableEnded) {
60
+ ctx._onTimeout();
61
+ return;
62
+ }
63
+ socket.destroy();
51
64
  }
52
65
  const PERF_PROBE_SUB = process.env.NF_PERF_PROBE === "1";
53
66
  var HttpContext = class extends Context {
@@ -61,6 +74,15 @@ var HttpContext = class extends Context {
61
74
  * son montage. `null` = servie par l'application (cas de toute requête).
62
75
  */
63
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;
64
86
  request;
65
87
  response;
66
88
  uploadService;
@@ -131,11 +153,34 @@ var HttpContext = class extends Context {
131
153
  _onTimeout() {
132
154
  if (this.listenerCount("onTimeout")) this.fire("onTimeout", this);
133
155
  this._abortIfPending("Request timeout");
156
+ if (this.rpc !== null) {
157
+ this.rpc.expire();
158
+ return;
159
+ }
134
160
  this.httpKernel?.warnUnsentResponse(this);
135
- let error;
136
- if (this.response.stream) error = new HttpError("Gateway Timeout", 504, this);
137
- else error = new HttpError("Request Timeout", 408, this);
138
- 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);
139
184
  }
140
185
  setScheme() {
141
186
  const req = this.request;
@@ -162,6 +207,8 @@ var HttpContext = class extends Context {
162
207
  if (kernel && (kernel.debug || kernel.listenerCount("onRequest") > 0)) kernel.fire("onRequest", this);
163
208
  if (!this.resolver && this.router) this.resolver = this.router.resolve(this);
164
209
  if (this.resolver?.resolve) {
210
+ const routeTimeout = this.resolver.route?.responseTimeout;
211
+ if (routeTimeout !== void 0 && routeTimeout !== null) this.rearmTimeout(routeTimeout);
165
212
  this.setMetaData();
166
213
  return this.resolver.callController();
167
214
  }
@@ -182,27 +229,38 @@ var HttpContext = class extends Context {
182
229
  this.activeSocket = socket;
183
230
  const ms = this.response.timeout;
184
231
  if (socket.timeout !== ms) socket.setTimeout(ms);
185
- if (!socketTimeoutArmed.has(socket)) {
186
- socketTimeoutArmed.add(socket);
187
- socket.on("timeout", idleSocketTimeoutHandler(socket));
188
- }
189
232
  }
190
233
  /**
191
234
  * Ligne d'accès — pour une requête RELAYÉE, avec le statut que l'amont a
192
235
  * réellement rendu (le relais écrit la réponse brute, pas celle du
193
236
  * contexte) ; un montage `accessLog: "errors"` ne l'écrit qu'en échec.
194
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
+ *
195
244
  * @param httpError - l'erreur de la requête, s'il y en a une
196
245
  */
197
246
  logRequest(httpError) {
247
+ if (this.rpc !== null) this.adoptNativeStatus();
198
248
  if (this.relayAccessLog !== null) {
199
- const raw = this.response.response;
200
- if (raw !== null) this.response.statusCode = raw.statusCode;
249
+ this.adoptNativeStatus();
201
250
  if (this.relayAccessLog === "errors" && this.response.statusCode < 400 && !httpError && !this.error) return;
202
251
  }
203
252
  return super.logRequest(httpError);
204
253
  }
205
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
+ /**
206
264
  * Nettoie le contexte, et le DÉTACHE de sa socket h1 : la connexion
207
265
  * persistante survit à la requête, et l'entrée de `socketActiveContext`
208
266
  * retiendrait sinon tout le contexte jusqu'à sa fermeture. Seule l'entrée qui
@@ -346,6 +404,7 @@ var HttpContext = class extends Context {
346
404
  *
347
405
  * La requête ne se termine qu'à `close()` du flux, ou au départ du client.
348
406
  *
407
+ * @typeParam Events - carte des événements du flux (cf `SseStream`).
349
408
  * @param options - battement de cœur, délai `retry:` initial.
350
409
  * @returns le flux, ou sa promesse quand une session est à sauver d'abord.
351
410
  * @throws HttpError 500 quand la réponse est déjà partie.
@@ -353,6 +412,54 @@ var HttpContext = class extends Context {
353
412
  openSse(options) {
354
413
  return openSseStream(this, options);
355
414
  }
415
+ /**
416
+ * Répond par un FLUX D'OCTETS : un `<video src>`, un `fetch()`, un
417
+ * téléchargement ou un outil tiers (curl) le consomment comme n'importe quelle
418
+ * réponse HTTP, une réponse par consommateur.
419
+ *
420
+ * La réponse est prise à l'appel, ses en-têtes partent au premier morceau de
421
+ * la source. La source est DÉTRUITE à la fin, quelle qu'en soit la raison
422
+ * (jamais `unpipe`, qui laisse un producteur bloqué en écriture). Pour des
423
+ * événements texte lus par `EventSource`, voir {@link HttpContext.openSse}.
424
+ * Dans un middleware, la requête est répondue : ne pas appeler la suite.
425
+ *
426
+ * @param source - flux Node, ou flux web (`fetch().body`) ; jamais en mode objet.
427
+ * @param options - statut, en-têtes, délais, écouteur de fin.
428
+ * @returns la raison de la fin et le nombre d'octets ; ne rejette jamais.
429
+ * @throws HttpError 500 quand la réponse est déjà partie, 429 au-delà du
430
+ * plafond de connexions par IP ; TypeError pour une source en mode objet.
431
+ * Quand l'appel lève, la source est DÉTRUITE (un flux web est annulé).
432
+ */
433
+ openStream(source, options) {
434
+ return openByteStream(this, source, options);
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
+ }
356
463
  flush(chunk, encoding) {
357
464
  return this.response.flush(chunk, encoding);
358
465
  }
@@ -414,7 +521,7 @@ var HttpContext = class extends Context {
414
521
  return this.request.getMethod();
415
522
  }
416
523
  setContentType(type, encoding) {
417
- return this.response.setContentType(type, encoding);
524
+ this.response.setContentType(type, encoding);
418
525
  }
419
526
  resolveIsHtml() {
420
527
  return this.request.acceptHtml;
@@ -437,4 +544,4 @@ var HttpContext = class extends Context {
437
544
  this.response.setContentType("html", encoding);
438
545
  }
439
546
  };
440
- export { HttpContext as default, switchUrlScheme };
547
+ export { HttpContext as default, routeSocketTimeout, switchUrlScheme };