@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
@@ -5,6 +5,20 @@ import { Pci, Pdu, Message, Severity, Msgid } from "nodefony";
5
5
  import type { MaybePromise } from "nodefony";
6
6
  import Cookie from "../../cookies/cookie.js";
7
7
  import type { IFirewallGate } from "../../../interfaces/ISecurity.js";
8
+ /**
9
+ * Plafond, en octets, d'un corps de requête HTTP/1.1 non lu que le serveur
10
+ * accepte de vider pour garder la connexion réutilisable — 256 Kio, le geste du
11
+ * serveur HTTP de Go que Connect reprend (ADR-0026, D5). Au-delà, la réponse
12
+ * ferme la connexion.
13
+ */
14
+ export declare const UNREAD_BODY_DRAIN_LIMIT: number;
15
+ /**
16
+ * Silence, en millisecondes, au bout duquel une connexion HTTP/1.1 fermée sur
17
+ * un corps non lu est détruite — 5 s, le `lingering_timeout` par défaut de
18
+ * nginx. La durée TOTALE reste bornée par le délai de réception de Node
19
+ * (`requestTimeout`), qui court tant que le corps n'est pas fini.
20
+ */
21
+ export declare const UNREAD_BODY_LINGER_MS = 5000;
8
22
  /**
9
23
  * Vrai pour un statut dont la réponse n'a, par définition, aucun contenu —
10
24
  * 204, 205 et 304 (RFC 9110 §15.3.5, §15.3.6, §15.4.5).
@@ -137,13 +151,23 @@ declare class HttpResponse {
137
151
  * sinon le `Content-Type` posé.
138
152
  */
139
153
  protected contentTypeForPolicy(): string | null;
154
+ /**
155
+ * Range les en-têtes de corrélation du contexte — `X-Request-Id` et le
156
+ * `traceparent` W3C — que toute réponse renvoie, quel qu'en soit l'émetteur.
157
+ * Validés par Node à la remise, comme le reste de la réserve.
158
+ */
159
+ protected putCorrelationHeaders(): void;
140
160
  /**
141
161
  * Recopie la réserve sur la réponse Node, pour un émetteur qui écrit sans
142
- * passer par {@link writeHead} — fichier statique, relais vers un amont. La
143
- * réserve se vide ; ce qui se pose ensuite y retourne, et Node fusionne les
144
- * deux au `writeHead`.
162
+ * passer par {@link writeHead} — fichier statique, relais vers un amont,
163
+ * adaptateur RPC. La réserve se vide ; ce qui se pose ensuite y retourne, et
164
+ * Node fusionne les deux au `writeHead` (l'émetteur l'emporte, nom par nom).
165
+ *
166
+ * @param contentType - type de la réponse que l'émetteur écrira, quand il
167
+ * est connu d'avance (adaptateur RPC). Omis : il se lit à l'extension du
168
+ * chemin, comme le serveur statique le fera.
145
169
  */
146
- commitHeaders(): void;
170
+ commitHeaders(contentType?: string): void;
147
171
  /**
148
172
  * Recopie la réserve sur la réponse Node au PREMIER `setHeader` qu'un tiers
149
173
  * y fera — fichier statique servi, redirection de dossier — et seulement
@@ -151,11 +175,20 @@ declare class HttpResponse {
151
175
  * rien, sa réponse part ensuite en un seul `writeHead`. À désarmer par
152
176
  * {@link disarmNativeWrite} quand le tiers rend la main sans avoir écrit.
153
177
  *
154
- * L'interception est une propriété PROPRE de la réponse, ajoutée puis
155
- * retirée : V8 annule alors la dernière transition de forme de l'objet.
178
+ * L'interception est une propriété PROPRE de la réponse, posée une fois avec
179
+ * des attributs constants — toutes les réponses interceptées partagent donc
180
+ * la même forme — et désarmée en lui RÉAFFECTANT la méthode native, jamais
181
+ * en la supprimant : un `delete` fait basculer la réponse Node en mode
182
+ * dictionnaire (constaté par `%HasFastProperties` sur Node 26, 200 réponses
183
+ * sur 200), et chaque accès suivant de `node:http` le paie — mesuré in situ
184
+ * sur un 404 : `writeHead` +1,1 µs, `end` +4,3 µs.
156
185
  */
157
186
  commitBeforeNativeWrite(): void;
158
- /** Retire l'interception posée par {@link commitBeforeNativeWrite}. */
187
+ /**
188
+ * Retire l'interception posée par {@link commitBeforeNativeWrite} : la
189
+ * propriété propre reçoit la méthode native — la forme de l'objet ne change
190
+ * pas, la réponse reste en mode rapide pour V8.
191
+ */
159
192
  disarmNativeWrite(): void;
160
193
  /**
161
194
  * Retire un en-tête, de la réserve comme de la réponse Node.
@@ -232,6 +265,27 @@ declare class HttpResponse {
232
265
  * @throws de façon SYNCHRONE quand la réponse Node n'existe plus.
233
266
  */
234
267
  end(chunk?: string | Buffer, encoding?: BufferEncoding): MaybePromise<http.ServerResponse | http2.ServerHttp2Stream>;
268
+ /**
269
+ * Règle le sort d'un corps de requête resté NON LU au moment de répondre —
270
+ * route inconnue (404, 405), refus avant l'action, 408, 413. À appeler AVANT
271
+ * l'écriture des en-têtes ; sans effet si le corps est entièrement reçu.
272
+ *
273
+ * HTTP/1.1 : Node vide lui-même un corps non consommé une fois la réponse
274
+ * écrite, pour réutiliser la connexion — sans borne. On ne le laisse faire
275
+ * que pour un corps DÉCLARÉ d'au plus {@link UNREAD_BODY_DRAIN_LIMIT}
276
+ * octets ; au-delà, ou sans longueur déclarée (`transfer-encoding`), la
277
+ * réponse part avec `connection: close` (RFC 9112 §9.6) et la connexion
278
+ * n'est pas réutilisée — fermée par étapes ({@link lingerBeforeClose}),
279
+ * pour que le client qui écrit encore lise la réponse. La décision se prend
280
+ * sur la longueur ANNONCÉE, sans attendre un octet : la réponse n'est jamais
281
+ * retardée par le corps.
282
+ *
283
+ * HTTP/2 surcharge cette méthode (`Http2Response`) : fermeture du flux en
284
+ * `NO_ERROR` une fois la réponse écrite. Une seule entrée pour les deux
285
+ * protocoles, partagée par tout refus qui laisse un corps non lu (ADR-0026,
286
+ * D5).
287
+ */
288
+ closeUnfinishedRequest(): void;
235
289
  /**
236
290
  * Valeur d'un en-tête posé — dans la réserve, sinon sur la réponse Node (ce
237
291
  * qu'un tiers y a écrit directement : `send`, quota).
@@ -70,7 +70,7 @@ export interface ISseSendOptions<K extends string = string> {
70
70
  }
71
71
  /** Battement de cœur par défaut, en ms (§9.2.7 : « every 15 seconds or so »). */
72
72
  export declare const SSE_HEARTBEAT_MS = 15000;
73
- /** Délai de blocage d'écriture par défaut, en ms. */
73
+ /** Délai de blocage d'écriture par défaut, en ms — celui de tout flux. */
74
74
  export declare const SSE_STALL_TIMEOUT_MS = 30000;
75
75
  /** Taille maximale d'un événement par défaut, en octets (1 Mio). */
76
76
  export declare const SSE_MAX_EVENT_BYTES = 1048576;
@@ -27,6 +27,29 @@ import type http2 from "node:http2";
27
27
  * mille fois par seconde.
28
28
  */
29
29
  export declare const MAX_TIMER_MS = 2147483647;
30
+ /**
31
+ * Délai de blocage par défaut d'un flux, en ms : un client qui ne lit plus
32
+ * depuis ce délai est coupé. Valeur commune aux flux d'événements, d'octets et
33
+ * aux flux RPC.
34
+ */
35
+ export declare const STREAM_STALL_TIMEOUT_MS = 30000;
36
+ /**
37
+ * Période du balayage de blocage ({@link StallSweep}) : la moitié du délai de
38
+ * blocage. Un client bloqué est coupé entre `stallTimeout` et
39
+ * `stallTimeout + période` après la première contre-pression vue — jamais
40
+ * avant le délai réglé.
41
+ */
42
+ export declare const STALL_SWEEP_PERIOD_MS: number;
43
+ /**
44
+ * Attente maximale, à l'arrêt du serveur, des flux longs qu'on vient de
45
+ * terminer (`HttpKernel.shutdownStreams`), avant le drain des connexions.
46
+ *
47
+ * Un flux qui écoute son signal se ferme en quelques millisecondes : l'attente
48
+ * finit avec lui. Elle ne dure jusqu'à son terme que pour un gestionnaire qui
49
+ * n'écoute rien — et ce temps s'AJOUTE au drain (`shutdownTimeout`), d'où une
50
+ * borne courte, celle de l'annonce du GOAWAY.
51
+ */
52
+ export declare const SHUTDOWN_STREAMS_WAIT_MS = 1000;
30
53
  /**
31
54
  * Qui a fermé le flux, quand ce n'est pas le client : le serveur (`close()` —
32
55
  * fin demandée, durée de vie, révocation), ou la coupure d'un client qui ne lit
@@ -43,6 +66,21 @@ export interface IStreamLifecycleSettings {
43
66
  revalidate: (() => Promise<boolean>) | null;
44
67
  /** Identifiant de connexion de l'identité qui ouvre le flux — `null` anonyme. */
45
68
  identifier: string | null;
69
+ /**
70
+ * Les écritures appartiennent à un tiers (adaptateur RPC) : la brique ne les
71
+ * voit pas. Le blocage s'observe alors par le balayage du noyau
72
+ * ({@link StallSweep}), et la fermeture par le serveur détruit le transport
73
+ * au lieu de terminer la réponse — jamais un second écrivain.
74
+ */
75
+ foreignWriter: boolean;
76
+ /**
77
+ * Le flux n'a pas de fin à lui (SSE, flux RPC, sortie d'un processus) : à
78
+ * l'arrêt du serveur, le noyau le termine AVANT le drain des connexions
79
+ * ({@link StreamLifecycle.shutdown}), au lieu de le laisser détruire sans un
80
+ * mot à `shutdownTimeout`. `false` pour une source FINIE (une archive) : elle
81
+ * a le drain pour finir, comme toute réponse en vol.
82
+ */
83
+ endsAtShutdown: boolean;
46
84
  }
47
85
  /** Ce que l'ouverture reçoit de l'appelant. */
48
86
  export interface IOpenStreamLifecycle {
@@ -50,4 +88,19 @@ export interface IOpenStreamLifecycle {
50
88
  label: string;
51
89
  stallTimeout: number | false;
52
90
  maxDuration: number | false;
91
+ /**
92
+ * Un tiers tient les écritures (adaptateur RPC) — cf
93
+ * {@link IStreamLifecycleSettings.foreignWriter}. Défaut `false`.
94
+ */
95
+ foreignWriter?: boolean;
96
+ /**
97
+ * Terminé par le noyau à l'arrêt du serveur — cf
98
+ * {@link IStreamLifecycleSettings.endsAtShutdown}. Défaut `true`.
99
+ */
100
+ endsAtShutdown?: boolean;
101
+ }
102
+ /** Ce que le registre de l'arrêt retient d'un flux : de quoi le terminer. */
103
+ export interface IShutdownStream {
104
+ /** Le serveur s'arrête : le flux prend fin (cf {@link StreamLifecycle.shutdown}). */
105
+ shutdown(): unknown;
53
106
  }
@@ -2,22 +2,36 @@ import HttpRequest from "./Request.js";
2
2
  import Http2Request from "../http2/Request.js";
3
3
  import QS from "qs";
4
4
  import xml2js from "xml2js";
5
+ import HttpError from "../../errors/httpError.js";
5
6
  declare class Parser {
6
7
  request: HttpRequest | Http2Request;
7
8
  chunks: Buffer[];
8
9
  private received;
9
10
  private aborted;
10
- private overflow;
11
- private onOverflow;
11
+ private failure;
12
+ private onFailure;
12
13
  constructor(request: HttpRequest | Http2Request);
13
14
  write(buffer: Buffer): Buffer;
15
+ /**
16
+ * Interrompt la réception du corps : `parse()` rejette avec `error`, que
17
+ * l'interruption survienne avant ou pendant l'attente de la fin du flux.
18
+ *
19
+ * La RAM déjà bufferisée est libérée et la lecture du flux suspendue ; la
20
+ * réponse d'erreur peut encore partir. Sans effet si le corps est déjà
21
+ * entièrement reçu, ou déjà interrompu.
22
+ *
23
+ * @param error - la cause rendue au client (413 dépassement, 408 délai)
24
+ * @returns vrai si la réception a été interrompue
25
+ */
26
+ fail(error: HttpError): boolean;
14
27
  /**
15
28
  * Résout quand le flux requête est **entièrement reçu** (`end`). No-op si déjà
16
29
  * terminé (corps vide / déjà drainé) → pas de hang d'un `once("end")` tardif.
17
30
  * Indispensable avant de concaténer les chunks pour un corps à décoder
18
31
  * (JSON…) : sinon on lit un buffer encore incomplet. (formidable drainait via
19
32
  * son `await form.parse()` ; ses remplaçants doivent drainer explicitement.)
20
- * Rejette en 413 si le corps dépasse `maxBodySize` (avant ou pendant l'attente).
33
+ * Rejette avec la cause de {@link Parser.fail} (413 dépassement de
34
+ * `maxBodySize`, 408 délai de réception), avant ou pendant l'attente.
21
35
  */
22
36
  protected ended(): Promise<void>;
23
37
  parse(): Promise<this>;
@@ -0,0 +1,31 @@
1
+ import type http from "node:http";
2
+ import type { ByteStreamSource } from "./ByteStream.js";
3
+ /** Ce qu'une `Response` Web devient pour le flux d'octets de Nodefony. */
4
+ export interface IWebResponseParts {
5
+ /** Statut de la réponse. */
6
+ status: number;
7
+ /** En-têtes à poser, filtrés (voir {@link webResponseParts}). */
8
+ headers: http.OutgoingHttpHeaders;
9
+ /** Corps, Web ou vide — jamais `null`. */
10
+ source: ByteStreamSource;
11
+ }
12
+ /**
13
+ * Traduit une `Response` Web standard en statut, en-têtes et corps pour le flux
14
+ * d'octets — sans la lire : le corps reste un flux.
15
+ *
16
+ * Deux origines, deux règles, que `response.type` distingue :
17
+ * - **construite par l'application** (`new Response(…)`, `Response.json(…)`,
18
+ * `Response.redirect(…)` — `type === "default"`) : ses en-têtes sont les
19
+ * siens, cookies compris (`getSetCookie()`), et partent tels quels ;
20
+ * - **relayée** (issue de `fetch` — `"basic"`, `"cors"`…) : `content-encoding`,
21
+ * `content-length` et `set-cookie` de l'amont sont écartés (voir
22
+ * `UPSTREAM_ONLY`).
23
+ *
24
+ * Les en-têtes de connexion (RFC 9110 §7.6.1 — la liste du relais,
25
+ * `HOP_BY_HOP`) ne partent jamais : ils décrivent un saut, et HTTP/2 les
26
+ * refuse.
27
+ *
28
+ * @param response - une `Response` dont le corps n'a pas été consommé.
29
+ * @returns statut, en-têtes filtrés et corps.
30
+ */
31
+ export declare function webResponseParts(response: Response): IWebResponseParts;
@@ -54,8 +54,9 @@ declare class Http2Response extends HttpResponse {
54
54
  */
55
55
  end(chunk?: string | Buffer, encoding?: BufferEncoding): MaybePromise<http.ServerResponse | http2.ServerHttp2Stream>;
56
56
  /**
57
- * Termine la réponse de compatibilité d'un HEAD, dont Node a clos le flux de
58
- * lui-même au `respond()`.
57
+ * Termine la réponse de compatibilité d'un HEAD, ou d'une réponse gRPC
58
+ * _Trailers-Only_ (statut gRPC dans les en-têtes), dont le flux s'est clos
59
+ * au `respond()`.
59
60
  *
60
61
  * Sans ce geste, `Http2ServerResponse` n'émet ni `finish` ni `close` : le
61
62
  * démontage de la requête, branché sur ce `close` (ligne d'accès, profil,
@@ -70,5 +71,37 @@ declare class Http2Response extends HttpResponse {
70
71
  code: number;
71
72
  message: string;
72
73
  };
74
+ /**
75
+ * Règle le sort d'un corps non lu quand la réponse part avant qu'il soit
76
+ * consommé (408 délai, 413 trop gros, refus précoce) : les octets en tampon
77
+ * sont jetés, puis — une fois la réponse écrite — le flux est fermé en
78
+ * `NO_ERROR` si le client ÉMET ENCORE.
79
+ *
80
+ * RFC 9113 §8.1 : après une réponse complète, le serveur PEUT demander au
81
+ * client d'arrêter d'émettre par un `RST_STREAM` `NO_ERROR`. `node:http2` ne
82
+ * le fait pas : le flux resterait ouvert tant que le client émet, et un
83
+ * client lent garderait sa place de flux sur la session. Attendre `finish`
84
+ * (fin d'écriture) garantit que la réponse elle-même n'est pas tronquée.
85
+ *
86
+ * « Émet encore » se décide après une GRÂCE d'un aller-retour
87
+ * ({@link closeStreamAfterGrace} : un `PING` par session pour tous les
88
+ * flux qui attendent, puis l'état du flux). Fini → le flux se ferme seul,
89
+ * sans `RST_STREAM` ; sinon — client qui émet encore, ou muet — fermé.
90
+ * Jamais de coupe avant la grâce : un `RST_STREAM` soumis pendant que la
91
+ * réponse attend dans la file de nghttp2 la fait jeter, et le client reçoit
92
+ * une fin de flux sans statut. grpc-go, Go `net/http2` et nginx coupent AUSSITÔT : Node, lui,
93
+ * répond avant de lire les trames DATA déjà en route, et la coupe
94
+ * immédiate frappait des clients qui finissaient d'émettre — le client
95
+ * HTTP/2 de Go la lit en erreur (ADR-0026 D13 point 1, constaté par la
96
+ * suite de conformité de Connect). `request.complete` ne dit rien ici : il
97
+ * ne passe à `true` qu'une fois le corps CONSOMMÉ.
98
+ *
99
+ * Chemin d'erreur seulement : un `PING` par aller-retour et par session,
100
+ * quel que soit le nombre de refus en attente.
101
+ *
102
+ * Sans effet si le corps est entièrement consommé, ou le flux déjà fermé.
103
+ * Peut s'appeler avant comme après l'écriture de la réponse.
104
+ */
105
+ closeUnfinishedRequest(): void;
73
106
  }
74
107
  export default Http2Response;
@@ -1,4 +1,5 @@
1
1
  import type { IRequestTrace, RequestTraceProtocol } from "../../interfaces/IRequestTrace.js";
2
+ import type { IRpcOutcome } from "../../interfaces/IRpcCall.js";
2
3
  import type Context from "./Context.js";
3
4
  /**
4
5
  * Nom du `TracingChannel` qui encadre chaque requête HTTP et chaque handshake
@@ -60,5 +61,6 @@ export declare function runTraced<ThisArg, Args extends unknown[], Result>(trace
60
61
  * @param context - le contexte de la requête
61
62
  * @param statusCode - le statut rendu au client, `null` s'il n'y en a pas eu
62
63
  * @param error - l'exception attrapée par le pipeline, `null` ou `undefined` sinon
64
+ * @param rpc - l'appel RPC de la requête, `null` hors RPC : copié, jamais publié tel quel
63
65
  */
64
- export declare function closeRequestTrace(context: Context, statusCode: number | null, error: unknown): void;
66
+ export declare function closeRequestTrace(context: Context, statusCode: number | null, error: unknown, rpc?: IRpcOutcome | null): void;
@@ -0,0 +1,11 @@
1
+ import type { IRpcOutcome } from "../../interfaces/IRpcCall.js";
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
+ export declare function rpcOutcomeOf(call: IRpcOutcome | null): IRpcOutcome | null;
@@ -54,7 +54,12 @@ declare class WebsocketResponse {
54
54
  handshakeHeaders: Record<string, string | number> | null;
55
55
  /**
56
56
  * Retient un en-tête du handshake (cf {@link handshakeHeaders}) ; sans effet
57
- * une fois la socket ouverte, où il n'y a plus de réponse HTTP.
57
+ * une fois la socket ouverte, où il n'y a plus de réponse HTTP — sauf
58
+ * `Retry-After` posé par une action invoquée par le pont `api.request` : il
59
+ * va au puits de CETTE invocation, et le pont le sert avec le refus. Le même
60
+ * code de contrôleur dit donc « réessaie dans N s » sur les deux transports.
61
+ * Seul cet en-tête passe : un `Set-Cookie` ou un `WWW-Authenticate` n'a
62
+ * aucun sens sur une trame, et rien d'autre ne doit fuir par là.
58
63
  *
59
64
  * @param name - nom de l'en-tête
60
65
  * @param value - valeur
@@ -123,6 +123,14 @@ declare class WebsocketContext extends Context implements IWebsocketContextInter
123
123
  handle(data?: unknown[]): Promise<this>;
124
124
  render(chunk: unknown, encoding?: BufferEncoding): Promise<WebsocketResponse>;
125
125
  send(data?: string | Buffer | null, encoding?: BufferEncoding): Promise<WebsocketResponse>;
126
+ /**
127
+ * Rend le corps rendu (`setBody`) en le RETIRANT de la réponse : une trame
128
+ * envoyée n'a plus à vivre aussi longtemps que la connexion.
129
+ *
130
+ * @param response - la réponse de ce contexte
131
+ * @returns le corps rendu, ou `null` s'il n'y en a pas
132
+ */
133
+ private static takeRenderedBody;
126
134
  broadcast(data?: string | Buffer | null, encoding?: BufferEncoding): null | undefined;
127
135
  /**
128
136
  * Le contenu des messages est-il journalisé ? Hors production seulement —
@@ -1,15 +1,40 @@
1
1
  import { nodefonyError as NodefonyError } from "nodefony";
2
2
  import { ContextType } from "../../service/http-kernel.js";
3
- import { HttpRequestType, HttpRsponseType } from "../context/http/HttpContext.js";
3
+ /**
4
+ * Sérialise les propriétés propres d'une erreur HTTP, sans jamais LIRE celles
5
+ * qu'on écarte.
6
+ *
7
+ * Ne pas lire compte : `stack` est un accesseur paresseux de V8, et sa première
8
+ * lecture FORMATE la pile (`prepareStackTrace`, plusieurs µs). Lire puis
9
+ * supprimer, c'est payer ce formatage pour le jeter — ce que faisait le rendu
10
+ * d'erreur en production, sur chaque 404.
11
+ *
12
+ * @param error - l'erreur à sérialiser (en pratique un `HttpError`).
13
+ * @param omit - clés à écarter EN PLUS des références vivantes, ou `null`.
14
+ * @returns un objet plat, prêt pour `JSON.stringify`.
15
+ */
16
+ export declare function serializeHttpError(error: Error, omit?: ReadonlySet<string> | null): Record<string, unknown>;
4
17
  declare class HttpError extends NodefonyError {
5
- context?: ContextType | undefined;
6
- response?: HttpRsponseType | undefined;
7
- request?: HttpRequestType | undefined;
8
18
  url?: string | undefined;
9
19
  controller?: string | undefined;
10
20
  action?: string | undefined;
11
21
  jsonResponse?: string | undefined;
12
22
  constructor(message?: unknown, code?: number, context?: ContextType);
23
+ /**
24
+ * Construit un VERDICT de protocole (404, 405, 426…) sans capturer de pile.
25
+ *
26
+ * La pile d'un verdict pointe toujours au même endroit du noyau ou du routeur
27
+ * et ne dit rien de la requête ; la capturer coûte ~3,6 µs à profondeur
28
+ * réelle (`micro-error-path.mjs`), payés par chaque requête d'un scanner.
29
+ * Réservé à ce que le framework DÉCIDE — une panne garde sa pile. `stack` ne
30
+ * porte que l'en-tête (`HttpError: Not Found`).
31
+ *
32
+ * @param message - message du verdict.
33
+ * @param code - statut HTTP (ou code de fermeture WebSocket).
34
+ * @param context - contexte de la requête, comme pour le constructeur.
35
+ * @returns l'erreur, sans pile.
36
+ */
37
+ static withoutStack(message: string, code: number, context?: ContextType): HttpError;
13
38
  toString(): string;
14
39
  }
15
40
  export default HttpError;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Ce que les protocoles RPC servis (gRPC, gRPC-Web, Connect) demandent à la
3
+ * politique CORS de l'application (ADR-0026 D9).
4
+ *
5
+ * @experimental Surface gRPC, ADR-0026 D11.
6
+ */
7
+ export interface IRpcCorsLists {
8
+ /** Méthodes que le préflight doit autoriser. */
9
+ readonly allowedMethods: readonly string[];
10
+ /** En-têtes de requête que le préflight doit autoriser. */
11
+ readonly allowedHeaders: readonly string[];
12
+ /**
13
+ * En-têtes qu'une réponse RPC doit exposer au script de la page — sans eux,
14
+ * un client gRPC-Web ne lit ni le statut ni l'encodage de la réponse.
15
+ */
16
+ readonly exposedHeaders: readonly string[];
17
+ }
18
+ /**
19
+ * `Access-Control-Expose-Headers` d'une réponse RPC, composé une fois au
20
+ * chargement.
21
+ */
22
+ export declare const RPC_EXPOSED_HEADERS: string;
23
+ /**
24
+ * Les listes CORS que demandent les protocoles RPC — dès qu'un service gRPC
25
+ * est déclaré dans le processus, `null` sinon. Le pare-feu les ajoute à la
26
+ * politique de l'application au démarrage (`onReady`) : le préflight ne
27
+ * connaît pas la route, l'union est la seule voie (ADR-0026 D9 point 2).
28
+ *
29
+ * @returns les listes, ou `null` sans service gRPC déclaré.
30
+ * @experimental Surface gRPC, ADR-0026 D11.
31
+ */
32
+ export declare function rpcCorsLists(): IRpcCorsLists | null;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Borne par défaut d'un message gRPC, reçu ou émis : 4 Mio (ADR-0026 D4,
3
+ * points 1 et 2). C'est la borne de réception des implémentations de
4
+ * référence (`grpc-js`, `grpc-go`) ; l'adaptateur Connect, lui, n'en pose
5
+ * aucune (`limit-io.js` : `0xffffffff`). Seule source de cette valeur : le
6
+ * schéma de la section `grpc` et le point d'entrée gRPC la lisent ici.
7
+ */
8
+ export declare const GRPC_DEFAULT_MAX_MESSAGE_BYTES: number;
9
+ /**
10
+ * Plus grande borne qu'accepte l'adaptateur Connect pour `readMaxBytes` et
11
+ * `writeMaxBytes` (`limit-io.js#validateReadWriteMaxBytes`) : la longueur
12
+ * d'une enveloppe tient sur 32 bits.
13
+ */
14
+ export declare const GRPC_MAX_MESSAGE_BYTES = 4294967295;
@@ -0,0 +1,136 @@
1
+ import type http from "node:http";
2
+ /**
3
+ * Refus d'un appel gRPC au format de son protocole (ADR-0026 D5 point 2, D13
4
+ * point 2). Sans Connect : une application sans service gRPC charge ce module
5
+ * avec le noyau, et n'y paie rien tant qu'aucune erreur n'est rendue.
6
+ *
7
+ * Le protocole se reconnaît aux en-têtes de la requête et se code en petit
8
+ * entier (`GRPC_WIRE_*`, 0 = aucun) : une valeur toujours du même type, lue
9
+ * sur le chemin d'erreur seulement.
10
+ */
11
+ /** Requête qui ne parle aucun protocole gRPC. */
12
+ export declare const GRPC_WIRE_NONE = 0;
13
+ /** gRPC natif (`application/grpc`, `application/grpc+proto`…). */
14
+ export declare const GRPC_WIRE_GRPC = 1;
15
+ /** gRPC-Web (`application/grpc-web`, `application/grpc-web-text`…). */
16
+ export declare const GRPC_WIRE_WEB = 2;
17
+ /** Connect unaire (`connect-protocol-version`, ou GET `?connect=v1`). */
18
+ export declare const GRPC_WIRE_CONNECT = 3;
19
+ /** Connect en flux (`application/connect+proto`, `application/connect+json`). */
20
+ export declare const GRPC_WIRE_CONNECT_STREAM = 4;
21
+ /** Le protocole gRPC d'une requête, `GRPC_WIRE_NONE` s'il n'y en a aucun. */
22
+ export type GrpcWire = typeof GRPC_WIRE_NONE | typeof GRPC_WIRE_GRPC | typeof GRPC_WIRE_WEB | typeof GRPC_WIRE_CONNECT | typeof GRPC_WIRE_CONNECT_STREAM;
23
+ /**
24
+ * Reconnaît le protocole gRPC d'une requête à ses en-têtes : `content-type`
25
+ * d'abord, puis `connect-protocol-version`. Une API JSON ordinaire rend
26
+ * `GRPC_WIRE_NONE`.
27
+ *
28
+ * @param headers - les en-têtes de la requête.
29
+ * @param connectGet - `true` pour un GET qui porte `connect=v1` (Connect
30
+ * unaire en GET, sans en-tête propre).
31
+ * @returns le protocole reconnu.
32
+ */
33
+ export declare function grpcWireOf(headers: http.IncomingHttpHeaders, connectGet?: boolean): GrpcWire;
34
+ /** 16 `UNAUTHENTICATED`. */
35
+ export declare const GRPC_UNAUTHENTICATED = 16;
36
+ /** 7 `PERMISSION_DENIED`. */
37
+ export declare const GRPC_PERMISSION_DENIED = 7;
38
+ /**
39
+ * Le nom d'un code gRPC (`unimplemented`, `unauthenticated`…) — le libellé
40
+ * générique d'un refus (ADR-0026 D5 point 4).
41
+ *
42
+ * @param code - le code gRPC, de 0 à 16.
43
+ * @returns son nom ; `unknown` hors de la plage.
44
+ */
45
+ export declare function grpcCodeName(code: number): string;
46
+ /**
47
+ * Le nom d'un code gRPC tel que le publient la ligne d'accès et le canal de
48
+ * traçage (`UNAVAILABLE`) — ADR-0026 D10 point 1.
49
+ *
50
+ * @param code - le code gRPC, de 0 à 16.
51
+ * @returns son nom ; `UNKNOWN` hors de la plage.
52
+ */
53
+ export declare function grpcStatusName(code: number): string;
54
+ /**
55
+ * Le code gRPC d'un refus de transport — rendu avant l'implémentation (chemin
56
+ * inconnu, méthode refusée, limite de débit, pare-feu) — en gRPC natif et en
57
+ * gRPC-Web (ADR-0026 D13 point 2). Distincte de la table des erreurs de
58
+ * l'application (D7) ; seule la règle d'identité de 401/403 est commune (D5).
59
+ *
60
+ * @param status - le statut HTTP que le noyau aurait rendu.
61
+ * @param identified - un appelant authentifié est connu.
62
+ * @returns le code gRPC.
63
+ */
64
+ export declare function transportRefusalCode(status: number, identified: boolean): number;
65
+ /**
66
+ * Le code gRPC d'une erreur levée par l'APPLICATION pendant un appel — une
67
+ * `HttpError` d'un service partagé avec des routes HTTP, une validation, un
68
+ * conflit (ADR-0026 D7, table « HTTP Mapping » de `google/rpc/code.proto`).
69
+ * Distincte de la table des refus de transport ({@link transportRefusalCode}) :
70
+ * un 404 levé par un service dit « entité introuvable » (5), un chemin inconnu
71
+ * dit « méthode absente » (12). Seule la règle d'identité de 401/403 est
72
+ * commune (D5).
73
+ *
74
+ * `code.proto` associe 409 à `ABORTED` et à `ALREADY_EXISTS` : `ABORTED`, le
75
+ * sens le plus large, est retenu ; `ALREADY_EXISTS` se lève en `ConnectError`.
76
+ *
77
+ * @param status - le statut HTTP que rendrait une route HTTP.
78
+ * @param identified - un appelant authentifié est connu.
79
+ * @returns le code gRPC.
80
+ */
81
+ export declare function applicationErrorCode(status: number, identified: boolean): number;
82
+ /**
83
+ * Le code d'un refus en protocole Connect : celui qu'un client DÉDUIT du
84
+ * statut HTTP (`connect-protocol.md`, « HTTP to Error Code »), puisque le
85
+ * statut voyage avec la réponse et doit s'accorder au code. 405 et 421 y
86
+ * valent donc `unknown` (ADR-0026 D13, précision du lot 3) ; 401/403 suivent
87
+ * la règle d'identité (D5).
88
+ *
89
+ * @param status - le statut HTTP du refus.
90
+ * @param identified - un appelant authentifié est connu.
91
+ * @returns le code gRPC.
92
+ */
93
+ export declare function connectRefusalCode(status: number, identified: boolean): number;
94
+ /** Ce que le refus touche de la réponse : son statut et ses en-têtes. */
95
+ export interface IGrpcRefusalTarget {
96
+ setStatusCode(status: number, message?: string): void;
97
+ setHeader(name: string, value: string): void;
98
+ }
99
+ /**
100
+ * Pose le statut et les en-têtes d'un refus au format du protocole, et rend
101
+ * le corps à envoyer — vide en gRPC et gRPC-Web, dont le statut voyage dans
102
+ * les en-têtes (_Trailers-Only_ ; en HTTP/2, `Http2Response.writeHead` termine
103
+ * alors le flux sur la trame d'en-têtes).
104
+ *
105
+ * | Protocole | Statut HTTP | Corps |
106
+ * | --------------- | ------------------------------ | -------------------------------------- |
107
+ * | gRPC, gRPC-Web | 200 | aucun |
108
+ * | Connect unaire | celui du refus (401/403 : D5) | `{"code":"<nom>"}` |
109
+ * | Connect en flux | 200 | message de fin `{"error":{…}}` |
110
+ *
111
+ * Le message n'est jamais celui de l'erreur, qui nomme parfois la zone
112
+ * refusée : seul le nom du code part (D5 point 4).
113
+ *
114
+ * @param response - la réponse du contexte, pas encore envoyée.
115
+ * @param wire - le protocole de la requête, autre que `GRPC_WIRE_NONE`.
116
+ * @param status - le statut HTTP que le noyau aurait rendu.
117
+ * @param identified - un appelant authentifié est connu.
118
+ * @param contentType - le `content-type` de la requête, recopié en gRPC,
119
+ * gRPC-Web et Connect en flux (déjà reconnu par {@link grpcWireOf}).
120
+ * @returns le corps : texte vide, JSON, ou octets d'un message de fin.
121
+ */
122
+ export declare function prepareGrpcRefusal(response: IGrpcRefusalTarget, wire: GrpcWire, status: number, identified: boolean, contentType: string | undefined): string | Uint8Array;
123
+ /**
124
+ * Marque une route comme servie par un point d'entrée gRPC — au démarrage,
125
+ * par `@nodefony/framework/grpc`. Allume la garde qui refuse en 12 une requête
126
+ * gRPC native aboutie à une route qui n'est pas gRPC (ADR-0026 D13 point 3) :
127
+ * tant qu'aucune route n'est marquée, la garde coûte une lecture de booléen.
128
+ *
129
+ * @param route - la route montée pour une méthode du contrat.
130
+ * @experimental Surface gRPC, ADR-0026 D11.
131
+ */
132
+ export declare function markGrpcRoute(route: {
133
+ grpc?: boolean | undefined;
134
+ }): void;
135
+ /** `true` dès qu'une route gRPC est déclarée dans le processus. */
136
+ export declare function hasGrpcRoutes(): boolean;
@@ -38,6 +38,13 @@ export interface IRelaySettings {
38
38
  */
39
39
  agent: http.Agent | false | undefined;
40
40
  }
41
+ /**
42
+ * En-têtes de CONNEXION (RFC 9110 §7.6.1) : ils décrivent un saut, jamais le
43
+ * message — un intermédiaire ne les transmet pas. Les cinq premiers sont en
44
+ * outre INTERDITS en HTTP/2 (RFC 9113 §8.2.2) : Node lève
45
+ * `ERR_HTTP2_INVALID_CONNECTION_HEADERS` si une réponse les porte.
46
+ */
47
+ export declare const HOP_BY_HOP: ReadonlySet<string>;
41
48
  /**
42
49
  * Sauts `Via` au-delà desquels un message est tenu pour une boucle. Notre
43
50
  * pseudonyme ne repère une boucle qu'au RETOUR dans ce processus : derrière un
@@ -44,8 +44,9 @@ export interface ProxyIntrospection {
44
44
  httpsPort: number;
45
45
  /**
46
46
  * Port h2c du backend (`servers.h2c`), ou `null` s'il n'est pas activé. Le
47
- * générateur haproxy y envoie le trafic en HTTP/2 (`proto h2`) ; nginx, qui
48
- * ne parle pas HTTP/2 à ses amonts, l'ignore.
47
+ * générateur haproxy y envoie tout le trafic hors WebSocket (`proto h2`) ;
48
+ * nginx, qui ne parle HTTP/2 à ses amonts que par `grpc_pass`, n'y envoie
49
+ * que gRPC. Règle d'usage : {@link upstreamH2cPort}.
49
50
  */
50
51
  h2cPort?: number | null;
51
52
  /** Dossiers statiques servis à la racine `/` (ordre = priorité). */
@@ -98,6 +99,18 @@ export interface ProxyIntrospection {
98
99
  }
99
100
  /** Valeurs par défaut d'un modèle d'introspection (complété par la commande). */
100
101
  export declare const defaultIntrospection: ProxyIntrospection;
102
+ /**
103
+ * Port h2c que le proxy doit joindre, ou `null` — règle UNIQUE des deux
104
+ * générateurs.
105
+ *
106
+ * Seulement vers un backend en clair : le re-chiffrement vise le port TLS, qui
107
+ * négocie déjà HTTP/2 par ALPN, et mélanger les deux liens ferait voyager une
108
+ * partie du trafic en clair alors que l'utilisateur a demandé le chiffrement.
109
+ *
110
+ * @param intro - modèle d'introspection Nodefony.
111
+ * @returns le port h2c à joindre, ou `null` s'il n'y a pas de lien h2c.
112
+ */
113
+ export declare function upstreamH2cPort(intro: ProxyIntrospection): number | null;
101
114
  /**
102
115
  * Génère une configuration nginx complète (reverse-proxy + offload statiques).
103
116
  *