@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.
- package/dist/_virtual/{_@oxc-project_runtime@0.157.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.157.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +3 -2
- package/dist/nodefony/config/config.js +25 -2
- package/dist/nodefony/grpc/GrpcCall.js +298 -0
- package/dist/nodefony/grpc/GrpcEndpoint.js +528 -0
- package/dist/nodefony/grpc/index.js +4 -0
- package/dist/nodefony/interfaces/IRpcCall.js +1 -0
- package/dist/nodefony/service/audit-logger.js +52 -6
- package/dist/nodefony/service/error-renderer.js +41 -9
- package/dist/nodefony/service/http-kernel.js +198 -19
- package/dist/nodefony/service/pretty-request-logger.js +5 -3
- package/dist/nodefony/service/request-logger.js +21 -6
- package/dist/nodefony/service/reverse-proxy.js +1 -1
- package/dist/nodefony/service/servers/clientError.js +26 -1
- package/dist/nodefony/service/servers/server-h2c.js +3 -3
- package/dist/nodefony/service/servers/server-http.js +3 -3
- package/dist/nodefony/service/servers/server-https.js +4 -4
- package/dist/nodefony/service/servers/server-websocket-secure.js +3 -3
- package/dist/nodefony/service/servers/server-websocket.js +3 -3
- package/dist/nodefony/service/servers/serverShutdown.js +14 -36
- package/dist/nodefony/service/sessions/sessions-service.js +6 -6
- package/dist/nodefony/service/upload/upload-service.js +2 -2
- package/dist/nodefony/src/context/Context.js +12 -1
- package/dist/nodefony/src/context/http/ByteStream.js +9 -4
- package/dist/nodefony/src/context/http/HttpContext.js +86 -8
- package/dist/nodefony/src/context/http/Request.js +96 -16
- package/dist/nodefony/src/context/http/Response.js +120 -17
- package/dist/nodefony/src/context/http/SseStream.js +4 -4
- package/dist/nodefony/src/context/http/StreamLifecycle.js +328 -19
- package/dist/nodefony/src/context/http/parser.js +29 -11
- package/dist/nodefony/src/context/http/webResponse.js +56 -0
- package/dist/nodefony/src/context/http2/Response.js +47 -8
- package/dist/nodefony/src/context/requestTrace.js +6 -2
- package/dist/nodefony/src/context/rpcOutcome.js +20 -0
- package/dist/nodefony/src/context/websocket/Response.js +11 -1
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +15 -4
- package/dist/nodefony/src/cookies/cookie.js +3 -1
- package/dist/nodefony/src/errors/httpError.js +58 -25
- package/dist/nodefony/src/grpc/cors.js +59 -0
- package/dist/nodefony/src/grpc/limits.js +16 -0
- package/dist/nodefony/src/grpc/refusal.js +271 -0
- package/dist/nodefony/src/profiler/aggregate.js +2 -2
- package/dist/nodefony/src/proxy/forward.js +1 -1
- package/dist/nodefony/src/proxy/generateProxyConfig.js +91 -15
- package/dist/nodefony/src/servers/http2Goaway.js +76 -0
- package/dist/nodefony/src/servers/http2Limits.js +41 -15
- package/dist/nodefony/src/servers/http2StreamGrace.js +100 -0
- package/dist/nodefony/src/servers/receiveDeadline.js +177 -0
- package/dist/types/index.d.ts +5 -2
- package/dist/types/nodefony/config/config.d.ts +10 -0
- package/dist/types/nodefony/grpc/GrpcCall.d.ts +142 -0
- package/dist/types/nodefony/grpc/GrpcEndpoint.d.ts +98 -0
- package/dist/types/nodefony/grpc/index.d.ts +15 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +9 -0
- package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +14 -0
- package/dist/types/nodefony/interfaces/IRequest.d.ts +2 -1
- package/dist/types/nodefony/interfaces/IRequestTrace.d.ts +10 -0
- package/dist/types/nodefony/interfaces/IRouting.d.ts +19 -0
- package/dist/types/nodefony/interfaces/IRpcCall.d.ts +83 -0
- package/dist/types/nodefony/interfaces/IUpload.d.ts +51 -4
- package/dist/types/nodefony/interfaces/index.d.ts +2 -1
- package/dist/types/nodefony/service/audit-logger.d.ts +33 -3
- package/dist/types/nodefony/service/error-renderer.d.ts +22 -1
- package/dist/types/nodefony/service/http-kernel.d.ts +21 -1
- package/dist/types/nodefony/service/request-logger.d.ts +10 -0
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +2 -2
- package/dist/types/nodefony/src/context/Context.d.ts +9 -1
- package/dist/types/nodefony/src/context/http/ByteStream.d.ts +1 -1
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +56 -2
- package/dist/types/nodefony/src/context/http/Request.d.ts +38 -8
- package/dist/types/nodefony/src/context/http/Response.d.ts +61 -7
- package/dist/types/nodefony/src/context/http/SseStream.d.ts +1 -1
- package/dist/types/nodefony/src/context/http/StreamLifecycle.d.ts +53 -0
- package/dist/types/nodefony/src/context/http/parser.d.ts +17 -3
- package/dist/types/nodefony/src/context/http/webResponse.d.ts +31 -0
- package/dist/types/nodefony/src/context/http2/Response.d.ts +35 -2
- package/dist/types/nodefony/src/context/requestTrace.d.ts +3 -1
- package/dist/types/nodefony/src/context/rpcOutcome.d.ts +11 -0
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +6 -1
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +8 -0
- package/dist/types/nodefony/src/errors/httpError.d.ts +29 -4
- package/dist/types/nodefony/src/grpc/cors.d.ts +32 -0
- package/dist/types/nodefony/src/grpc/limits.d.ts +14 -0
- package/dist/types/nodefony/src/grpc/refusal.d.ts +136 -0
- package/dist/types/nodefony/src/proxy/forward.d.ts +7 -0
- package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +15 -2
- package/dist/types/nodefony/src/servers/http2Goaway.d.ts +41 -0
- package/dist/types/nodefony/src/servers/http2Limits.d.ts +27 -14
- package/dist/types/nodefony/src/servers/http2StreamGrace.d.ts +34 -0
- package/dist/types/nodefony/src/servers/receiveDeadline.d.ts +77 -0
- package/docs/byte-stream.md +52 -17
- package/docs/diagnostics-channel.md +45 -1
- package/docs/servers.md +57 -44
- package/docs/upload.md +43 -17
- package/package.json +26 -6
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ServerHttp2Session } from "node:http2";
|
|
2
|
+
/** Plus long délai d'annonce (ms) : un pair qui ne répond pas au PING ne retient pas la fermeture. */
|
|
3
|
+
export declare const ANNOUNCE_CAP_MS = 1000;
|
|
4
|
+
/**
|
|
5
|
+
* Annonce à UNE session HTTP/2 qu'elle va fermer, SANS écarter un flux déjà en route.
|
|
6
|
+
*
|
|
7
|
+
* Un PING mesure l'aller-retour, puis GOAWAY à `2^31-1`, puis l'attente de cet
|
|
8
|
+
* aller-retour : après elle, tout flux ouvert par le client avant l'annonce est
|
|
9
|
+
* arrivé, et le GOAWAY définitif (`session.close()`, dernier flux traité) peut
|
|
10
|
+
* suivre. RFC 9113 §6.8 : « SHOULD send an initial GOAWAY frame with the last
|
|
11
|
+
* stream identifier set to 2^31-1 […] After allowing time for any in-flight
|
|
12
|
+
* stream creation (at least one round-trip time) ». Sans annonce, un flux croisé
|
|
13
|
+
* avec le GOAWAY est refusé alors qu'il aurait pu être servi.
|
|
14
|
+
*
|
|
15
|
+
* L'aller-retour se MESURE avant l'annonce : après un GOAWAY, un client Node ne
|
|
16
|
+
* répond plus aux PING (constaté sur Node 26.13).
|
|
17
|
+
*
|
|
18
|
+
* @param session - la session à annoncer, ni fermée ni détruite
|
|
19
|
+
* @param settle - appelé UNE fois : après l'attente, à la fermeture de la
|
|
20
|
+
* session, ou aussitôt si le PING ne part pas. Un pair muet ne l'appelle
|
|
21
|
+
* jamais — le délai maximal appartient à l'appelant.
|
|
22
|
+
*/
|
|
23
|
+
export declare function announceSessionClose(session: ServerHttp2Session, settle: () => void): void;
|
|
24
|
+
/**
|
|
25
|
+
* Ferme EN DOUCEUR une session HTTP/2 restée inactive au-delà du délai du serveur.
|
|
26
|
+
*
|
|
27
|
+
* RFC 9113 §9.1 : un serveur est « permitted to terminate idle connections »,
|
|
28
|
+
* et « SHOULD first send a GOAWAY ». La fermeture suit l'annonce en deux temps
|
|
29
|
+
* ({@link announceSessionClose}), puis `session.close()` : aucun nouveau flux,
|
|
30
|
+
* mais les flux en cours vont à leur terme — un flux long et muet (abonnement
|
|
31
|
+
* gRPC) n'est pas coupé, seuls les NOUVEAUX flux repartent sur une autre
|
|
32
|
+
* connexion, ce que tout client HTTP/2 fait sur GOAWAY.
|
|
33
|
+
*
|
|
34
|
+
* Le délai de la session est désarmé d'abord : Node (26.13) marque une session
|
|
35
|
+
* détruite AVANT de désarmer son minuteur, qui la retiendrait alors jusqu'à son
|
|
36
|
+
* échéance (nodejs/node#66885).
|
|
37
|
+
*
|
|
38
|
+
* @param session - la session inactive
|
|
39
|
+
* @param capMs - délai maximal d'attente de l'écho du PING (ms)
|
|
40
|
+
*/
|
|
41
|
+
export declare function closeIdleSession(session: ServerHttp2Session, capMs?: number): void;
|
|
@@ -14,24 +14,37 @@ import type { IHttpConfig } from "../../config/config.js";
|
|
|
14
14
|
*/
|
|
15
15
|
export declare function http2ServerLimits(limits: Partial<IHttpConfig["http2"]> | undefined): Pick<ServerOptions, "maxSessionMemory" | "settings">;
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
* et contourne un défaut de Node qui garde
|
|
19
|
-
* l'échéance de ce délai.
|
|
17
|
+
* Ferme les sessions d'un serveur HTTP/2 — TLS (5152) comme h2c — restées
|
|
18
|
+
* inactives au-delà de `timeoutMs`, et contourne un défaut de Node qui garde
|
|
19
|
+
* chaque session fermée en vie jusqu'à l'échéance de ce délai.
|
|
20
20
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
21
|
+
* **La fermeture est À NOUS.** Node arme chaque session par
|
|
22
|
+
* `session.setTimeout(server.timeout, sessionOnTimeout)` — un écouteur `once` —
|
|
23
|
+
* et `sessionOnTimeout` ne détruit la session que si le serveur n'a AUCUN
|
|
24
|
+
* écouteur `timeout` ; s'il en a un, il lui passe la main, UNE fois par session
|
|
25
|
+
* (`internal/http2/core`, Node 26.13). Le serveur 5152 en a toujours un
|
|
26
|
+
* (`routeSocketTimeout`, pour ses sockets HTTP/1.1), et ce délai doit prévenir
|
|
27
|
+
* l'application (`onTimeout`) : sans fermeture ici, une session inactive vivait
|
|
28
|
+
* jusqu'à ce que le CLIENT la ferme (#763). À l'échéance, la session est fermée
|
|
29
|
+
* en douceur — GOAWAY annoncé puis définitif, les flux en cours vont à leur
|
|
30
|
+
* terme ({@link closeIdleSession}, RFC 9113 §9.1 et §6.8).
|
|
26
31
|
*
|
|
27
|
-
* Le contournement
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
+
* **Le contournement** (Node 24, 26) : `closeSession` marque la session détruite
|
|
33
|
+
* PUIS appelle `session.setTimeout(0)`, qui sort aussitôt sur un objet détruit ;
|
|
34
|
+
* le minuteur retient alors la session (~2,4 Ko) pendant `timeoutMs`. Il est
|
|
35
|
+
* désarmé à la réception du GOAWAY, AVANT la destruction — c'est ainsi que
|
|
36
|
+
* ferment un proxy et un client Node, même par `destroy()` ; et
|
|
37
|
+
* `closeIdleSession` le désarme avant de fermer. Signalé à Node :
|
|
38
|
+
* https://github.com/nodejs/node/issues/66885 (corrigé par nodejs/node#66886,
|
|
39
|
+
* absent de la 26.13). Une coupure TCP sans GOAWAY garde la rétention, bornée
|
|
40
|
+
* au délai.
|
|
41
|
+
*
|
|
42
|
+
* Coût par requête : nul — un écouteur par serveur, et la fermeture n'agit qu'à
|
|
43
|
+
* l'échéance.
|
|
32
44
|
*
|
|
33
45
|
* @param server - le serveur HTTP/2.
|
|
34
46
|
* @param timeoutMs - délai d'inactivité ; 0 ou absent le laisse désactivé.
|
|
35
|
-
* @param onTimeout - appelé à l'échéance (événement `timeout` du
|
|
47
|
+
* @param onTimeout - appelé à l'échéance d'une session (événement `timeout` du
|
|
48
|
+
* serveur), après le début de sa fermeture.
|
|
36
49
|
*/
|
|
37
50
|
export declare function setHttp2SessionTimeout(server: Http2Server | Http2SecureServer, timeoutMs: number | undefined, onTimeout: () => void): void;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type Http2Stream } from "node:http2";
|
|
2
|
+
/**
|
|
3
|
+
* Ferme en `NO_ERROR` un flux HTTP/2 dont la réponse est partie mais dont le
|
|
4
|
+
* client émet encore, après la grâce d'un aller-retour (`PING`, RFC 9113 §6.7).
|
|
5
|
+
*
|
|
6
|
+
* RFC 9113 §8.1 : après une réponse complète, le serveur PEUT demander au
|
|
7
|
+
* client d'arrêter d'émettre par un `RST_STREAM` `NO_ERROR`. La coupe ne doit
|
|
8
|
+
* jamais précéder l'envoi de la réponse : un `RST_STREAM` soumis pendant que
|
|
9
|
+
* les en-têtes attendent encore dans la file de nghttp2 les fait JETER, et le
|
|
10
|
+
* client reçoit une fin de flux sans statut (un client gRPC la lit INTERNAL).
|
|
11
|
+
* Le `PING` part derrière la réponse ; son écho prouve qu'elle est arrivée.
|
|
12
|
+
*
|
|
13
|
+
* **Un seul `PING` par session** couvre tous les flux qui attendent : un flux
|
|
14
|
+
* arrivé pendant qu'un `PING` est en vol attend le suivant. Le plafond de
|
|
15
|
+
* `PING` en vol de Node (10 par session) n'est donc jamais atteint par ce
|
|
16
|
+
* chemin, et aucun flux n'est coupé sans grâce — ce que faisait la coupe
|
|
17
|
+
* immédiate quand `ping()` rendait `false`, au-delà de 10 refus simultanés.
|
|
18
|
+
*
|
|
19
|
+
* Session en fermeture GRACIEUSE (délai d'inactivité, arrêt) : Node n'envoie
|
|
20
|
+
* plus de `PING`, et la session attend la fin de ses flux — un client muet la
|
|
21
|
+
* retiendrait. La coupe part alors au tour de boucle suivant, le temps que les
|
|
22
|
+
* réponses déjà soumises quittent la file de nghttp2.
|
|
23
|
+
*
|
|
24
|
+
* Après la grâce : client qui a fini d'émettre → rien (le flux se ferme seul,
|
|
25
|
+
* sans `RST_STREAM`) ; sinon → coupé. L'écho d'un `PING` passe avant les autres
|
|
26
|
+
* trames (§6.7) : une trame DATA encore en route peut arriver après lui, la
|
|
27
|
+
* coupe reste conforme (§8.1).
|
|
28
|
+
*
|
|
29
|
+
* Coût : chemin d'erreur seulement — une entrée de `WeakMap` par session tant
|
|
30
|
+
* que des flux attendent, un `PING` par aller-retour et par session.
|
|
31
|
+
*
|
|
32
|
+
* @param stream - flux dont la réponse est ÉCRITE (`finish` passé).
|
|
33
|
+
*/
|
|
34
|
+
export declare function closeStreamAfterGrace(stream: Http2Stream): void;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Une échéance ABSOLUE pour des réceptions en cours, sans un minuteur par
|
|
3
|
+
* réception : un seul balayage périodique pour toutes.
|
|
4
|
+
*
|
|
5
|
+
* ## Pourquoi
|
|
6
|
+
*
|
|
7
|
+
* `node:http2` ne borne pas la réception d'une requête : il n'a que des délais
|
|
8
|
+
* d'INACTIVITÉ (`http2stream.setTimeout`, `http2session.setTimeout`), qu'un
|
|
9
|
+
* client tient en vie avec un octet avant chaque échéance. `requestTimeout`,
|
|
10
|
+
* la borne absolue anti « slow-loris », n'existe que pour HTTP/1.1.
|
|
11
|
+
*
|
|
12
|
+
* ## Le coût, et pourquoi cette forme
|
|
13
|
+
*
|
|
14
|
+
* C'est la forme que Node donne lui-même à `requestTimeout` : aucun minuteur
|
|
15
|
+
* par requête, un contrôle périodique (`connectionsCheckingInterval`). Les
|
|
16
|
+
* réceptions suivies vivent dans un ANNEAU — un tableau des éléments et un
|
|
17
|
+
* `Int32Array` des tours d'entrée, indexés par un numéro de séquence :
|
|
18
|
+
* - suivre = deux écritures ; libérer = une comparaison et une écriture. Ni
|
|
19
|
+
* minuteur, ni fermeture, ni lecture d'horloge (le temps se compte en TOURS),
|
|
20
|
+
* ni champ ajouté à l'élément suivi ;
|
|
21
|
+
* - mesuré contre une `Map` (hachage d'identité de la clé) : ~26–32 ns par
|
|
22
|
+
* réception contre ~90–150 ns, de 1 à 10 000 réceptions simultanées.
|
|
23
|
+
*
|
|
24
|
+
* Toutes les entrées ont le même délai : l'ordre d'insertion EST l'ordre
|
|
25
|
+
* d'échéance. Un tour avance la tête et s'arrête à la première entrée vivante
|
|
26
|
+
* non échue — il ne coûte que ce qu'il expire ou saute.
|
|
27
|
+
*
|
|
28
|
+
* Contrepartie, bornée : une case libérée DERRIÈRE une entrée vivante attend
|
|
29
|
+
* que la tête la dépasse, soit au plus un délai de réception — 12 octets par
|
|
30
|
+
* réception commencée dans cette fenêtre. Sous charge, l'anneau garde sa
|
|
31
|
+
* capacité d'un tour à l'autre (jamais rétréci puis recrû) ; il la rend au
|
|
32
|
+
* premier tour sans réception nouvelle. Le minuteur unique est `unref`, et s'arrête au
|
|
33
|
+
* même moment.
|
|
34
|
+
*
|
|
35
|
+
* ## Précision
|
|
36
|
+
*
|
|
37
|
+
* Une entrée expire entre `timeoutMs` et `timeoutMs + periodMs` après son
|
|
38
|
+
* suivi — jamais AVANT le délai configuré.
|
|
39
|
+
*
|
|
40
|
+
* @typeParam T - l'élément suivi (un contexte de requête)
|
|
41
|
+
*/
|
|
42
|
+
export declare class ReceiveDeadline<T extends object> {
|
|
43
|
+
#private;
|
|
44
|
+
/**
|
|
45
|
+
* @param timeoutMs - le délai de réception (> 0)
|
|
46
|
+
* @param onExpire - appelé une fois par entrée échue, déjà retirée
|
|
47
|
+
* @param periodMs - la période du balayage, bornée par le délai (défaut 1 s)
|
|
48
|
+
* @throws RangeError si `timeoutMs` ou `periodMs` n'est pas un entier > 0
|
|
49
|
+
*/
|
|
50
|
+
constructor(timeoutMs: number, onExpire: (item: T) => void, periodMs?: number);
|
|
51
|
+
/** Cases occupées, libérées ou non, entre la tête et la queue. */
|
|
52
|
+
get size(): number;
|
|
53
|
+
/** Capacité courante de l'anneau. */
|
|
54
|
+
get capacity(): number;
|
|
55
|
+
/** Vrai tant que le balayage tourne. */
|
|
56
|
+
get running(): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Suit une réception qui commence ; démarre le balayage s'il dormait.
|
|
59
|
+
*
|
|
60
|
+
* @param item - la réception
|
|
61
|
+
* @returns le numéro à rendre à {@link ReceiveDeadline.release}
|
|
62
|
+
*/
|
|
63
|
+
track(item: T): number;
|
|
64
|
+
/**
|
|
65
|
+
* Cesse de suivre une réception terminée (reçue, refusée ou échue). Sans
|
|
66
|
+
* effet si la case a déjà été balayée ou réattribuée : l'identité de
|
|
67
|
+
* l'élément est vérifiée. Le balayage ne s'arrête pas ici mais au tour
|
|
68
|
+
* suivant s'il ne reste rien — jamais un minuteur armé puis désarmé par
|
|
69
|
+
* requête.
|
|
70
|
+
*
|
|
71
|
+
* @param seq - le numéro rendu par {@link ReceiveDeadline.track}
|
|
72
|
+
* @param item - la réception suivie
|
|
73
|
+
*/
|
|
74
|
+
release(seq: number, item: T): void;
|
|
75
|
+
/** Arrête le balayage et oublie toutes les réceptions (arrêt du serveur). */
|
|
76
|
+
close(): void;
|
|
77
|
+
}
|
package/docs/byte-stream.md
CHANGED
|
@@ -181,6 +181,27 @@ L'union **peut s'agrandir** : un `switch` exhaustif sur `reason` garde une branc
|
|
|
181
181
|
|
|
182
182
|
### Relayer un autre service
|
|
183
183
|
|
|
184
|
+
**Le plus court : renvoyer la réponse de `fetch` telle quelle.** Une action qui renvoie une
|
|
185
|
+
`Response` Web est envoyée en flux par `HttpContext.openWebResponse()`
|
|
186
|
+
(`HttpContext.ts#HttpContext.openWebResponse`) — même tuyauterie que `openStream`, statut et
|
|
187
|
+
en-têtes de l'amont compris :
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
@route("camera-live", { path: "/camera" })
|
|
191
|
+
camera() {
|
|
192
|
+
return fetch("http://camera.local/stream", { signal: this.context.signal });
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Ce que ce geste court fait pour toi (`webResponse.ts#webResponseParts`) : il recopie le statut et les
|
|
197
|
+
en-têtes de l'amont **sauf** `content-encoding` et `content-length` (le corps a déjà été décompressé
|
|
198
|
+
par `fetch`), `set-cookie` (les cookies d'un tiers ne se posent pas sous ton domaine) et les en-têtes
|
|
199
|
+
de connexion (RFC 9110 §7.6.1). Une `Response` que ton code CONSTRUIT (`new Response(…)`) garde, elle,
|
|
200
|
+
tous ses en-têtes, cookies compris — `response.type` les distingue (`"default"` contre `"basic"`).
|
|
201
|
+
|
|
202
|
+
**Le réglage fin : `renderStream`**, dès qu'il faut refuser un amont en erreur, choisir les en-têtes
|
|
203
|
+
recopiés ou poser des délais :
|
|
204
|
+
|
|
184
205
|
```ts
|
|
185
206
|
@route("camera-live", { path: "/camera" })
|
|
186
207
|
async camera() {
|
|
@@ -277,6 +298,17 @@ deux types de flux.
|
|
|
277
298
|
Aucun minuteur ni appel d'horloge par morceau : la coupure tombe entre `idleTimeout` et
|
|
278
299
|
1,25 × `idleTimeout`.
|
|
279
300
|
4. **HEAD** : les en-têtes du GET, aucun corps, la source n'est jamais lue (toute route GET sert HEAD).
|
|
301
|
+
Pour ne pas lancer un producteur coûteux (ffmpeg) pour rien, passer une source VIDE en mode
|
|
302
|
+
octets : `Readable.from([])` seul est en mode objet, donc refusé (500).
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
if (this.method === "HEAD") {
|
|
306
|
+
return this.renderStream(Readable.from([], { objectMode: false }), {
|
|
307
|
+
headers: { "content-type": "video/mp4" },
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
280
312
|
5. **Au premier morceau** : les en-têtes, puis le morceau. Sous contre-pression, la source est mise en
|
|
281
313
|
pause et le minuteur `stallTimeout` armé ; il est désarmé au `drain`.
|
|
282
314
|
6. **La fin passe par un seul nettoyage** (`ByteStream.ts#openByteStream`, puis
|
|
@@ -336,26 +368,29 @@ désoptimisation de V8 (`--trace-deopt`) ne cite pas `ByteStream`.
|
|
|
336
368
|
|
|
337
369
|
## ⚠️ Pièges (symptôme → cause → correction)
|
|
338
370
|
|
|
339
|
-
| Symptôme
|
|
340
|
-
|
|
|
341
|
-
| `TypeError : une source en mode objet`
|
|
342
|
-
| Un processus fils survit à un crash du processus Node
|
|
343
|
-
| Coupé au bout de 30 s alors que la source est lente
|
|
344
|
-
| Coupé derrière nginx au bout de 60 s
|
|
345
|
-
| Le flux répond 429 sans avoir rien fait
|
|
346
|
-
| `content-encoding` faux sur un relais
|
|
371
|
+
| Symptôme | Cause | Correction |
|
|
372
|
+
| -------------------------------------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
|
|
373
|
+
| `TypeError : une source en mode objet` | `Readable.from([...])` est en mode objet par défaut | `Readable.from(chunks, { objectMode: false })` |
|
|
374
|
+
| Un processus fils survit à un crash du processus Node | la destruction de la source n'a jamais eu lieu : le processus Node est mort avant | supervision de l'application (cgroup, `detached: false`, `kill` au signal) |
|
|
375
|
+
| Coupé au bout de 30 s alors que la source est lente | `idleTimeout` (défaut 30 s) : la source n'a rien produit pendant ce délai | relever `idleTimeout`, ou `0` pour une source légitimement silencieuse |
|
|
376
|
+
| Coupé derrière nginx au bout de 60 s | `proxy_read_timeout` (60 s) plus court que le silence de la source | garder `idleTimeout` SOUS ce délai, ou l'allonger sur le proxy |
|
|
377
|
+
| Le flux répond 429 sans avoir rien fait | `wsMaxConnectionsPerIp` atteint — sockets, flux SSE et flux d'octets comptent ensemble | relever le plafond, ou fermer les connexions laissées ouvertes |
|
|
378
|
+
| `content-encoding` faux sur un relais | en-têtes de l'amont recopiés, alors que `fetch` a décompressé | ne recopier que `content-type` — ou `return fetch(…)`, qui les écarte |
|
|
379
|
+
| Requête pendue jusqu'au 408 sur `return new Response(…)` | framework antérieur à la reconnaissance de la `Response` Web | mettre à jour, ou `return this.renderStream(response.body, …)` |
|
|
380
|
+
| Un cookie de l'amont n'arrive pas au navigateur | `return fetch(…)` écarte le `set-cookie` d'une réponse RELAYÉE | le reposer soi-même, en connaissance de cause (`Response.ts#HttpResponse.setCookie`) |
|
|
347
381
|
|
|
348
382
|
## 🧪 Tests & couverture
|
|
349
383
|
|
|
350
|
-
| Type
|
|
351
|
-
|
|
|
352
|
-
| Parité du cycle de vie
|
|
353
|
-
| Parité — unitaire
|
|
354
|
-
| Intégration
|
|
355
|
-
|
|
|
356
|
-
|
|
|
357
|
-
|
|
|
358
|
-
|
|
|
384
|
+
| Type | Où |
|
|
385
|
+
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
386
|
+
| Parité du cycle de vie | `http/streamLifecycle.test.ts` — la MÊME table jouée sur le SSE et le flux d'octets : réponse prise, départ du client en HTTP/1.1, HTTP/2 et h2c, fin propre, session, double ouverture, client lent, durée de vie |
|
|
387
|
+
| Parité — unitaire | `unit/streamLifecycleParity.test.ts` — client parti PENDANT la sauvegarde de session : flux né fermé, source détruite |
|
|
388
|
+
| Intégration | `http/byteStream.test.ts` — HEAD (3 transports), coupure après le premier octet (3 transports), 500 / 502 avant, source muette 504 et coupure, `idleTimeout: 0`, source déjà finie, en-tête refusé, producteur bloqué libéré, relais `fetch` ; anneau du journal relu (une seule `ERROR`, aucun « onError on closed context », statut de la ligne d'accès) ; session sauvée deux fois, écrite une fois |
|
|
389
|
+
| `Response` Web et flux nu renvoyés | `http/webResponse.test.ts` — `return new Response(…)` (statut, en-têtes, deux cookies, en-tête de connexion écarté), `Response.json`, réponse sans corps (204), `Response.error()` (502), `return fetch(…)` d'un amont gzip (encodage et cookies de l'amont écartés), `Readable` et `ReadableStream` nus, départ du client (source détruite / annulée) — chaque cas vu ROUGE (408) sans la reconnaissance |
|
|
390
|
+
| Délais réels | même fichier, derrière `NF_RUN_LONG_STREAM=1` (~80 s) — défaut d'`idleTimeout` (coupure entre 30 et 37,5 s, 504 au journal) et `idleTimeout: 0` tenu 40 s, au-delà du délai de socket du serveur |
|
|
391
|
+
| Gardes | `http/sse-guards.attack.test.ts` — plafond par IP (429, place rendue, source refusée détruite), anonyme refusé (401) et révocation, joués sur le SSE ET le flux d'octets |
|
|
392
|
+
| Refus — unitaire | `unit/byteStreamRefusal.test.ts` — un appel qui lève (500, 429, `RangeError`, `TypeError`) détruit la source, annule un flux web |
|
|
393
|
+
| Mémoire | `http/memory.test.ts` — flux finis par leur source et flux abandonnés par le client : pente du tas, scopes et contextes résiduels |
|
|
359
394
|
|
|
360
395
|
Le cas du **producteur bloqué** lance un enfant `node -e` qui écrit sans fin par `fs.writeSync(1, …)`
|
|
361
396
|
— une écriture bloquante, comme ffmpeg — et vérifie qu'il sort en moins de 5 s après le départ du
|
|
@@ -167,6 +167,7 @@ seule pour l'abonné.
|
|
|
167
167
|
| `statusCode` | fin | statut rendu : celui de l'amont pour une requête relayée, `101` pour un handshake accepté, `null` si le client est parti |
|
|
168
168
|
| `durationMs` | fin | construction du contexte → dernier octet remis (fin du handshake en WebSocket) ; `null` si `timing.enabled: false` |
|
|
169
169
|
| `error` | `error` | l'exception attrapée par le pipeline, `null` sinon |
|
|
170
|
+
| `rpc` | fin | appel RPC (gRPC, gRPC-Web, Connect) servi par la requête — `system`, `method`, `code`, `codeName` — ; `null` hors RPC |
|
|
170
171
|
|
|
171
172
|
Les champs « fin » valent `null` à `start` et `end` : c'est normal, la route et le statut ne sont
|
|
172
173
|
pas encore connus. On les lit sur `asyncEnd`.
|
|
@@ -188,6 +189,43 @@ Pour qui écrit un plugin OTel, la table des attributs du span serveur (conventi
|
|
|
188
189
|
Un **4xx sans exception** ne publie pas `error` : pour un span serveur, OTel demande de laisser le
|
|
189
190
|
statut d'un 4xx non renseigné. C'est à l'abonné de classer un statut ; `statusCode` est là pour ça.
|
|
190
191
|
|
|
192
|
+
### Un appel RPC — gRPC, gRPC-Web, Connect
|
|
193
|
+
|
|
194
|
+
Un appel gRPC répond le plus souvent en HTTP 200 : son résultat voyage dans le trailer
|
|
195
|
+
`grpc-status`. `statusCode` reste le statut HTTP, vrai sur le fil ; le résultat de l'appel est dans
|
|
196
|
+
`rpc`, une COPIE de quatre champs, fixée au `close` de la réponse :
|
|
197
|
+
|
|
198
|
+
| Champ | Contenu |
|
|
199
|
+
| ---------- | ------------------------------------------------------------------------------------------ |
|
|
200
|
+
| `system` | protocole parlé sur le fil : `grpc`, `grpc-web` ou `connect` |
|
|
201
|
+
| `method` | `paquet.Service/Méthode`, tiré de la route |
|
|
202
|
+
| `code` | code de l'appel (0 à 16) ; `null` si l'appel n'a jamais commencé (refusé par le transport) |
|
|
203
|
+
| `codeName` | son nom (`OK`, `UNAVAILABLE`…), `null` avec lui |
|
|
204
|
+
|
|
205
|
+
Le code est celui que lit le client, à une course près : quand l'échéance du client expire, sa
|
|
206
|
+
coupure et le minuteur du serveur se croisent, et le serveur publie 4 ou 1. Une réponse que le
|
|
207
|
+
serveur n'a pas terminée vaut 1 `CANCELLED` — le client est parti, comme le statut 499 —, sauf une
|
|
208
|
+
fin qu'il a lui-même posée (échéance 4, identité révoquée 16). Un refus de l'adaptateur avant le
|
|
209
|
+
gestionnaire (message trop grand, encodage inconnu) porte son code. Une exception devient `error` :
|
|
210
|
+
un appel en erreur publie UN `error`. Rien n'est publié par message d'un flux : un flux est un span,
|
|
211
|
+
de l'ouverture au `close`.
|
|
212
|
+
|
|
213
|
+
Correspondance avec les conventions RPC d'OpenTelemetry :
|
|
214
|
+
|
|
215
|
+
| Attribut OTel | Champ |
|
|
216
|
+
| -------------------------- | ----------------------------------------------------------------------------------------------- |
|
|
217
|
+
| nom du span | `rpc.method` |
|
|
218
|
+
| `rpc.method` | `rpc.method` |
|
|
219
|
+
| `rpc.system.name` | `grpc` (convention stable) ou `connectrpc` (convention en développement) — au choix de l'abonné |
|
|
220
|
+
| `rpc.status_code` | `rpc.codeName` |
|
|
221
|
+
| `error.type` | dérivé de `error`, sinon `rpc.codeName` pour un code d'erreur |
|
|
222
|
+
| `rpc.server.call.duration` | `durationMs / 1000` — même horloge que le span |
|
|
223
|
+
|
|
224
|
+
Un code est une erreur du serveur — statut du span en erreur — pour 2, 4, 12, 13, 14 et 15 ; les
|
|
225
|
+
autres codes non nuls sont des refus ou des échecs du client. La ligne d'accès suit la même règle
|
|
226
|
+
pour sa gravité, et ajoute `RPC : <méthode> <NOM>` au statut HTTP. `traceparent` voyage dans les
|
|
227
|
+
métadonnées gRPC, qui sont des en-têtes HTTP : la propagation W3C existante les lit.
|
|
228
|
+
|
|
191
229
|
## La séquence, sortie par sortie
|
|
192
230
|
|
|
193
231
|
| Situation | Événements publiés | `statusCode` |
|
|
@@ -240,6 +278,9 @@ pas — ou on se désabonne (`channel.unsubscribe(subscribers)`).
|
|
|
240
278
|
- **Conventions sémantiques HTTP d'OpenTelemetry** — `http.route` exige un gabarit de faible
|
|
241
279
|
cardinalité, d'où `routePath` ; la durée de `http.server.request.duration` doit égaler celle du
|
|
242
280
|
span serveur, d'où une seule horloge.
|
|
281
|
+
- **Conventions sémantiques RPC d'OpenTelemetry** (`docs/rpc/rpc-spans.md`, `grpc.md`,
|
|
282
|
+
`connect-rpc.md`, `rpc-metrics.md`) — nom du span = `rpc.method`, jamais une valeur inventée ;
|
|
283
|
+
codes d'erreur du serveur gRPC (« SHOULD be considered errors ») ; durée sur l'horloge du span.
|
|
243
284
|
|
|
244
285
|
## Sécurité
|
|
245
286
|
|
|
@@ -261,11 +302,14 @@ Mesuré au micro-banc (Node 26, médiane de 7 passages) :
|
|
|
261
302
|
| ----------------------------- | ------------------------------------------------------------------------------------- |
|
|
262
303
|
| Personne n'écoute (le défaut) | une lecture de `hasSubscribers` (~1,6 ns), un champ `null` sur le contexte (8 octets) |
|
|
263
304
|
| Un abonné | un objet de message, l'appel des abonnés, l'entrée dans les stores liés |
|
|
305
|
+
| Un appel RPC, avec un abonné | une copie de quatre champs (`rpc`), à la fin — rien par message d'un flux |
|
|
264
306
|
|
|
265
307
|
`diagnostics_channel.hasSubscribers("nom")` coûterait ~41 ns : la garde lit donc le
|
|
266
308
|
`TracingChannel` gardé en mémoire de module, jamais une recherche par nom. Le gate mémoire
|
|
267
309
|
(`npm run test:memory`) est vert **sans** abonné et **avec** un abonné armé : le message ne retient
|
|
268
|
-
ni contexte ni scope.
|
|
310
|
+
ni contexte ni scope. Un abonné peut le garder après la fin de la requête : son `error` ne
|
|
311
|
+
retient pas davantage la requête — une `HttpError` lit le contexte à sa construction et n'en garde
|
|
312
|
+
aucune référence.
|
|
269
313
|
|
|
270
314
|
## Pièges
|
|
271
315
|
|
package/docs/servers.md
CHANGED
|
@@ -428,7 +428,7 @@ souvent **moins** de requêtes par seconde qu'en HTTP/1.1. Le choix se fait sur
|
|
|
428
428
|
|
|
429
429
|
| Forme du trafic | h2c ? | Pourquoi |
|
|
430
430
|
| -------------------------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
431
|
-
| gRPC
|
|
431
|
+
| gRPC natif derrière le proxy | **requis** | gRPC n'existe qu'en HTTP/2 ; gRPC-Web et Connect passent aussi en HTTP/1.1 — voir [gRPC](../../framework/docs/grpc.md). |
|
|
432
432
|
| Beaucoup de flux longs (SSE) par pod | **oui** | En HTTP/1.1, chaque flux occupe une connexion du proxy ; en h2c, ils partagent quelques connexions. |
|
|
433
433
|
| En-têtes lourds et répétés (cookies, jetons) | probable | HPACK les compresse d'une requête à l'autre. |
|
|
434
434
|
| REST classique, petites réponses | **mesurer** | Souvent neutre, parfois perdant. |
|
|
@@ -473,9 +473,9 @@ automatiquement un éventuel décalage de port.
|
|
|
473
473
|
le battement de cœur (`server-websocket.ts#Websocket.createServer`).
|
|
474
474
|
- **Keep-alive** armé à la création (`startHeartbeat()`, `server-websocket.ts#Websocket.createServer`) et par connexion
|
|
475
475
|
(`trackPong()`, `http-kernel.ts#HttpKernel.handleWebsocket`, juste après l'acceptation du handshake).
|
|
476
|
-
- **Arrêt** : il s'inscrit
|
|
477
|
-
(`
|
|
478
|
-
|
|
476
|
+
- **Arrêt** : il s'inscrit à l'étape « congé » de l'arrêt du trafic
|
|
477
|
+
(`addShutdownStep("dismiss")`, `server-websocket.ts#Websocket.createServer`), attendue avant le
|
|
478
|
+
drain HTTP — voir la section Arrêt gracieux.
|
|
479
479
|
|
|
480
480
|
### `server-websocket-secure` — WebSocket sur TLS
|
|
481
481
|
|
|
@@ -522,16 +522,16 @@ Défauts matérialisés dans `defaultAppConfig` (`src/nodefony/src/config/defaul
|
|
|
522
522
|
Table dérivée de `httpServerSchema` (`config.ts#httpServerSchema`) ; la section `https` reprend les mêmes clés et en
|
|
523
523
|
ajoute une (`httpsServerSchema`, `config.ts#httpsServerSchema`).
|
|
524
524
|
|
|
525
|
-
| Option | Type | Défaut | Effet
|
|
526
|
-
| ---------------------------- | ----- | ------- |
|
|
527
|
-
| `maxHeadersCount` | int | `2000` | Nombre maximum d'en-têtes par requête — anti _header flooding_.
|
|
528
|
-
| `keepAliveTimeout` | ms | `5000` | Délai de réutilisation de la socket TCP entre deux requêtes.
|
|
529
|
-
| `timeout` | ms | `30000` | Timeout global de socket, aligné sur `responseTimeout`. `0` = désactivé.
|
|
530
|
-
| `requestTimeout` | ms | `30000` | Délai de réception de la requête complète — **anti slow-loris
|
|
531
|
-
| `responseTimeout` | ms | `30000` | Délai d'
|
|
532
|
-
| `shutdownTimeout` | ms | `5000` | Drain au shutdown avant destruction forcée. Garder < grâce orchestrateur.
|
|
533
|
-
| `headers` | objet | `null` | En-têtes ajoutés à toutes les réponses.
|
|
534
|
-
| `rejectUnauthorized` (https) | bool | `false` | Rejette les certificats invalides. `false` en dev (auto-signés), `true` en prod.
|
|
525
|
+
| Option | Type | Défaut | Effet |
|
|
526
|
+
| ---------------------------- | ----- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
527
|
+
| `maxHeadersCount` | int | `2000` | Nombre maximum d'en-têtes par requête — anti _header flooding_. |
|
|
528
|
+
| `keepAliveTimeout` | ms | `5000` | Délai de réutilisation de la socket TCP entre deux requêtes. |
|
|
529
|
+
| `timeout` | ms | `30000` | Timeout global de socket, aligné sur `responseTimeout`. En HTTP/2 (5152, h2c), une connexion inactive au-delà de ce délai est fermée par GOAWAY ; les flux en cours vont à leur terme. `0` = désactivé. |
|
|
530
|
+
| `requestTimeout` | ms | `30000` | Délai de réception de la requête complète — **anti slow-loris**, en HTTP/1.1 (Node) comme en HTTP/2 (framework : 408, puis flux fermé). `0` le désactive. |
|
|
531
|
+
| `responseTimeout` | ms | `30000` | Délai d'inactivité d'une action (couche pipeline) : au-delà, 503 et `ctx.signal` annulé. Réglable par route (`responseTimeout` d'une option de route). |
|
|
532
|
+
| `shutdownTimeout` | ms | `5000` | Drain au shutdown avant destruction forcée. Garder < grâce orchestrateur. |
|
|
533
|
+
| `headers` | objet | `null` | En-têtes ajoutés à toutes les réponses. |
|
|
534
|
+
| `rejectUnauthorized` (https) | bool | `false` | Rejette les certificats invalides. `false` en dev (auto-signés), `true` en prod. |
|
|
535
535
|
|
|
536
536
|
Ces deux sections sont **permissives** (`z.looseObject`) : toute option supplémentaire de
|
|
537
537
|
`http.Server` / `net.Server` / TLS (`insecureHTTPParser`, `ciphers`, `minVersion`…) est transmise telle
|
|
@@ -552,22 +552,23 @@ Depuis `http2Schema` (`config.ts#http2Schema`), appliqué seulement si défini
|
|
|
552
552
|
Depuis `websocketSchema` (`config.ts#websocketSchema`). Les deux sections partagent la forme et les défauts ; le WSS
|
|
553
553
|
lit `websocketSecure` (`config.ts#httpConfigSchema`).
|
|
554
554
|
|
|
555
|
-
| Option | Type | Défaut | Effet
|
|
556
|
-
| ----------------------------- | ------------------- | ------- |
|
|
557
|
-
| `keepaliveInterval` | ms | `20000` | Intervalle des pings — détecte les connexions zombies.
|
|
558
|
-
| `keepaliveGracePeriod` | ms | `10000` | Délai de grâce après un ping sans réponse avant fermeture.
|
|
559
|
-
| `closeTimeout` | ms | `5000` | Délai de fermeture propre avant destruction de la socket.
|
|
560
|
-
| `maxPayload` | octets | `1 MiB` | Taille max d'un message entrant → au-delà, **close 1009** (`config.ts#websocketSchema`).
|
|
561
|
-
| `allowedOrigins` | bool \| str \| list | `false` | Allowlist d'`Origin` au handshake — **anti-CSWSH** (`config.ts#websocketSchema`).
|
|
562
|
-
| `perMessageDeflate` | bool \| objet | `false` | Compression RFC 7692. Désactivée par défaut : coût CPU/RAM + risque de _zip bomb_.
|
|
563
|
-
| `skipUTF8Validation` | bool | `false` | Désactive la validation UTF-8 des frames texte (RFC 6455 §8.1). À laisser `false`.
|
|
564
|
-
| `autoPong` | bool | `true` | Répond automatiquement aux pings entrants (RFC 6455 §5.5.2-3). À laisser `true`.
|
|
565
|
-
| `
|
|
566
|
-
| `
|
|
567
|
-
| `
|
|
568
|
-
| `
|
|
569
|
-
| `
|
|
570
|
-
| `
|
|
555
|
+
| Option | Type | Défaut | Effet |
|
|
556
|
+
| ----------------------------- | ------------------- | ------- | ----------------------------------------------------------------------------------------------- |
|
|
557
|
+
| `keepaliveInterval` | ms | `20000` | Intervalle des pings — détecte les connexions zombies. |
|
|
558
|
+
| `keepaliveGracePeriod` | ms | `10000` | Délai de grâce après un ping sans réponse avant fermeture. |
|
|
559
|
+
| `closeTimeout` | ms | `5000` | Délai de fermeture propre avant destruction de la socket. |
|
|
560
|
+
| `maxPayload` | octets | `1 MiB` | Taille max d'un message entrant → au-delà, **close 1009** (`config.ts#websocketSchema`). |
|
|
561
|
+
| `allowedOrigins` | bool \| str \| list | `false` | Allowlist d'`Origin` au handshake — **anti-CSWSH** (`config.ts#websocketSchema`). |
|
|
562
|
+
| `perMessageDeflate` | bool \| objet | `false` | Compression RFC 7692. Désactivée par défaut : coût CPU/RAM + risque de _zip bomb_. |
|
|
563
|
+
| `skipUTF8Validation` | bool | `false` | Désactive la validation UTF-8 des frames texte (RFC 6455 §8.1). À laisser `false`. |
|
|
564
|
+
| `autoPong` | bool | `true` | Répond automatiquement aux pings entrants (RFC 6455 §5.5.2-3). À laisser `true`. |
|
|
565
|
+
| `maxPendingAutoPongs` | entier ≥ 0 | `60` | Pongs en attente au-delà desquels la connexion est coupée — anti-DoS mémoire. `0` = sans borne. |
|
|
566
|
+
| `allowSynchronousEvents` | bool | `true` | Émet plusieurs frames d'un même chunk réseau de façon synchrone (débit vs équité). |
|
|
567
|
+
| `maxBackpressure` | octets | `4 MiB` | Seuil du tampon d'envoi par connexion (client lent à recevoir) — anti-OOM. |
|
|
568
|
+
| `backpressurePolicy` | `drop` \| `close` | `drop` | Au-delà du seuil : sauter la frame, ou fermer le client (close 1013). |
|
|
569
|
+
| `backpressureCloseAfterDrops` | frames | `1000` | Solde de frames refusées (+1 refus, −1 envoi) qui ferme en 1013 ; `0` = jamais. |
|
|
570
|
+
| `maxFramesPerSecond` | trames / s | `0` | Trames REÇUES admises par connexion ; au-delà, écartées avant décodage. `0` = coupé. |
|
|
571
|
+
| `frameRateCloseAfterRejects` | trames | `1000` | Solde de trames écartées (+1 écartée, −1 admise) qui ferme en 1008 ; `0` = jamais. |
|
|
571
572
|
|
|
572
573
|
> [!WARNING]
|
|
573
574
|
> `keepalive*`, `closeTimeout`, `maxBackpressure`, `backpressurePolicy`,
|
|
@@ -855,7 +856,9 @@ serveurs (HTTP et HTTPS) servent ces chemins — un kubelet configuré en `schem
|
|
|
855
856
|
## Arrêt gracieux — la séquence du SIGTERM
|
|
856
857
|
|
|
857
858
|
Un arrêt brutal coupe les requêtes en vol : à chaque mise à jour progressive, des utilisateurs voient
|
|
858
|
-
une erreur. L'arrêt de Nodefony est une **séquence ordonnée**, et l'ordre est le cœur du sujet.
|
|
859
|
+
une erreur. L'arrêt de Nodefony est une **séquence ordonnée**, et l'ordre est le cœur du sujet. Il
|
|
860
|
+
est écrit dans **une seule fonction**, `http-kernel.ts#HttpKernel.drainTraffic`, l'écouteur `onDrain` du
|
|
861
|
+
module (ADR-0027) : les serveurs s'y inscrivent à leur création, ils n'écoutent pas le noyau.
|
|
859
862
|
|
|
860
863
|
```mermaid
|
|
861
864
|
sequenceDiagram
|
|
@@ -864,21 +867,22 @@ sequenceDiagram
|
|
|
864
867
|
participant W as server-websocket
|
|
865
868
|
participant H as server-http / https
|
|
866
869
|
O->>K: SIGTERM
|
|
867
|
-
K->>K:
|
|
870
|
+
K->>K: isTerminating → readyz 503 (+ shutdownDelay)
|
|
868
871
|
Note over K: le load balancer retire le pod
|
|
869
|
-
K->>W:
|
|
872
|
+
K->>W: onDrain — étape « congé »
|
|
870
873
|
W->>W: close 1001 « Going Away » à chaque client
|
|
871
|
-
Note over W: fenêtre ~300 ms
|
|
872
|
-
K->>H:
|
|
874
|
+
Note over W: fenêtre ~300 ms, fin des flux longs en même temps
|
|
875
|
+
K->>H: onDrain — étape « drain », serveurs ensemble
|
|
873
876
|
H->>H: drain — in-flight terminées, sockets idle fermées
|
|
874
877
|
Note over H: destruction forcée après shutdownTimeout
|
|
878
|
+
K->>K: onTerminate — sessions, base, Redis (dernier inscrit d'abord)
|
|
875
879
|
K->>O: exit 0
|
|
876
880
|
```
|
|
877
881
|
|
|
878
|
-
**Étape 1 — la disponibilité bascule en premier.**
|
|
879
|
-
|
|
880
|
-
load balancer cesse d'envoyer du trafic, et `shutdownDelay`
|
|
881
|
-
propager.
|
|
882
|
+
**Étape 1 — la disponibilité bascule en premier.** Le noyau pose `kernel.isTerminating` avant toute
|
|
883
|
+
phase d'arrêt (`Kernel.ts#Kernel.isTerminating`), y compris pour un arrêt reçu pendant le démarrage :
|
|
884
|
+
`readyz` répond `503`, le load balancer cesse d'envoyer du trafic, et `shutdownDelay` (attendu si le
|
|
885
|
+
pod a été annoncé disponible) laisse à cette information le temps de se propager.
|
|
882
886
|
|
|
883
887
|
**Étape 2 — les clients WebSocket sont prévenus.** `Websocket.terminate()` (`server-websocket.ts#Websocket.terminate`)
|
|
884
888
|
envoie un message applicatif puis une **frame Close 1001 « Going Away »** (`server-websocket.ts#Websocket.terminate`).
|
|
@@ -895,11 +899,16 @@ indiscernable d'une panne réseau. Avec 1001, le client sait qu'il doit simpleme
|
|
|
895
899
|
fermées.
|
|
896
900
|
|
|
897
901
|
Ce qui reste au-delà de `shutdownTimeout` (défaut `5000` ms, `serverShutdown.ts#DEFAULT_SHUTDOWN_TIMEOUT`)
|
|
898
|
-
est détruit.
|
|
902
|
+
est détruit. Les serveurs (HTTP, HTTPS, h2c) se drainent **ensemble** : ports distincts, aucune
|
|
903
|
+
dépendance — l'arrêt dure un `shutdownTimeout`, pas un par serveur.
|
|
904
|
+
|
|
905
|
+
**Étape 4 — les ressources sont libérées.** Une fois plus aucune requête en vol, `onTerminate` ferme
|
|
906
|
+
le stockage des sessions, les pools du proxy inverse, les connexions aux bases et à Redis — du
|
|
907
|
+
dernier inscrit au premier.
|
|
899
908
|
|
|
900
909
|
> [!IMPORTANT]
|
|
901
|
-
> L'ordre WS-avant-HTTP n'est pas cosmétique.
|
|
902
|
-
>
|
|
910
|
+
> L'ordre WS-avant-HTTP n'est pas cosmétique. `drainTraffic` attend la fin de l'étape « congé »
|
|
911
|
+
> (`server-websocket.ts#Websocket.createServer`) avant de lancer l'étape « drain »
|
|
903
912
|
> (`server-http.ts#ServerHttp.createServer`). Inversé, le drain détruirait les sockets déjà upgradées **sans** frame
|
|
904
913
|
> Close : retour du 1006 pour tous les clients temps réel.
|
|
905
914
|
|
|
@@ -918,8 +927,12 @@ transport.
|
|
|
918
927
|
Dès qu'un écouteur `clientError` est attaché, **Node cesse de fermer la socket automatiquement**. Un
|
|
919
928
|
écouteur naïf (« je journalise et je passe ») transforme donc une requête malformée en **fuite de
|
|
920
929
|
socket et de descripteur de fichier** — c'est-à-dire un vecteur de déni de service. Nodefony répond et
|
|
921
|
-
ferme explicitement (`
|
|
922
|
-
(RFC 6585 §5)
|
|
930
|
+
ferme explicitement (`clientError.ts#handleClientError`), avec la réponse que Node aurait donnée sans
|
|
931
|
+
écouteur : `431` si les en-têtes débordent (RFC 6585 §5) ou `414` si c'est la cible (RFC 9112 §3),
|
|
932
|
+
`413` si les extensions de chunk débordent, `408` si la réception dépasse `requestTimeout`
|
|
933
|
+
(RFC 9110 §15.5.9), `400` sinon. Rien n'est écrit si la socket est déjà morte — ni si une réponse en
|
|
934
|
+
vol a déjà envoyé ses en-têtes : une ligne de statut écrite après elle corromprait ce que lit le
|
|
935
|
+
client ; la socket est alors détruite, comme le fait Node.
|
|
923
936
|
|
|
924
937
|
### Connexions WebSocket zombies
|
|
925
938
|
|