@nodefony/http 10.0.0-beta.5 → 10.0.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +30 -26
- package/dist/nodefony/command/proxyGenerateCommand.js +2 -2
- package/dist/nodefony/config/config.js +29 -4
- 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/ProfilerAdminApi.js +5 -2
- package/dist/nodefony/service/audit-logger.js +52 -6
- package/dist/nodefony/service/error-renderer.js +47 -13
- package/dist/nodefony/service/http-kernel.js +306 -49
- 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 +8 -3
- package/dist/nodefony/service/servers/clientError.js +53 -4
- package/dist/nodefony/service/servers/server-h2c.js +6 -6
- package/dist/nodefony/service/servers/server-http.js +6 -4
- package/dist/nodefony/service/servers/server-https.js +14 -7
- package/dist/nodefony/service/servers/server-static.js +1 -6
- package/dist/nodefony/service/servers/server-websocket-secure.js +7 -11
- package/dist/nodefony/service/servers/server-websocket.js +7 -11
- package/dist/nodefony/service/servers/serverShutdown.js +43 -6
- package/dist/nodefony/service/sessions/sessions-service.js +7 -7
- package/dist/nodefony/service/upload/upload-service.js +2 -2
- package/dist/nodefony/src/assets/prebuiltUi.js +23 -5
- package/dist/nodefony/src/context/Context.js +12 -1
- package/dist/nodefony/src/context/http/ByteStream.js +364 -0
- package/dist/nodefony/src/context/http/HttpContext.js +139 -32
- package/dist/nodefony/src/context/http/Request.js +111 -22
- package/dist/nodefony/src/context/http/Response.js +459 -37
- package/dist/nodefony/src/context/http/SseStream.js +83 -148
- package/dist/nodefony/src/context/http/StreamLifecycle.js +582 -0
- package/dist/nodefony/src/context/http/parser.js +29 -11
- package/dist/nodefony/src/context/http/urlFastPath.js +80 -1
- package/dist/nodefony/src/context/http/webResponse.js +56 -0
- package/dist/nodefony/src/context/http2/Request.js +1 -1
- package/dist/nodefony/src/context/http2/Response.js +55 -12
- package/dist/nodefony/src/context/requestTrace.js +6 -2
- package/dist/nodefony/src/context/rpcOutcome.js +20 -0
- package/dist/nodefony/src/context/transport.js +20 -1
- package/dist/nodefony/src/context/websocket/Response.js +15 -3
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +86 -18
- package/dist/nodefony/src/context/websocket/websocketUrl.js +7 -0
- package/dist/nodefony/src/context/websocket/wsBackpressure.js +15 -15
- package/dist/nodefony/src/context/websocket/wsFrameRate.js +71 -0
- package/dist/nodefony/src/context/websocket/wsLogContent.js +55 -17
- package/dist/nodefony/src/cookies/cookie.js +57 -14
- 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/Profiler.js +14 -1
- package/dist/nodefony/src/profiler/aggregate.js +2 -2
- package/dist/nodefony/src/proxy/forward.js +109 -9
- package/dist/nodefony/src/proxy/generateProxyConfig.js +97 -17
- package/dist/nodefony/src/proxy/rules.js +29 -14
- 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/nodefony/src/servers/upgradeDispatch.js +62 -4
- package/dist/types/index.d.ts +11 -2
- package/dist/types/nodefony/config/config.d.ts +14 -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/ISecurity.d.ts +27 -2
- 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/ProfilerAdminApi.d.ts +2 -1
- package/dist/types/nodefony/service/audit-logger.d.ts +33 -3
- package/dist/types/nodefony/service/error-renderer.d.ts +27 -5
- package/dist/types/nodefony/service/http-kernel.d.ts +36 -3
- package/dist/types/nodefony/service/request-logger.d.ts +10 -0
- package/dist/types/nodefony/service/servers/clientError.d.ts +3 -2
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +7 -5
- package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +9 -4
- package/dist/types/nodefony/src/context/Context.d.ts +9 -1
- package/dist/types/nodefony/src/context/http/ByteStream.d.ts +109 -0
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +99 -3
- package/dist/types/nodefony/src/context/http/Request.d.ts +46 -9
- package/dist/types/nodefony/src/context/http/Response.d.ts +188 -8
- package/dist/types/nodefony/src/context/http/SseStream.d.ts +33 -8
- package/dist/types/nodefony/src/context/http/StreamLifecycle.d.ts +106 -0
- package/dist/types/nodefony/src/context/http/parser.d.ts +17 -3
- package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +40 -0
- package/dist/types/nodefony/src/context/http/webResponse.d.ts +31 -0
- package/dist/types/nodefony/src/context/http2/Response.d.ts +40 -5
- 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/transport.d.ts +17 -0
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +6 -1
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +48 -0
- package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +4 -0
- package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +12 -2
- package/dist/types/nodefony/src/context/websocket/wsFrameRate.d.ts +69 -0
- package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +16 -8
- package/dist/types/nodefony/src/cookies/cookie.d.ts +20 -2
- 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/profiler/Profiler.d.ts +11 -1
- package/dist/types/nodefony/src/proxy/forward.d.ts +37 -2
- package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +15 -2
- package/dist/types/nodefony/src/proxy/rules.d.ts +20 -0
- 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/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +41 -1
- package/docs/byte-stream.md +406 -0
- package/docs/cookies.md +13 -3
- package/docs/diagnostics-channel.md +45 -1
- package/docs/index.md +8 -0
- package/docs/reverse-proxy.md +15 -3
- package/docs/servers.md +67 -43
- package/docs/session.md +3 -3
- package/docs/sse.md +27 -20
- package/docs/upload.md +43 -17
- package/package.json +38 -18
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
import Http2Response from "../http2/Response.js";
|
|
2
|
+
import HttpError from "../../errors/httpError.js";
|
|
3
|
+
import { ANNOUNCE_CAP_MS } from "../../servers/http2Goaway.js";
|
|
4
|
+
import { REVOCATION_REVALIDATE_MS, RequestContext } from "nodefony";
|
|
5
|
+
import http from "node:http";
|
|
6
|
+
import { AsyncResource } from "node:async_hooks";
|
|
7
|
+
// nodefony/src/context/http/StreamLifecycle.ts
|
|
8
|
+
/**
|
|
9
|
+
* Cycle de vie d'une réponse en flux — la brique commune au flux d'événements
|
|
10
|
+
* (`SseStream`) et au flux d'octets, NON exportée.
|
|
11
|
+
*
|
|
12
|
+
* Elle porte ce qu'un flux doit à la requête qui l'ouvre, quel que soit son
|
|
13
|
+
* format :
|
|
14
|
+
* - la PRISE de la réponse (`sended`), avant toute attente : le noyau ne peut
|
|
15
|
+
* plus répondre par-dessus, et l'avertissement « réponse non envoyée » se tait ;
|
|
16
|
+
* - la session sauvée AVANT les en-têtes — ses cookies partent avec eux ;
|
|
17
|
+
* - la fermeture lue sur la RÉPONSE, écouteur posé de façon SYNCHRONE à
|
|
18
|
+
* l'ouverture : un client parti pendant la sauvegarde de la session est vu ;
|
|
19
|
+
* - le délai d'inactivité de la requête désarmé, remplacé par les gardes du
|
|
20
|
+
* flux : coupure d'un client qui ne lit plus (`stallTimeout`), durée de vie
|
|
21
|
+
* maximale (`maxDuration`) ;
|
|
22
|
+
* - le plafond de connexions longues par IP (`wsMaxConnectionsPerIp`, le budget
|
|
23
|
+
* des sockets WebSocket) et la fermeture à la révocation de l'identité ;
|
|
24
|
+
* - une fin unique, et des écouteurs de fin liés au contexte asynchrone.
|
|
25
|
+
*
|
|
26
|
+
* Le FORMAT (en-têtes, écriture, battement de cœur) appartient au flux qui la
|
|
27
|
+
* porte.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Plus long délai qu'un minuteur sait tenir (2^31-1 ms). Au-delà, Node le
|
|
31
|
+
* ramène à 1 ms : un délai qui le dépasse ferait battre ou couper le flux
|
|
32
|
+
* mille fois par seconde.
|
|
33
|
+
*/
|
|
34
|
+
const MAX_TIMER_MS = 2147483647;
|
|
35
|
+
/**
|
|
36
|
+
* Délai de blocage par défaut d'un flux, en ms : un client qui ne lit plus
|
|
37
|
+
* depuis ce délai est coupé. Valeur commune aux flux d'événements, d'octets et
|
|
38
|
+
* aux flux RPC.
|
|
39
|
+
*/
|
|
40
|
+
const STREAM_STALL_TIMEOUT_MS = 3e4;
|
|
41
|
+
/**
|
|
42
|
+
* Période du balayage de blocage ({@link StallSweep}) : la moitié du délai de
|
|
43
|
+
* blocage. Un client bloqué est coupé entre `stallTimeout` et
|
|
44
|
+
* `stallTimeout + période` après la première contre-pression vue — jamais
|
|
45
|
+
* avant le délai réglé.
|
|
46
|
+
*/
|
|
47
|
+
const STALL_SWEEP_PERIOD_MS = STREAM_STALL_TIMEOUT_MS / 2;
|
|
48
|
+
/**
|
|
49
|
+
* Attente maximale, à l'arrêt du serveur, des flux longs qu'on vient de
|
|
50
|
+
* terminer (`HttpKernel.shutdownStreams`), avant le drain des connexions.
|
|
51
|
+
*
|
|
52
|
+
* Un flux qui écoute son signal se ferme en quelques millisecondes : l'attente
|
|
53
|
+
* finit avec lui. Elle ne dure jusqu'à son terme que pour un gestionnaire qui
|
|
54
|
+
* n'écoute rien — et ce temps s'AJOUTE au drain (`shutdownTimeout`), d'où une
|
|
55
|
+
* borne courte, celle de l'annonce du GOAWAY.
|
|
56
|
+
*/
|
|
57
|
+
const SHUTDOWN_STREAMS_WAIT_MS = ANNOUNCE_CAP_MS;
|
|
58
|
+
/**
|
|
59
|
+
* Refuse une option de délai hors de ce qu'un minuteur sait tenir.
|
|
60
|
+
*
|
|
61
|
+
* @param label - préfixe du message (`SSE`, `Flux`).
|
|
62
|
+
* @param name - nom de l'option.
|
|
63
|
+
* @param value - valeur reçue ; `undefined` et `false` passent.
|
|
64
|
+
* @throws RangeError quand la valeur n'est ni `false` ni un entier entre 1 et 2^31-1.
|
|
65
|
+
*
|
|
66
|
+
* @internal
|
|
67
|
+
*/
|
|
68
|
+
function checkStreamTimer(label, name, value) {
|
|
69
|
+
if (value === void 0 || value === false) return;
|
|
70
|
+
if (!Number.isInteger(value) || value < 1 || value > 2147483647) throw new RangeError(`${label} : ${name} entier entre 1 et ${MAX_TIMER_MS} ms, ou false, attendu`);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Constate le transport de la réponse d'un contexte : le flux en HTTP/2, la
|
|
74
|
+
* réponse en HTTP/1.1. Seule implémentation de ce constat — un flux SSE,
|
|
75
|
+
* d'octets ou un appel gRPC le lisent ici.
|
|
76
|
+
*
|
|
77
|
+
* @param context - le contexte de la requête.
|
|
78
|
+
* @returns la cible d'écriture, ou `null` quand la réponse est absente ou que
|
|
79
|
+
* son transport n'est pas reconnu.
|
|
80
|
+
*
|
|
81
|
+
* @internal
|
|
82
|
+
*/
|
|
83
|
+
function resolveStreamTarget(context) {
|
|
84
|
+
const response = context.response;
|
|
85
|
+
const stream = response instanceof Http2Response ? response.stream : null;
|
|
86
|
+
if (stream !== null) return stream;
|
|
87
|
+
const nodeResponse = response.response;
|
|
88
|
+
return nodeResponse instanceof http.ServerResponse ? nodeResponse : null;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Désarme le délai de réponse que Nodefony arme sur toute requête
|
|
92
|
+
* (`HttpContext.setTimeout`) : celui du flux en HTTP/2, celui de la socket en
|
|
93
|
+
* HTTP/1.1. Seule implémentation de cette règle — un flux long l'appelle, et
|
|
94
|
+
* une réponse possédée par un tiers aussi, pour qu'à l'expiration Nodefony
|
|
95
|
+
* n'écrive pas un 503 par-dessus (un seul écrivain).
|
|
96
|
+
*
|
|
97
|
+
* En HTTP/1.1, la requête suivante de la même connexion réarme la socket
|
|
98
|
+
* (`HttpContext.setTimeout` compare `socket.timeout` à son délai).
|
|
99
|
+
*
|
|
100
|
+
* @param target - la cible d'écriture constatée par {@link resolveStreamTarget}.
|
|
101
|
+
*
|
|
102
|
+
* @internal
|
|
103
|
+
*/
|
|
104
|
+
function disarmResponseTimeout(target) {
|
|
105
|
+
if (target instanceof http.ServerResponse) target.socket?.setTimeout(0);
|
|
106
|
+
else target.setTimeout(0);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Le cycle de vie d'UN flux ouvert sur une réponse.
|
|
110
|
+
*
|
|
111
|
+
* S'obtient par {@link openStreamLifecycle} ; le constructeur n'est appelé
|
|
112
|
+
* directement que par les tests.
|
|
113
|
+
*
|
|
114
|
+
* @internal
|
|
115
|
+
*/
|
|
116
|
+
var StreamLifecycle = class StreamLifecycle {
|
|
117
|
+
context;
|
|
118
|
+
/** Où l'on écrit : la réponse HTTP/1.1, ou le flux HTTP/2. */
|
|
119
|
+
target;
|
|
120
|
+
/** La réponse dont on écoute la fermeture — en HTTP/2, l'objet de compatibilité. */
|
|
121
|
+
#response;
|
|
122
|
+
#stallTimeout;
|
|
123
|
+
#revalidate;
|
|
124
|
+
identifier;
|
|
125
|
+
#foreignWriter;
|
|
126
|
+
/**
|
|
127
|
+
* Le noyau des registres où le flux s'inscrit, pris À L'OUVERTURE : le
|
|
128
|
+
* nettoyage du contexte (`Context.clean`) remet `context.httpKernel` à
|
|
129
|
+
* `null`, et il passe à la fermeture AVANT celui du flux — relu là, le
|
|
130
|
+
* retrait ne se ferait pas, et le flux fermé resterait dans les registres
|
|
131
|
+
* (révocation, balayage, arrêt) avec son contexte.
|
|
132
|
+
*/
|
|
133
|
+
#kernel;
|
|
134
|
+
/** Terminé par le noyau à l'arrêt du serveur — et donc inscrit à son registre. */
|
|
135
|
+
#endsAtShutdown;
|
|
136
|
+
/**
|
|
137
|
+
* Fin d'attente d'un flux balayé : posée sur `drain` quand le balayage voit
|
|
138
|
+
* une contre-pression — créée à la première, `null` avant (lazy).
|
|
139
|
+
*/
|
|
140
|
+
#onSweptDrain = null;
|
|
141
|
+
/**
|
|
142
|
+
* Fenêtre laissée à un appel RPC révoqué pour se terminer à son format avant
|
|
143
|
+
* la destruction du transport — armée seulement à la révocation.
|
|
144
|
+
*/
|
|
145
|
+
#revokeGrace = null;
|
|
146
|
+
#closed = false;
|
|
147
|
+
#cause = null;
|
|
148
|
+
/**
|
|
149
|
+
* Minuteur de la coupure d'un client qui ne lit plus — créé à la PREMIÈRE
|
|
150
|
+
* contre-pression, puis réarmé au reste du délai : jamais un minuteur par
|
|
151
|
+
* épisode (un flux d'octets en connaît un par morceau sur une socket rapide).
|
|
152
|
+
*/
|
|
153
|
+
#stall = null;
|
|
154
|
+
/** Début de l'attente de `drain` en cours (`Date.now()`) ; `0` : aucune. */
|
|
155
|
+
#stallSince = 0;
|
|
156
|
+
/** Fin de vie du flux — armée seulement si `maxDuration` est posé. */
|
|
157
|
+
#lifetime = null;
|
|
158
|
+
/** Nettoyage du flux qui porte la brique — posé une fois, appelé en premier. */
|
|
159
|
+
#release = null;
|
|
160
|
+
/** Écouteurs de fermeture — `null` tant que personne n'en pose (lazy). */
|
|
161
|
+
#onClose = null;
|
|
162
|
+
#onResponseClose = () => this.finish();
|
|
163
|
+
/**
|
|
164
|
+
* Tour du minuteur de blocage. Le client ne lit plus depuis `stallTimeout` :
|
|
165
|
+
* on coupe la connexion, comme s'il était parti — la fermeture de la réponse
|
|
166
|
+
* termine le flux. Une attente plus récente (le client a lu entre-temps) est
|
|
167
|
+
* réarmée au reste de son délai ; plus d'attente du tout : le minuteur s'éteint.
|
|
168
|
+
*/
|
|
169
|
+
#onStall = () => {
|
|
170
|
+
this.#stall = null;
|
|
171
|
+
const limit = this.#stallTimeout;
|
|
172
|
+
if (this.#stallSince === 0 || this.#closed || limit === false) return;
|
|
173
|
+
const waited = Date.now() - this.#stallSince;
|
|
174
|
+
if (waited < limit) {
|
|
175
|
+
this.#stall = setTimeout(this.#onStall, limit - waited);
|
|
176
|
+
this.#stall.unref();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
this.#cutStalled();
|
|
180
|
+
};
|
|
181
|
+
constructor(context, target, response, settings) {
|
|
182
|
+
this.context = context;
|
|
183
|
+
this.target = target;
|
|
184
|
+
this.#response = response;
|
|
185
|
+
this.#stallTimeout = settings.stallTimeout;
|
|
186
|
+
this.#revalidate = settings.revalidate;
|
|
187
|
+
this.identifier = settings.identifier;
|
|
188
|
+
this.#foreignWriter = settings.foreignWriter;
|
|
189
|
+
this.#endsAtShutdown = settings.endsAtShutdown;
|
|
190
|
+
const kernel = context.httpKernel;
|
|
191
|
+
this.#kernel = kernel;
|
|
192
|
+
response.on("close", this.#onResponseClose);
|
|
193
|
+
if (settings.maxDuration !== false) {
|
|
194
|
+
this.#lifetime = setTimeout(() => void this.close(), settings.maxDuration);
|
|
195
|
+
this.#lifetime.unref();
|
|
196
|
+
}
|
|
197
|
+
if (this.#revalidate !== null) kernel?.watchStream(this);
|
|
198
|
+
if (this.#endsAtShutdown) kernel?.trackStream(this);
|
|
199
|
+
if (this.#foreignWriter && this.#stallTimeout !== false) kernel?.sweepStall(this);
|
|
200
|
+
}
|
|
201
|
+
/** Le flux est-il fermé — par le serveur, ou par le client parti ? */
|
|
202
|
+
get closed() {
|
|
203
|
+
return this.#closed;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Qui a fermé le flux : le serveur, la coupure d'un client qui ne lit plus,
|
|
207
|
+
* ou `null` — client parti, ou flux encore ouvert. Lu par le flux qui porte
|
|
208
|
+
* la brique, dans son nettoyage, pour nommer la raison de la fin.
|
|
209
|
+
*/
|
|
210
|
+
get cause() {
|
|
211
|
+
return this.#cause;
|
|
212
|
+
}
|
|
213
|
+
/** L'identité qui a ouvert le flux est-elle toujours valable ? */
|
|
214
|
+
revalidate() {
|
|
215
|
+
return this.#revalidate === null ? Promise.resolve(true) : this.#revalidate();
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Pose le nettoyage du flux qui porte la brique (minuteurs, attentes) : il
|
|
219
|
+
* passe AVANT les écouteurs de l'application. Un cycle déjà fermé l'appelle
|
|
220
|
+
* aussitôt.
|
|
221
|
+
*/
|
|
222
|
+
onRelease(release) {
|
|
223
|
+
if (this.#closed) {
|
|
224
|
+
release();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
this.#release = release;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Appelle `listener` à la fermeture du flux, quelle qu'en soit la cause.
|
|
231
|
+
*
|
|
232
|
+
* L'écouteur est lié au contexte asynchrone de la requête au moment de
|
|
233
|
+
* l'enregistrement : la fermeture par le client part de la SOCKET, hors de
|
|
234
|
+
* la bulle de la requête. Un flux déjà fermé appelle l'écouteur au tour
|
|
235
|
+
* suivant.
|
|
236
|
+
*/
|
|
237
|
+
onClose(listener) {
|
|
238
|
+
const bound = AsyncResource.bind(listener);
|
|
239
|
+
if (this.#closed) {
|
|
240
|
+
queueMicrotask(bound);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
this.#onClose ??= [];
|
|
244
|
+
this.#onClose.push(bound);
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Arme la coupure d'un client qui ne lit plus — à appeler quand une écriture
|
|
248
|
+
* attend `drain`. Sans effet si elle est déjà armée ou désactivée.
|
|
249
|
+
*/
|
|
250
|
+
armStall() {
|
|
251
|
+
if (this.#stallTimeout === false || this.#stallSince !== 0 || this.#closed) return;
|
|
252
|
+
this.#stallSince = Date.now();
|
|
253
|
+
if (this.#stall !== null) return;
|
|
254
|
+
this.#stall = setTimeout(this.#onStall, this.#stallTimeout);
|
|
255
|
+
this.#stall.unref();
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Désarme la coupure : le client a repris sa lecture. Le minuteur n'est pas
|
|
259
|
+
* détruit — il s'éteindra seul à son tour s'il n'y a plus d'attente, ou
|
|
260
|
+
* resservira à la suivante sans nouvelle allocation.
|
|
261
|
+
*/
|
|
262
|
+
clearStall() {
|
|
263
|
+
this.#stallSince = 0;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Un tour du balayage de blocage, pour un flux dont un tiers tient les
|
|
267
|
+
* écritures ({@link StallSweep}).
|
|
268
|
+
*
|
|
269
|
+
* Une contre-pression vue (`writableNeedDrain`) ouvre l'attente et pose un
|
|
270
|
+
* `drain` : seul ce progrès RÉEL la ferme, jamais un tour qui verrait le
|
|
271
|
+
* tampon vide — un client lent qui lit voit son tampon se remplir à nouveau
|
|
272
|
+
* aussitôt, et serait sinon pris pour bloqué. Une attente qui dure
|
|
273
|
+
* `stallTimeout` coupe le client.
|
|
274
|
+
*
|
|
275
|
+
* @param now - l'heure du tour (`Date.now()`), lue une fois pour tous les flux.
|
|
276
|
+
*/
|
|
277
|
+
sweepStall(now) {
|
|
278
|
+
const limit = this.#stallTimeout;
|
|
279
|
+
if (this.#closed || limit === false) return;
|
|
280
|
+
if (this.#stallSince !== 0) {
|
|
281
|
+
if (now - this.#stallSince >= limit) this.#cutStalled();
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (!this.target.writableNeedDrain) return;
|
|
285
|
+
this.#stallSince = now;
|
|
286
|
+
this.target.once("drain", this.#onSweptDrain ??= () => {
|
|
287
|
+
this.#stallSince = 0;
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
/** Coupe un client qui ne lit plus : la fermeture de la réponse termine le flux. */
|
|
291
|
+
#cutStalled() {
|
|
292
|
+
this.#cause = "stall";
|
|
293
|
+
this.target.destroy();
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Désarme le délai d'inactivité de la requête : un flux n'a pas de fin
|
|
297
|
+
* attendue, ses propres gardes le remplacent.
|
|
298
|
+
*/
|
|
299
|
+
disarmIdleTimeout() {
|
|
300
|
+
disarmResponseTimeout(this.target);
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Ferme le flux et termine la requête. Idempotent. Quand un tiers tient les
|
|
304
|
+
* écritures, le transport est détruit : terminer la réponse en ferait un
|
|
305
|
+
* second écrivain.
|
|
306
|
+
*/
|
|
307
|
+
close() {
|
|
308
|
+
if (this.#closed) return void 0;
|
|
309
|
+
this.#cause = "server";
|
|
310
|
+
this.finish();
|
|
311
|
+
if (this.#foreignWriter) {
|
|
312
|
+
this.target.destroy();
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const done = this.context.close();
|
|
316
|
+
return done instanceof Promise ? done.then(() => void 0) : void 0;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* L'identité qui a ouvert le flux est morte (révocation, droits changés) :
|
|
320
|
+
* le flux prend fin. Idempotent.
|
|
321
|
+
*
|
|
322
|
+
* Quand un adaptateur RPC tient les écritures, c'est LUI qui termine l'appel,
|
|
323
|
+
* au format de son protocole (`HttpContext.rpc.revoke`, gRPC : 16) : le
|
|
324
|
+
* client lit un statut, pas une coupure, et aucun second écrivain ne touche
|
|
325
|
+
* la réponse. Un gestionnaire qui n'entend rien — bloqué, sans message à
|
|
326
|
+
* produire — est détruit à la fenêtre de re-validation suivante. Sinon :
|
|
327
|
+
* {@link close}.
|
|
328
|
+
*/
|
|
329
|
+
revoke() {
|
|
330
|
+
if (this.#closed) return void 0;
|
|
331
|
+
const rpc = this.#foreignWriter ? this.context.rpc : null;
|
|
332
|
+
if (rpc === null) return this.close();
|
|
333
|
+
if (this.#revokeGrace !== null) return void 0;
|
|
334
|
+
this.#cause = "server";
|
|
335
|
+
rpc.revoke();
|
|
336
|
+
this.#revokeGrace = setTimeout(StreamLifecycle.#destroyRevoked, REVOCATION_REVALIDATE_MS, this);
|
|
337
|
+
this.#revokeGrace.unref();
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Le serveur s'arrête : le flux prend fin AVANT le drain des connexions
|
|
341
|
+
* (ADR-0026 D8). Idempotent.
|
|
342
|
+
*
|
|
343
|
+
* Quand un adaptateur RPC tient les écritures, c'est LUI qui termine l'appel,
|
|
344
|
+
* au format de son protocole (`HttpContext.rpc.shutdown`, gRPC : 14
|
|
345
|
+
* `UNAVAILABLE`) : le client le rejoue ailleurs. Un gestionnaire qui n'entend
|
|
346
|
+
* rien tombe avec sa connexion, détruite par le drain à `shutdownTimeout` —
|
|
347
|
+
* aucun minuteur de plus. Sinon : {@link close} — `EventSource` se
|
|
348
|
+
* reconnecte, vers un autre exemplaire.
|
|
349
|
+
*/
|
|
350
|
+
shutdown() {
|
|
351
|
+
if (this.#closed) return void 0;
|
|
352
|
+
const rpc = this.#foreignWriter ? this.context.rpc : null;
|
|
353
|
+
if (rpc === null) return this.close();
|
|
354
|
+
this.#cause = "server";
|
|
355
|
+
rpc.shutdown();
|
|
356
|
+
}
|
|
357
|
+
/** Fenêtre écoulée : l'appel révoqué n'a pas fini, le transport est détruit. */
|
|
358
|
+
static #destroyRevoked(lifecycle) {
|
|
359
|
+
lifecycle.#revokeGrace = null;
|
|
360
|
+
if (lifecycle.#closed) return;
|
|
361
|
+
lifecycle.finish();
|
|
362
|
+
lifecycle.target.destroy();
|
|
363
|
+
}
|
|
364
|
+
/** Fin du flux : minuteurs, écouteurs et registre libérés, une seule fois. */
|
|
365
|
+
finish() {
|
|
366
|
+
if (this.#closed) return;
|
|
367
|
+
this.#closed = true;
|
|
368
|
+
this.#response.removeListener("close", this.#onResponseClose);
|
|
369
|
+
if (this.#lifetime !== null) {
|
|
370
|
+
clearTimeout(this.#lifetime);
|
|
371
|
+
this.#lifetime = null;
|
|
372
|
+
}
|
|
373
|
+
if (this.#revokeGrace !== null) {
|
|
374
|
+
clearTimeout(this.#revokeGrace);
|
|
375
|
+
this.#revokeGrace = null;
|
|
376
|
+
}
|
|
377
|
+
this.#stallSince = 0;
|
|
378
|
+
if (this.#stall !== null) {
|
|
379
|
+
clearTimeout(this.#stall);
|
|
380
|
+
this.#stall = null;
|
|
381
|
+
}
|
|
382
|
+
const kernel = this.#kernel;
|
|
383
|
+
if (this.#revalidate !== null) kernel?.unwatchStream(this);
|
|
384
|
+
if (this.#endsAtShutdown) kernel?.untrackStream(this);
|
|
385
|
+
if (this.#foreignWriter) {
|
|
386
|
+
kernel?.unsweepStall(this);
|
|
387
|
+
if (this.#onSweptDrain !== null) this.target.removeListener("drain", this.#onSweptDrain);
|
|
388
|
+
}
|
|
389
|
+
const release = this.#release;
|
|
390
|
+
this.#release = null;
|
|
391
|
+
release?.();
|
|
392
|
+
const listeners = this.#onClose;
|
|
393
|
+
this.#onClose = null;
|
|
394
|
+
if (listeners !== null) for (const listener of listeners) listener();
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
/**
|
|
398
|
+
* Le balayage de blocage des flux dont un tiers tient les écritures : UN
|
|
399
|
+
* minuteur pour tous, armé au premier flux inscrit, arrêté au dernier retiré.
|
|
400
|
+
*
|
|
401
|
+
* Pourquoi un balayage : l'adaptateur écrit et attend `drain` lui-même, la
|
|
402
|
+
* brique ne voit aucune écriture. Le délai d'inactivité que Node tient sur un
|
|
403
|
+
* flux HTTP/2 ne suffit pas : il est repoussé tant que la SESSION avance
|
|
404
|
+
* (`internal/http2/core#callTimeout`), si bien qu'un flux bloqué ne serait
|
|
405
|
+
* jamais coupé tant qu'un voisin de la même connexion est lu.
|
|
406
|
+
*
|
|
407
|
+
* Le seuil reste dans {@link StreamLifecycle.sweepStall} : une règle, une
|
|
408
|
+
* implémentation.
|
|
409
|
+
*
|
|
410
|
+
* Coût mesuré (300 flux HTTP/2 réels, médiane de 9 passes) : ~6 ns par flux et
|
|
411
|
+
* par tour ; ~60 ns pour inscrire puis retirer un flux ; 0 par message.
|
|
412
|
+
*
|
|
413
|
+
* @internal
|
|
414
|
+
*/
|
|
415
|
+
var StallSweep = class {
|
|
416
|
+
#entries = null;
|
|
417
|
+
#timer = null;
|
|
418
|
+
#periodMs;
|
|
419
|
+
#tick = () => {
|
|
420
|
+
this.sweep(Date.now());
|
|
421
|
+
};
|
|
422
|
+
/** @param periodMs - période du balayage (défaut {@link STALL_SWEEP_PERIOD_MS}). */
|
|
423
|
+
constructor(periodMs = STALL_SWEEP_PERIOD_MS) {
|
|
424
|
+
this.#periodMs = periodMs;
|
|
425
|
+
}
|
|
426
|
+
/** Nombre de flux balayés. */
|
|
427
|
+
get size() {
|
|
428
|
+
return this.#entries?.size ?? 0;
|
|
429
|
+
}
|
|
430
|
+
/** Le minuteur est-il armé ? */
|
|
431
|
+
get running() {
|
|
432
|
+
return this.#timer !== null;
|
|
433
|
+
}
|
|
434
|
+
/** Inscrit un flux ; arme le minuteur au premier. */
|
|
435
|
+
add(lifecycle) {
|
|
436
|
+
(this.#entries ??= /* @__PURE__ */ new Set()).add(lifecycle);
|
|
437
|
+
if (this.#timer === null) {
|
|
438
|
+
this.#timer = setInterval(this.#tick, this.#periodMs);
|
|
439
|
+
this.#timer.unref();
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
/** Retire un flux (idempotent) ; arrête le minuteur quand il n'en reste aucun. */
|
|
443
|
+
delete(lifecycle) {
|
|
444
|
+
if (this.#entries === null) return;
|
|
445
|
+
this.#entries.delete(lifecycle);
|
|
446
|
+
if (this.#entries.size === 0 && this.#timer !== null) {
|
|
447
|
+
clearInterval(this.#timer);
|
|
448
|
+
this.#timer = null;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Un tour : chaque flux inscrit juge son propre blocage.
|
|
453
|
+
*
|
|
454
|
+
* @param now - l'heure du tour (`Date.now()`).
|
|
455
|
+
*/
|
|
456
|
+
sweep(now) {
|
|
457
|
+
if (this.#entries === null) return;
|
|
458
|
+
for (const lifecycle of this.#entries) lifecycle.sweepStall(now);
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
/**
|
|
462
|
+
* Le registre de l'arrêt : TOUS les flux longs ouverts qui n'ont pas de fin à
|
|
463
|
+
* eux — SSE, flux RPC, flux d'octets vivants (ADR-0026 D8). À l'arrêt du
|
|
464
|
+
* serveur, le noyau les termine AVANT le drain des connexions, au lieu de les
|
|
465
|
+
* laisser détruire sans un mot à `shutdownTimeout`.
|
|
466
|
+
*
|
|
467
|
+
* Distinct du registre de révocation (`RevocationWatch`), qui ne tient que les
|
|
468
|
+
* flux à identité : y mettre les anonymes ferait travailler chaque fenêtre de
|
|
469
|
+
* re-validation pour rien.
|
|
470
|
+
*
|
|
471
|
+
* Coût : un ajout et un retrait par flux long, constants (44 à 77 ns mesurés
|
|
472
|
+
* de 0 à 10 000 flux ouverts, ADR-0026 D8) ; 0 par appel unaire, 0 par
|
|
473
|
+
* message.
|
|
474
|
+
*
|
|
475
|
+
* @internal
|
|
476
|
+
*/
|
|
477
|
+
var ShutdownRegistry = class {
|
|
478
|
+
#entries = /* @__PURE__ */ new Set();
|
|
479
|
+
/** L'arrêt a commencé : un flux inscrit désormais est terminé au tour suivant. */
|
|
480
|
+
#closing = false;
|
|
481
|
+
/** Fin de l'attente de l'arrêt, appelée au retrait du dernier flux. */
|
|
482
|
+
#onEmpty = null;
|
|
483
|
+
/** Nombre de flux ouverts. */
|
|
484
|
+
get size() {
|
|
485
|
+
return this.#entries.size;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Inscrit un flux. Pendant l'arrêt, il est terminé au tour suivant — une
|
|
489
|
+
* fois son appel RPC relié, qui ne l'est qu'après l'ouverture.
|
|
490
|
+
*/
|
|
491
|
+
add(stream) {
|
|
492
|
+
this.#entries.add(stream);
|
|
493
|
+
if (this.#closing) setImmediate(shutdownLate, stream);
|
|
494
|
+
}
|
|
495
|
+
/** Retire un flux (idempotent) ; le dernier retiré met fin à l'attente. */
|
|
496
|
+
delete(stream) {
|
|
497
|
+
if (!this.#entries.delete(stream) || this.#entries.size !== 0) return;
|
|
498
|
+
const done = this.#onEmpty;
|
|
499
|
+
this.#onEmpty = null;
|
|
500
|
+
done?.();
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* L'arrêt : termine chaque flux, puis attend que tous se soient retirés, au
|
|
504
|
+
* plus `waitMs`.
|
|
505
|
+
*
|
|
506
|
+
* @param waitMs - attente maximale des fermetures.
|
|
507
|
+
* @returns le nombre de flux encore ouverts à la fin de l'attente — tout de
|
|
508
|
+
* suite quand il n'y a rien à attendre, sinon sa promesse.
|
|
509
|
+
*/
|
|
510
|
+
shutdown(waitMs) {
|
|
511
|
+
this.#closing = true;
|
|
512
|
+
for (const stream of this.#entries) stream.shutdown();
|
|
513
|
+
if (this.#entries.size === 0) return 0;
|
|
514
|
+
return new Promise((resolve) => {
|
|
515
|
+
const timer = setTimeout(() => {
|
|
516
|
+
this.#onEmpty = null;
|
|
517
|
+
resolve(this.#entries.size);
|
|
518
|
+
}, waitMs);
|
|
519
|
+
this.#onEmpty = () => {
|
|
520
|
+
clearTimeout(timer);
|
|
521
|
+
resolve(0);
|
|
522
|
+
};
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
/** Termine un flux inscrit pendant l'arrêt (rappel de `setImmediate`). */
|
|
527
|
+
function shutdownLate(stream) {
|
|
528
|
+
stream.shutdown();
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Ouvre le cycle de vie d'un flux sur la réponse d'un contexte HTTP.
|
|
532
|
+
*
|
|
533
|
+
* Prend la réponse, pose l'écoute de sa fermeture, puis sauve la session —
|
|
534
|
+
* sans rien écrire : les en-têtes appartiennent au flux. Un client parti
|
|
535
|
+
* pendant la sauvegarde rend un cycle déjà fermé (`closed`) : le flux n'écrit
|
|
536
|
+
* alors rien.
|
|
537
|
+
*
|
|
538
|
+
* @param context - le contexte de la requête, pas encore répondu.
|
|
539
|
+
* @param options - gardes du flux et préfixe des messages.
|
|
540
|
+
* @returns le cycle de vie, ou sa promesse quand une session est à sauver.
|
|
541
|
+
* @throws HttpError 500 quand la réponse est déjà partie, ou que son
|
|
542
|
+
* transport n'est pas reconnu.
|
|
543
|
+
* @throws HttpError 429 quand l'IP du client a déjà tous ses flux et sockets
|
|
544
|
+
* (`wsMaxConnectionsPerIp`) — refusé AVANT d'écrire quoi que ce soit.
|
|
545
|
+
*
|
|
546
|
+
* @internal
|
|
547
|
+
*/
|
|
548
|
+
function openStreamLifecycle(context, options) {
|
|
549
|
+
const { label } = options;
|
|
550
|
+
if (context.sended || context.finished || context.response.isHeaderSent()) throw new HttpError(`${label} : la réponse est déjà partie`, 500);
|
|
551
|
+
const nodeResponse = context.response.response;
|
|
552
|
+
if (nodeResponse === null) throw new HttpError(`${label} : réponse introuvable`, 500, context);
|
|
553
|
+
const target = resolveStreamTarget(context);
|
|
554
|
+
if (target === null) throw new HttpError(`${label} : transport de réponse inconnu`, 500, context);
|
|
555
|
+
const gone = target.destroyed;
|
|
556
|
+
const kernel = context.httpKernel;
|
|
557
|
+
if (!gone && kernel !== null && !kernel.acquireConnectionSlot(context.request.request, nodeResponse)) throw new HttpError(`${label} : trop de connexions ouvertes depuis cette adresse`, 429, context);
|
|
558
|
+
const revalidate = kernel?.firewall?.currentRevalidator?.() ?? null;
|
|
559
|
+
const identifier = revalidate === null ? null : RequestContext.getAuthenticatedUser()?.identifier ?? null;
|
|
560
|
+
context.sended = true;
|
|
561
|
+
const lifecycle = new StreamLifecycle(context, target, nodeResponse, {
|
|
562
|
+
stallTimeout: options.stallTimeout,
|
|
563
|
+
maxDuration: options.maxDuration,
|
|
564
|
+
revalidate,
|
|
565
|
+
identifier,
|
|
566
|
+
foreignWriter: options.foreignWriter ?? false,
|
|
567
|
+
endsAtShutdown: options.endsAtShutdown ?? true
|
|
568
|
+
});
|
|
569
|
+
if (gone) {
|
|
570
|
+
lifecycle.finish();
|
|
571
|
+
return lifecycle;
|
|
572
|
+
}
|
|
573
|
+
if (context.session == null) return lifecycle;
|
|
574
|
+
return context.saveSession().catch((e) => {
|
|
575
|
+
context.log(e, "ERROR", "SESSION-STORE");
|
|
576
|
+
return null;
|
|
577
|
+
}).then(() => {
|
|
578
|
+
if (target.destroyed) lifecycle.finish();
|
|
579
|
+
return lifecycle;
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
export { MAX_TIMER_MS, SHUTDOWN_STREAMS_WAIT_MS, STALL_SWEEP_PERIOD_MS, STREAM_STALL_TIMEOUT_MS, ShutdownRegistry, StallSweep, StreamLifecycle, checkStreamTimer, disarmResponseTimeout, openStreamLifecycle, resolveStreamTarget };
|
|
@@ -8,8 +8,8 @@ var Parser = class {
|
|
|
8
8
|
chunks;
|
|
9
9
|
received = 0;
|
|
10
10
|
aborted = false;
|
|
11
|
-
|
|
12
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
79
|
+
this.onFailure = (err) => {
|
|
62
80
|
cleanup();
|
|
63
81
|
reject(err);
|
|
64
82
|
};
|