@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
@@ -8,8 +8,8 @@ var Parser = class {
8
8
  chunks;
9
9
  received = 0;
10
10
  aborted = false;
11
- overflow = null;
12
- onOverflow = null;
11
+ failure = null;
12
+ onFailure = null;
13
13
  constructor(request) {
14
14
  this.request = request;
15
15
  this.chunks = [];
@@ -22,33 +22,51 @@ var Parser = class {
22
22
  const max = this.request.maxBodySize;
23
23
  this.received += buffer.length;
24
24
  if (max > 0 && this.received > max) {
25
- this.aborted = true;
26
- this.chunks.length = 0;
27
- this.overflow = new HttpError(`Request body exceeds maxBodySize (${max} bytes)`, 413, this.request.context);
28
- this.request.request.pause?.();
29
- if (this.onOverflow) this.onOverflow(this.overflow);
25
+ this.fail(new HttpError(`Request body exceeds maxBodySize (${max} bytes)`, 413, this.request.context));
30
26
  return buffer;
31
27
  }
32
28
  this.chunks.push(buffer);
33
29
  return buffer;
34
30
  }
35
31
  /**
32
+ * Interrompt la réception du corps : `parse()` rejette avec `error`, que
33
+ * l'interruption survienne avant ou pendant l'attente de la fin du flux.
34
+ *
35
+ * La RAM déjà bufferisée est libérée et la lecture du flux suspendue ; la
36
+ * réponse d'erreur peut encore partir. Sans effet si le corps est déjà
37
+ * entièrement reçu, ou déjà interrompu.
38
+ *
39
+ * @param error - la cause rendue au client (413 dépassement, 408 délai)
40
+ * @returns vrai si la réception a été interrompue
41
+ */
42
+ fail(error) {
43
+ const req = this.request.request;
44
+ if (this.aborted || req.readableEnded === true || req.complete === true) return false;
45
+ this.aborted = true;
46
+ this.chunks.length = 0;
47
+ this.failure = error;
48
+ req.pause?.();
49
+ if (this.onFailure) this.onFailure(error);
50
+ return true;
51
+ }
52
+ /**
36
53
  * Résout quand le flux requête est **entièrement reçu** (`end`). No-op si déjà
37
54
  * terminé (corps vide / déjà drainé) → pas de hang d'un `once("end")` tardif.
38
55
  * Indispensable avant de concaténer les chunks pour un corps à décoder
39
56
  * (JSON…) : sinon on lit un buffer encore incomplet. (formidable drainait via
40
57
  * son `await form.parse()` ; ses remplaçants doivent drainer explicitement.)
41
- * Rejette en 413 si le corps dépasse `maxBodySize` (avant ou pendant l'attente).
58
+ * Rejette avec la cause de {@link Parser.fail} (413 dépassement de
59
+ * `maxBodySize`, 408 délai de réception), avant ou pendant l'attente.
42
60
  */
43
61
  ended() {
44
- if (this.overflow) return Promise.reject(this.overflow);
62
+ if (this.failure) return Promise.reject(this.failure);
45
63
  const req = this.request.request;
46
64
  return new Promise((resolve, reject) => {
47
65
  if (req.readableEnded || req.complete) return resolve();
48
66
  const cleanup = () => {
49
67
  req.removeListener("end", onEnd);
50
68
  req.removeListener("error", onError);
51
- this.onOverflow = null;
69
+ this.onFailure = null;
52
70
  };
53
71
  const onEnd = () => {
54
72
  cleanup();
@@ -58,7 +76,7 @@ var Parser = class {
58
76
  cleanup();
59
77
  reject(e instanceof Error ? e : new Error(String(e)));
60
78
  };
61
- this.onOverflow = (err) => {
79
+ this.onFailure = (err) => {
62
80
  cleanup();
63
81
  reject(err);
64
82
  };
@@ -0,0 +1,56 @@
1
+ import { HOP_BY_HOP } from "../../proxy/forward.js";
2
+ import { Readable } from "node:stream";
3
+ // nodefony/src/context/http/webResponse.ts
4
+ /**
5
+ * En-têtes qu'une réponse RELAYÉE (issue de `fetch`) ne garde pas. Son corps a
6
+ * déjà été DÉCODÉ par `fetch` : `content-encoding` et `content-length`
7
+ * décrivent encore les octets compressés de l'amont, et les recopier
8
+ * annoncerait un corps qui n'est pas celui qu'on envoie. Ses cookies
9
+ * appartiennent à l'amont : les relayer les poserait sous le domaine de
10
+ * l'application.
11
+ */
12
+ const UPSTREAM_ONLY = /* @__PURE__ */ new Set([
13
+ "content-encoding",
14
+ "content-length",
15
+ "set-cookie"
16
+ ]);
17
+ /** Corps d'une `Response` sans corps (`new Response(null, { status: 204 })`). */
18
+ const emptyBody = () => Readable.from([], { objectMode: false });
19
+ /**
20
+ * Traduit une `Response` Web standard en statut, en-têtes et corps pour le flux
21
+ * d'octets — sans la lire : le corps reste un flux.
22
+ *
23
+ * Deux origines, deux règles, que `response.type` distingue :
24
+ * - **construite par l'application** (`new Response(…)`, `Response.json(…)`,
25
+ * `Response.redirect(…)` — `type === "default"`) : ses en-têtes sont les
26
+ * siens, cookies compris (`getSetCookie()`), et partent tels quels ;
27
+ * - **relayée** (issue de `fetch` — `"basic"`, `"cors"`…) : `content-encoding`,
28
+ * `content-length` et `set-cookie` de l'amont sont écartés (voir
29
+ * `UPSTREAM_ONLY`).
30
+ *
31
+ * Les en-têtes de connexion (RFC 9110 §7.6.1 — la liste du relais,
32
+ * `HOP_BY_HOP`) ne partent jamais : ils décrivent un saut, et HTTP/2 les
33
+ * refuse.
34
+ *
35
+ * @param response - une `Response` dont le corps n'a pas été consommé.
36
+ * @returns statut, en-têtes filtrés et corps.
37
+ */
38
+ function webResponseParts(response) {
39
+ const relayed = response.type !== "default";
40
+ const headers = {};
41
+ for (const [name, value] of response.headers) {
42
+ if (name === "set-cookie" || HOP_BY_HOP.has(name)) continue;
43
+ if (relayed && UPSTREAM_ONLY.has(name)) continue;
44
+ headers[name] = value;
45
+ }
46
+ if (!relayed) {
47
+ const cookies = response.headers.getSetCookie();
48
+ if (cookies.length > 0) headers["set-cookie"] = cookies;
49
+ }
50
+ return {
51
+ status: response.status,
52
+ headers,
53
+ source: response.body ?? emptyBody()
54
+ };
55
+ }
56
+ export { webResponseParts };
@@ -1,4 +1,5 @@
1
1
  import HttpResponse from "../http/Response.js";
2
+ import { closeStreamAfterGrace } from "../../servers/http2StreamGrace.js";
2
3
  import { thenMaybe } from "nodefony";
3
4
  import http from "node:http";
4
5
  import http2 from "node:http2";
@@ -69,16 +70,13 @@ var Http2Response = class extends HttpResponse {
69
70
  if (this.statusCode > 599) this.statusCode = 500;
70
71
  }
71
72
  this.statusMessage = this.getStatusMessage();
73
+ this.putCorrelationHeaders();
72
74
  this.setLength();
73
75
  this.ensureContentTypeHeader();
74
76
  this.applyHeaderPolicy(this.contentTypeForPolicy());
75
77
  this.headers = mergeResponseHeaders(this.statusMessage, this.response ?? emptyHeaders, headers, this.headerList());
76
78
  this.headers[HTTP2_HEADER_STATUS] = this.statusCode;
77
- const requestId = this.context.requestId;
78
- if (requestId) this.headers["x-request-id"] = requestId;
79
- const traceparent = this.context.traceparent;
80
- if (traceparent) this.headers["traceparent"] = traceparent;
81
- this.stream.respond(this.headers, { endStream: false });
79
+ this.stream.respond(this.headers, { endStream: this.headers["grpc-status"] !== void 0 });
82
80
  } else this.log("Headers already sent !!", "WARNING");
83
81
  } else return super.writeHead(statusCode, headers);
84
82
  }
@@ -130,8 +128,9 @@ var Http2Response = class extends HttpResponse {
130
128
  return super.end(chunk, encoding);
131
129
  }
132
130
  /**
133
- * Termine la réponse de compatibilité d'un HEAD, dont Node a clos le flux de
134
- * lui-même au `respond()`.
131
+ * Termine la réponse de compatibilité d'un HEAD, ou d'une réponse gRPC
132
+ * _Trailers-Only_ (statut gRPC dans les en-têtes), dont le flux s'est clos
133
+ * au `respond()`.
135
134
  *
136
135
  * Sans ce geste, `Http2ServerResponse` n'émet ni `finish` ni `close` : le
137
136
  * démontage de la requête, branché sur ce `close` (ligne d'accès, profil,
@@ -142,7 +141,7 @@ var Http2Response = class extends HttpResponse {
142
141
  */
143
142
  settleHeadResponse() {
144
143
  const raw = this.response;
145
- if (raw !== null && this.context.method === "HEAD" && this.stream?.destroyed === false && !raw.writableEnded) raw.end();
144
+ if (raw !== null && (this.context.method === "HEAD" || this.headers["grpc-status"] !== void 0) && this.stream?.destroyed === false && !raw.writableEnded) raw.end();
146
145
  }
147
146
  getStatusMessage(code) {
148
147
  if (code) {
@@ -157,5 +156,45 @@ var Http2Response = class extends HttpResponse {
157
156
  message: ""
158
157
  };
159
158
  }
159
+ /**
160
+ * Règle le sort d'un corps non lu quand la réponse part avant qu'il soit
161
+ * consommé (408 délai, 413 trop gros, refus précoce) : les octets en tampon
162
+ * sont jetés, puis — une fois la réponse écrite — le flux est fermé en
163
+ * `NO_ERROR` si le client ÉMET ENCORE.
164
+ *
165
+ * RFC 9113 §8.1 : après une réponse complète, le serveur PEUT demander au
166
+ * client d'arrêter d'émettre par un `RST_STREAM` `NO_ERROR`. `node:http2` ne
167
+ * le fait pas : le flux resterait ouvert tant que le client émet, et un
168
+ * client lent garderait sa place de flux sur la session. Attendre `finish`
169
+ * (fin d'écriture) garantit que la réponse elle-même n'est pas tronquée.
170
+ *
171
+ * « Émet encore » se décide après une GRÂCE d'un aller-retour
172
+ * ({@link closeStreamAfterGrace} : un `PING` par session pour tous les
173
+ * flux qui attendent, puis l'état du flux). Fini → le flux se ferme seul,
174
+ * sans `RST_STREAM` ; sinon — client qui émet encore, ou muet — fermé.
175
+ * Jamais de coupe avant la grâce : un `RST_STREAM` soumis pendant que la
176
+ * réponse attend dans la file de nghttp2 la fait jeter, et le client reçoit
177
+ * une fin de flux sans statut. grpc-go, Go `net/http2` et nginx coupent AUSSITÔT : Node, lui,
178
+ * répond avant de lire les trames DATA déjà en route, et la coupe
179
+ * immédiate frappait des clients qui finissaient d'émettre — le client
180
+ * HTTP/2 de Go la lit en erreur (ADR-0026 D13 point 1, constaté par la
181
+ * suite de conformité de Connect). `request.complete` ne dit rien ici : il
182
+ * ne passe à `true` qu'une fois le corps CONSOMMÉ.
183
+ *
184
+ * Chemin d'erreur seulement : un `PING` par aller-retour et par session,
185
+ * quel que soit le nombre de refus en attente.
186
+ *
187
+ * Sans effet si le corps est entièrement consommé, ou le flux déjà fermé.
188
+ * Peut s'appeler avant comme après l'écriture de la réponse.
189
+ */
190
+ closeUnfinishedRequest() {
191
+ const stream = this.stream;
192
+ if (stream === null || stream.closed || stream.destroyed) return;
193
+ const request = this.context.request.request;
194
+ if (request.complete) return;
195
+ request.resume();
196
+ if (stream.writableFinished) closeStreamAfterGrace(stream);
197
+ else stream.once("finish", () => closeStreamAfterGrace(stream));
198
+ }
160
199
  };
161
200
  export { Http2Response as default, mergeResponseHeaders };
@@ -1,3 +1,4 @@
1
+ import { rpcOutcomeOf } from "./rpcOutcome.js";
1
2
  import diagnosticsChannel from "node:diagnostics_channel";
2
3
  // nodefony/src/context/requestTrace.ts
3
4
  /**
@@ -46,7 +47,8 @@ function openRequestTrace(context, protocol) {
46
47
  module: null,
47
48
  statusCode: null,
48
49
  durationMs: null,
49
- error: null
50
+ error: null,
51
+ rpc: null
50
52
  };
51
53
  context.requestTrace = trace;
52
54
  return trace;
@@ -82,8 +84,9 @@ function runTraced(trace, fn, thisArg, ...args) {
82
84
  * @param context - le contexte de la requête
83
85
  * @param statusCode - le statut rendu au client, `null` s'il n'y en a pas eu
84
86
  * @param error - l'exception attrapée par le pipeline, `null` ou `undefined` sinon
87
+ * @param rpc - l'appel RPC de la requête, `null` hors RPC : copié, jamais publié tel quel
85
88
  */
86
- function closeRequestTrace(context, statusCode, error) {
89
+ function closeRequestTrace(context, statusCode, error, rpc = null) {
87
90
  const trace = context.requestTrace;
88
91
  if (trace === null) return;
89
92
  context.requestTrace = null;
@@ -93,6 +96,7 @@ function closeRequestTrace(context, statusCode, error) {
93
96
  trace.routePath = path === void 0 ? null : path === "" ? "/" : path;
94
97
  trace.module = context.logModule ?? route?.module?.name ?? null;
95
98
  trace.statusCode = statusCode;
99
+ trace.rpc = rpcOutcomeOf(rpc);
96
100
  trace.durationMs = context.stopClock();
97
101
  if (error !== null && error !== void 0) {
98
102
  trace.error = error;
@@ -0,0 +1,20 @@
1
+ // nodefony/src/context/rpcOutcome.ts
2
+ /**
3
+ * Copie le résultat d'un appel RPC — les quatre champs, jamais la fiche
4
+ * d'appel entière, qui porte le contrôleur et le contexte (ADR-0026 D10). La
5
+ * forme est fixe : un objet par appel, alloué seulement par qui publie (audit
6
+ * JSON, canal de traçage avec abonné).
7
+ *
8
+ * @param call - l'appel RPC de la requête, `null` hors RPC.
9
+ * @returns sa copie, `null` hors RPC.
10
+ */
11
+ function rpcOutcomeOf(call) {
12
+ if (call === null) return null;
13
+ return {
14
+ system: call.system,
15
+ method: call.method,
16
+ code: call.code,
17
+ codeName: call.codeName
18
+ };
19
+ }
20
+ export { rpcOutcomeOf };
@@ -194,13 +194,23 @@ var WebsocketResponse = class {
194
194
  handshakeHeaders = null;
195
195
  /**
196
196
  * Retient un en-tête du handshake (cf {@link handshakeHeaders}) ; sans effet
197
- * une fois la socket ouverte, où il n'y a plus de réponse HTTP.
197
+ * une fois la socket ouverte, où il n'y a plus de réponse HTTP — sauf
198
+ * `Retry-After` posé par une action invoquée par le pont `api.request` : il
199
+ * va au puits de CETTE invocation, et le pont le sert avec le refus. Le même
200
+ * code de contrôleur dit donc « réessaie dans N s » sur les deux transports.
201
+ * Seul cet en-tête passe : un `Set-Cookie` ou un `WWW-Authenticate` n'a
202
+ * aucun sens sur une trame, et rien d'autre ne doit fuir par là.
198
203
  *
199
204
  * @param name - nom de l'en-tête
200
205
  * @param value - valeur
201
206
  * @returns toujours `true`
202
207
  */
203
208
  setHeader(name, value) {
209
+ const sink = RequestContext.get()?.renderSink;
210
+ if (sink) {
211
+ if (name.toLowerCase() === "retry-after" && typeof value !== "object") sink.retryAfter = String(value);
212
+ return true;
213
+ }
204
214
  if (this.connection !== null) return true;
205
215
  this.handshakeHeaders ??= {};
206
216
  this.handshakeHeaders[name] = typeof value === "object" ? value.join(", ") : value;
@@ -107,7 +107,7 @@ function handshakeRefusalStatus(context, error) {
107
107
  * le gate court-circuite AVANT toute allocation/concat (0 surcoût hot path).
108
108
  */
109
109
  let wsContentLogging = null;
110
- var WebsocketContext = class extends Context {
110
+ var WebsocketContext = class WebsocketContext extends Context {
111
111
  request;
112
112
  response = null;
113
113
  acceptedProtocol;
@@ -297,7 +297,7 @@ var WebsocketContext = class extends Context {
297
297
  }
298
298
  async send(data, encoding) {
299
299
  if (this.response) {
300
- const payload = data ?? this.response.body;
300
+ const payload = data ?? WebsocketContext.takeRenderedBody(this.response);
301
301
  const sink = RequestContext.get()?.renderSink;
302
302
  if (sink) {
303
303
  sink.body = payload;
@@ -310,9 +310,21 @@ var WebsocketContext = class extends Context {
310
310
  }
311
311
  throw new Error("No response found");
312
312
  }
313
+ /**
314
+ * Rend le corps rendu (`setBody`) en le RETIRANT de la réponse : une trame
315
+ * envoyée n'a plus à vivre aussi longtemps que la connexion.
316
+ *
317
+ * @param response - la réponse de ce contexte
318
+ * @returns le corps rendu, ou `null` s'il n'y en a pas
319
+ */
320
+ static takeRenderedBody(response) {
321
+ const body = response.body;
322
+ response.body = null;
323
+ return body;
324
+ }
313
325
  broadcast(data, encoding) {
314
326
  if (this.response) {
315
- const payload = data ?? this.response.body;
327
+ const payload = data ?? WebsocketContext.takeRenderedBody(this.response);
316
328
  if (payload) {
317
329
  this.logMessageContent("BROADCAST", payload);
318
330
  this.fire("onMessage", payload, this, "BROADCAST");
@@ -451,7 +463,6 @@ var WebsocketContext = class extends Context {
451
463
  this.webSocketState = "message";
452
464
  const message = isBinary ? data : data.toString();
453
465
  this.logMessageContent("RECEIVE", message);
454
- if (this.response) this.response.body = Buffer.isBuffer(data) ? data : Buffer.from(data);
455
466
  try {
456
467
  if (!this.resolver) this.resolver = this.router?.resolve(this) ?? null;
457
468
  else this.resolver.match(this.resolver.route, this);
@@ -1,7 +1,7 @@
1
1
  import { extend } from "nodefony";
2
- import crypto from "node:crypto";
3
2
  import { parseCookie, stringifySetCookie } from "cookie";
4
3
  import MS from "ms";
4
+ import crypto from "node:crypto";
5
5
  // nodefony/src/cookies/cookie.ts
6
6
  const encode = encodeURIComponent;
7
7
  /**
@@ -43,6 +43,7 @@ function cookiesParser(context) {
43
43
  if (cookies) {
44
44
  const obj = parseCookie(cookies, RAW_VALUES);
45
45
  for (const cookie in obj) {
46
+ if (cookie === "") continue;
46
47
  co = new Cookie(cookie, obj[cookie]);
47
48
  context.addRequestCookie(co);
48
49
  }
@@ -54,6 +55,7 @@ function cookiesParser(context) {
54
55
  if (cookieHeader) {
55
56
  const parsed = parseCookie(cookieHeader, RAW_VALUES);
56
57
  for (const name in parsed) {
58
+ if (name === "") continue;
57
59
  co = new Cookie(name, parsed[name]);
58
60
  context.addRequestCookie(co);
59
61
  }
@@ -1,41 +1,51 @@
1
1
  import { clc, nodefonyError } from "nodefony";
2
2
  // nodefony/src/errors/httpError.ts
3
- const exclude = {
4
- context: true,
5
- resolver: true,
6
- container: true,
7
- secure: true,
8
- request: true,
9
- response: true
10
- };
3
+ /** Références vivantes (circulaires) jamais sérialisées, quel que soit l'environnement. */
4
+ const EXCLUDED_KEYS = /* @__PURE__ */ new Set([
5
+ "context",
6
+ "resolver",
7
+ "container",
8
+ "secure",
9
+ "request",
10
+ "response"
11
+ ]);
12
+ /**
13
+ * Sérialise les propriétés propres d'une erreur HTTP, sans jamais LIRE celles
14
+ * qu'on écarte.
15
+ *
16
+ * Ne pas lire compte : `stack` est un accesseur paresseux de V8, et sa première
17
+ * lecture FORMATE la pile (`prepareStackTrace`, plusieurs µs). Lire puis
18
+ * supprimer, c'est payer ce formatage pour le jeter — ce que faisait le rendu
19
+ * d'erreur en production, sur chaque 404.
20
+ *
21
+ * @param error - l'erreur à sérialiser (en pratique un `HttpError`).
22
+ * @param omit - clés à écarter EN PLUS des références vivantes, ou `null`.
23
+ * @returns un objet plat, prêt pour `JSON.stringify`.
24
+ */
25
+ function serializeHttpError(error, omit = null) {
26
+ const alt = {};
27
+ for (const key of Object.getOwnPropertyNames(error)) {
28
+ if (EXCLUDED_KEYS.has(key) || omit?.has(key)) continue;
29
+ alt[key] = Reflect.get(error, key);
30
+ }
31
+ return alt;
32
+ }
11
33
  const jsonHttpError = {
12
34
  configurable: true,
13
35
  writable: true,
14
36
  value() {
15
- const alt = {};
16
- const storeKey = function(key) {
17
- if (key in exclude) return;
18
- alt[key] = this[key];
19
- };
20
- Object.getOwnPropertyNames(this).forEach(storeKey, this);
21
- return alt;
37
+ return serializeHttpError(this);
22
38
  }
23
39
  };
24
- var HttpError = class extends nodefonyError {
25
- context;
26
- response;
27
- request;
40
+ var HttpError = class HttpError extends nodefonyError {
28
41
  url;
29
42
  controller;
30
43
  action;
31
44
  jsonResponse;
32
45
  constructor(message, code, context) {
33
46
  super(message, code);
34
- this.context = context;
35
47
  const res = context?.response;
36
- this.response = res ?? void 0;
37
- this.request = context?.request ?? void 0;
38
- this.url = this.context?.url;
48
+ this.url = context?.url;
39
49
  if (res && code) res.statusCode = code;
40
50
  if (!this.message && res) this.message = res.body?.toString() || res.statusMessage || res.getStatusMessage();
41
51
  const resolver = context?.resolver;
@@ -43,8 +53,31 @@ var HttpError = class extends nodefonyError {
43
53
  this.action = resolver?.actionName ?? void 0;
44
54
  if (res) this.jsonResponse = `${res.statusCode} ${res.statusMessage}`.trim();
45
55
  }
56
+ /**
57
+ * Construit un VERDICT de protocole (404, 405, 426…) sans capturer de pile.
58
+ *
59
+ * La pile d'un verdict pointe toujours au même endroit du noyau ou du routeur
60
+ * et ne dit rien de la requête ; la capturer coûte ~3,6 µs à profondeur
61
+ * réelle (`micro-error-path.mjs`), payés par chaque requête d'un scanner.
62
+ * Réservé à ce que le framework DÉCIDE — une panne garde sa pile. `stack` ne
63
+ * porte que l'en-tête (`HttpError: Not Found`).
64
+ *
65
+ * @param message - message du verdict.
66
+ * @param code - statut HTTP (ou code de fermeture WebSocket).
67
+ * @param context - contexte de la requête, comme pour le constructeur.
68
+ * @returns l'erreur, sans pile.
69
+ */
70
+ static withoutStack(message, code, context) {
71
+ const limit = Error.stackTraceLimit;
72
+ Error.stackTraceLimit = 0;
73
+ try {
74
+ return new HttpError(message, code, context);
75
+ } finally {
76
+ Error.stackTraceLimit = limit;
77
+ }
78
+ }
46
79
  toString() {
47
- if (this.context) return `${clc.red(this.message)}
80
+ if (this.url !== void 0) return `${clc.red(this.message)}
48
81
  ${clc.blue("Name :")} ${this.name}
49
82
  ${clc.blue("Type :")} ${this.errorType}
50
83
  ${clc.green("Controller :")} ${this.controller}
@@ -65,4 +98,4 @@ var HttpError = class extends nodefonyError {
65
98
  }
66
99
  };
67
100
  Object.defineProperty(HttpError.prototype, "toJSON", jsonHttpError);
68
- export { HttpError as default };
101
+ export { HttpError as default, serializeHttpError };
@@ -0,0 +1,59 @@
1
+ import { hasGrpcRoutes } from "./refusal.js";
2
+ // nodefony/src/grpc/cors.ts
3
+ /**
4
+ * Les listes de Connect (`cors` de `@connectrpc/connect`, `connect-cors.md`),
5
+ * recopiées : `@nodefony/http` principal ne charge jamais Connect, pair
6
+ * facultatif (ADR-0026 D12). La copie est COMPARÉE à l'original par un test
7
+ * (`grpcCors.test.ts`) : une version de Connect qui change ses listes le fait
8
+ * tomber.
9
+ */
10
+ const GRPC_CORS = Object.freeze({
11
+ allowedMethods: Object.freeze(["POST", "GET"]),
12
+ allowedHeaders: Object.freeze([
13
+ "Content-Type",
14
+ "Connect-Protocol-Version",
15
+ "Connect-Timeout-Ms",
16
+ "Connect-Content-Encoding",
17
+ "Connect-Accept-Encoding",
18
+ "Content-Encoding",
19
+ "Accept-Encoding",
20
+ "Grpc-Message-Type",
21
+ "X-Grpc-Web",
22
+ "X-User-Agent",
23
+ "Grpc-Timeout"
24
+ ]),
25
+ exposedHeaders: Object.freeze([
26
+ "Grpc-Status",
27
+ "Grpc-Message",
28
+ "Grpc-Status-Details-Bin",
29
+ "Content-Encoding",
30
+ "Connect-Content-Encoding"
31
+ ])
32
+ });
33
+ /**
34
+ * `Access-Control-Expose-Headers` d'une réponse RPC, composé une fois au
35
+ * chargement.
36
+ */
37
+ const RPC_EXPOSED_HEADERS = GRPC_CORS.exposedHeaders.join(", ");
38
+ /**
39
+ * Les listes CORS que demandent les protocoles RPC — dès qu'un service gRPC
40
+ * est déclaré dans le processus, `null` sinon. Le pare-feu les ajoute à la
41
+ * politique de l'application au démarrage (`onReady`) : le préflight ne
42
+ * connaît pas la route, l'union est la seule voie (ADR-0026 D9 point 2).
43
+ *
44
+ * @returns les listes, ou `null` sans service gRPC déclaré.
45
+ * @experimental Surface gRPC, ADR-0026 D11.
46
+ */
47
+ function rpcCorsLists() {
48
+ return hasGrpcRoutes() ? GRPC_CORS : null;
49
+ }
50
+ /**
51
+ * Les listes de Connect telles que recopiées, sans condition — réservé au test
52
+ * qui les compare à l'original.
53
+ *
54
+ * @internal
55
+ */
56
+ function grpcCorsCopy() {
57
+ return GRPC_CORS;
58
+ }
59
+ export { RPC_EXPOSED_HEADERS, grpcCorsCopy, rpcCorsLists };
@@ -0,0 +1,16 @@
1
+ // nodefony/src/grpc/limits.ts
2
+ /**
3
+ * Borne par défaut d'un message gRPC, reçu ou émis : 4 Mio (ADR-0026 D4,
4
+ * points 1 et 2). C'est la borne de réception des implémentations de
5
+ * référence (`grpc-js`, `grpc-go`) ; l'adaptateur Connect, lui, n'en pose
6
+ * aucune (`limit-io.js` : `0xffffffff`). Seule source de cette valeur : le
7
+ * schéma de la section `grpc` et le point d'entrée gRPC la lisent ici.
8
+ */
9
+ const GRPC_DEFAULT_MAX_MESSAGE_BYTES = 4194304;
10
+ /**
11
+ * Plus grande borne qu'accepte l'adaptateur Connect pour `readMaxBytes` et
12
+ * `writeMaxBytes` (`limit-io.js#validateReadWriteMaxBytes`) : la longueur
13
+ * d'une enveloppe tient sur 32 bits.
14
+ */
15
+ const GRPC_MAX_MESSAGE_BYTES = 4294967295;
16
+ export { GRPC_DEFAULT_MAX_MESSAGE_BYTES, GRPC_MAX_MESSAGE_BYTES };