@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import HttpError from "../src/errors/httpError.js";
|
|
1
|
+
import HttpError, { serializeHttpError } from "../src/errors/httpError.js";
|
|
2
2
|
import { toWsCloseCode } from "../src/context/websocket/WebsocketContext.js";
|
|
3
3
|
import { Nodefony, nodefonyError } from "nodefony";
|
|
4
4
|
// nodefony/service/error-renderer.ts
|
|
@@ -49,17 +49,23 @@ const UNIQUE_VIOLATION_MESSAGE = "Conflict — a resource with these unique valu
|
|
|
49
49
|
*/
|
|
50
50
|
const OPAQUE_SERVER_ERROR = "Internal Server Error";
|
|
51
51
|
/**
|
|
52
|
+
* Le message rendu au client : celui de l'erreur, sauf une panne 5xx en
|
|
53
|
+
* production, qui devient opaque. Un 4xx garde le sien — c'est un message
|
|
54
|
+
* écrit POUR le client (validation, refus de politique), pas une fuite.
|
|
55
|
+
*/
|
|
56
|
+
const clientMessage = (status, message, production) => production && status >= 500 ? OPAQUE_SERVER_ERROR : message;
|
|
57
|
+
/**
|
|
52
58
|
* Clés d'un `HttpError` sérialisé qui décrivent les ENTRAILLES du serveur et ne
|
|
53
59
|
* doivent jamais franchir la frontière en production.
|
|
54
60
|
*/
|
|
55
|
-
const INTERNAL_ERROR_KEYS = [
|
|
61
|
+
const INTERNAL_ERROR_KEYS = /* @__PURE__ */ new Set([
|
|
56
62
|
"stack",
|
|
57
63
|
"controller",
|
|
58
64
|
"action",
|
|
59
65
|
"bundle",
|
|
60
66
|
"url",
|
|
61
67
|
"pdu"
|
|
62
|
-
];
|
|
68
|
+
]);
|
|
63
69
|
/**
|
|
64
70
|
* Vrai si le runtime tourne en production — le détail des erreurs reste alors au
|
|
65
71
|
* journal, seul le serveur le voit.
|
|
@@ -208,10 +214,11 @@ const PG_INVALID_INPUT = /^invalid input syntax for type (\w+): "(.*)"$/su;
|
|
|
208
214
|
/**
|
|
209
215
|
* Reconnaît un identifiant MAL FORMÉ refusé par la base, et rend la valeur fautive.
|
|
210
216
|
*
|
|
211
|
-
*
|
|
212
|
-
* un ObjectId), PostgreSQL par `22P02` (la clé est un `uuid` ou un entier)
|
|
213
|
-
*
|
|
214
|
-
*
|
|
217
|
+
* Trois sources le disent : MongoDB par une `CastError` de Mongoose (la clé est
|
|
218
|
+
* un ObjectId), PostgreSQL par `22P02` (la clé est un `uuid` ou un entier), et
|
|
219
|
+
* le kit de colonnes portable par une `ColumnValueError` de genre `uuid` ou
|
|
220
|
+
* `integer` — il refuse sur les trois moteurs la clé que PostgreSQL refuserait. Une clé rangée en
|
|
221
|
+
* texte NATIF ne lève rien, la lecture rend « rien » : le même 404.
|
|
215
222
|
*
|
|
216
223
|
* Même descente dans `cause` que {@link isUniqueViolation} (Drizzle enveloppe).
|
|
217
224
|
*
|
|
@@ -223,6 +230,7 @@ function malformedIdentifierOf(error) {
|
|
|
223
230
|
for (let depth = 0; depth < 5 && current instanceof Error; depth += 1) {
|
|
224
231
|
const candidate = current;
|
|
225
232
|
if (candidate.name === "CastError" && (candidate.kind === "ObjectId" || candidate.kind === "UUID") && (typeof candidate.value === "string" || typeof candidate.value === "number")) return String(candidate.value);
|
|
233
|
+
if (candidate.name === "ColumnValueError" && (candidate.kind === "uuid" || candidate.kind === "integer") && typeof candidate.value === "string") return candidate.value;
|
|
226
234
|
if (String(candidate.code) === "22P02") {
|
|
227
235
|
const match = PG_INVALID_INPUT.exec(candidate.message);
|
|
228
236
|
if (match) {
|
|
@@ -374,12 +382,10 @@ var DefaultErrorRenderer = class {
|
|
|
374
382
|
const status = this.normalizeHttpStatus(httpError.code);
|
|
375
383
|
httpError.code = status;
|
|
376
384
|
const obj = context.metaData;
|
|
377
|
-
const
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
serialized.message = message;
|
|
382
|
-
}
|
|
385
|
+
const production = isProduction();
|
|
386
|
+
const serialized = serializeHttpError(httpError, production ? INTERNAL_ERROR_KEYS : null);
|
|
387
|
+
const message = clientMessage(status, httpError.message, production);
|
|
388
|
+
if (production) serialized.message = message;
|
|
383
389
|
obj.error = serialized;
|
|
384
390
|
obj.code = status;
|
|
385
391
|
obj.message = message;
|
|
@@ -403,6 +409,34 @@ var DefaultErrorRenderer = class {
|
|
|
403
409
|
reason
|
|
404
410
|
};
|
|
405
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* Le statut HTTP et le message client qu'une route HTTP rendrait pour cette
|
|
414
|
+
* erreur — mêmes classements que {@link renderHttp} (validation 422, conflit
|
|
415
|
+
* 409, identifiant malformé, code numérique) et même opacité d'un 5xx en
|
|
416
|
+
* production —, sans RIEN écrire : ni statut sur la réponse, ni `metaData`,
|
|
417
|
+
* ni pile formatée. Sert les transports sans corps d'erreur : un appel gRPC
|
|
418
|
+
* traduit ce statut en code (ADR-0026 D7).
|
|
419
|
+
*
|
|
420
|
+
* Un identifiant malformé y vaut 400 : sans contexte, aucun chemin ne le
|
|
421
|
+
* désigne.
|
|
422
|
+
*
|
|
423
|
+
* @param error - l'erreur levée.
|
|
424
|
+
* @returns son statut émettable et le message destiné au client.
|
|
425
|
+
*
|
|
426
|
+
* @experimental Surface gRPC, ADR-0026 D11.
|
|
427
|
+
*/
|
|
428
|
+
describe(error) {
|
|
429
|
+
const httpError = this.toHttpError(error, void 0);
|
|
430
|
+
const status = this.normalizeHttpStatus(httpError.code);
|
|
431
|
+
return {
|
|
432
|
+
status,
|
|
433
|
+
message: clientMessage(status, httpError.message, isProduction())
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Ramène une erreur à une `HttpError`. Sans contexte (`describe`), rien
|
|
438
|
+
* n'est écrit : `HttpError` ne pose le statut sur la réponse qu'avec lui.
|
|
439
|
+
*/
|
|
406
440
|
toHttpError(error, context) {
|
|
407
441
|
if (error instanceof HttpError) return error;
|
|
408
442
|
const fields = toValidationFields(error);
|
|
@@ -1,34 +1,44 @@
|
|
|
1
|
-
import { isProfiledUrl } from "../src/profiler/aggregate.js";
|
|
2
1
|
import HttpError from "../src/errors/httpError.js";
|
|
2
|
+
import { isUnsupportedTransferCoding } from "../src/proxy/rules.js";
|
|
3
|
+
import { hasConflictingAuthority } from "../src/context/transport.js";
|
|
4
|
+
import { grpcWireOf, hasGrpcRoutes, prepareGrpcRefusal } from "../src/grpc/refusal.js";
|
|
3
5
|
import { buildTrustProxy } from "../src/context/trustProxy.js";
|
|
4
6
|
import { resolveForwarded } from "../src/context/forwarded.js";
|
|
5
7
|
import { MemoryRateLimitStore } from "../src/rateLimit/MemoryRateLimitStore.js";
|
|
6
8
|
import { WsConnectionCounter } from "../src/rateLimit/WsConnectionCounter.js";
|
|
7
9
|
import { compileDomainPatterns, compileTrustedHosts, isDomainAllowed } from "../src/context/domainMatcher.js";
|
|
10
|
+
import { isProfiledUrl } from "../src/profiler/aggregate.js";
|
|
8
11
|
import { responseEnded } from "../src/context/responseEnded.js";
|
|
9
12
|
import { unsentResponseWarning } from "../src/context/unsentResponse.js";
|
|
10
13
|
import { closeRequestTrace, hasRequestTraceSubscribers, openRequestTrace, runTraced } from "../src/context/requestTrace.js";
|
|
11
|
-
import {
|
|
14
|
+
import { SHUTDOWN_STREAMS_WAIT_MS, ShutdownRegistry, StallSweep } from "../src/context/http/StreamLifecycle.js";
|
|
12
15
|
import WebsocketContext, { toHandshakeStatus } from "../src/context/websocket/WebsocketContext.js";
|
|
16
|
+
import { ReceiveDeadline } from "../src/servers/receiveDeadline.js";
|
|
13
17
|
import { trackPong } from "./servers/wsHeartbeat.js";
|
|
18
|
+
import { parseWebsocketUrl } from "../src/context/websocket/websocketUrl.js";
|
|
14
19
|
import HttpContext from "../src/context/http/HttpContext.js";
|
|
15
20
|
import DefaultErrorRenderer from "./error-renderer.js";
|
|
16
|
-
import JsonAuditLogger from "./audit-logger.js";
|
|
17
21
|
import DefaultRequestLogger from "./request-logger.js";
|
|
18
22
|
import PrettyRequestLogger from "./pretty-request-logger.js";
|
|
23
|
+
import JsonAuditLogger from "./audit-logger.js";
|
|
19
24
|
import { resolveTraceparent } from "./trace.js";
|
|
20
25
|
import { asServersConfig, configuredServerPort } from "../src/servers/kernelServers.js";
|
|
21
|
-
import __decorate from "../../_virtual/_@oxc-project_runtime@0.
|
|
22
|
-
import { GcScheduler, IDENTITY_CHANGED_EVENT, RequestContext, RevocationWatch, Service, finallyMaybe, injectable, isPromise, logColor, nodefonyError, servedUrl, thenMaybe, writeRuntimeState } from "nodefony";
|
|
26
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorate.js";
|
|
27
|
+
import { BootConfigurationError, GcScheduler, IDENTITY_CHANGED_EVENT, RequestContext, RevocationWatch, Service, finallyMaybe, injectable, isPromise, logColor, nodefonyError, servedUrl, thenMaybe, writeRuntimeState } from "nodefony";
|
|
23
28
|
import cluster from "node:cluster";
|
|
24
29
|
import http from "node:http";
|
|
25
30
|
// nodefony/service/http-kernel.ts
|
|
26
|
-
const HEALTH_HEADERS =
|
|
27
|
-
"content-type"
|
|
28
|
-
"
|
|
29
|
-
|
|
31
|
+
const HEALTH_HEADERS = [
|
|
32
|
+
"content-type",
|
|
33
|
+
"application/json",
|
|
34
|
+
"cache-control",
|
|
35
|
+
"no-store"
|
|
36
|
+
];
|
|
30
37
|
const HEALTH_BODY_OK = "{\"status\":\"ok\"}";
|
|
31
38
|
const HEALTH_BODY_UNREADY = "{\"status\":\"unready\"}";
|
|
39
|
+
/** Corps du 429 du limiteur de débit — figé, rien à sérialiser par refus. */
|
|
40
|
+
const TOO_MANY_REQUESTS_BODY = "{\"code\":429,\"message\":\"Too Many Requests\"}";
|
|
41
|
+
const TOO_MANY_REQUESTS_LENGTH = Buffer.byteLength(TOO_MANY_REQUESTS_BODY);
|
|
32
42
|
const perfProbe = process.env.NF_PERF_PROBE === "1" ? {
|
|
33
43
|
count: 0,
|
|
34
44
|
enterScopeNs: 0,
|
|
@@ -104,9 +114,33 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
104
114
|
}
|
|
105
115
|
responseTimeout;
|
|
106
116
|
closeTimeOutWs;
|
|
117
|
+
/**
|
|
118
|
+
* Échéance de réception du corps HTTP/2, par serveur (`requestTimeout` de sa
|
|
119
|
+
* section) : `node:http2` ne borne pas la réception, HTTP/1.1 l'est par Node.
|
|
120
|
+
* `undefined` tant qu'aucun corps HTTP/2 n'a été lu, `null` si le délai est
|
|
121
|
+
* désactivé (0). Cf {@link ReceiveDeadline}.
|
|
122
|
+
*/
|
|
123
|
+
#h2Receive = {
|
|
124
|
+
http: void 0,
|
|
125
|
+
https: void 0
|
|
126
|
+
};
|
|
107
127
|
secContentTypeOptions = null;
|
|
108
128
|
secFrameOptions = null;
|
|
109
129
|
secHsts = null;
|
|
130
|
+
/**
|
|
131
|
+
* En-têtes du transport, tableau plat `[nom, valeur, …]` aux noms en
|
|
132
|
+
* minuscules, par famille de serveur (HSTS sur TLS seulement). Figés et
|
|
133
|
+
* VALIDÉS au calcul : chaque réponse les recopie sans les revalider.
|
|
134
|
+
*/
|
|
135
|
+
#transportHeaders = {
|
|
136
|
+
http: [],
|
|
137
|
+
https: []
|
|
138
|
+
};
|
|
139
|
+
/** Réponse d'une sonde de santé : le transport, puis type et cache. */
|
|
140
|
+
#healthHeaders = {
|
|
141
|
+
http: [],
|
|
142
|
+
https: []
|
|
143
|
+
};
|
|
110
144
|
sessionService;
|
|
111
145
|
router;
|
|
112
146
|
firewall;
|
|
@@ -121,9 +155,11 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
121
155
|
rateLimitGc = null;
|
|
122
156
|
#wsConnCounter = null;
|
|
123
157
|
#streams = null;
|
|
158
|
+
#stallSweep = null;
|
|
159
|
+
#openStreams = null;
|
|
124
160
|
#healthPaths = null;
|
|
125
161
|
#healthShutdownDelay = 0;
|
|
126
|
-
#
|
|
162
|
+
#shutdownSteps = null;
|
|
127
163
|
constructor(module) {
|
|
128
164
|
super(serviceName, module.container, module.notificationsCenter, module.options);
|
|
129
165
|
this.module = module;
|
|
@@ -157,12 +193,49 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
157
193
|
if (hsts.preload) v += "; preload";
|
|
158
194
|
this.secHsts = v;
|
|
159
195
|
} else this.secHsts = null;
|
|
196
|
+
const headerServer = this.options.headerServer;
|
|
197
|
+
const http1 = [];
|
|
198
|
+
if (headerServer != null && headerServer !== "") http1.push("server", headerServer);
|
|
199
|
+
if (this.secContentTypeOptions !== null) http1.push("x-content-type-options", this.secContentTypeOptions);
|
|
200
|
+
if (this.secFrameOptions !== null) http1.push("x-frame-options", this.secFrameOptions);
|
|
201
|
+
const tls = this.secHsts === null ? http1 : [
|
|
202
|
+
...http1,
|
|
203
|
+
"strict-transport-security",
|
|
204
|
+
this.secHsts
|
|
205
|
+
];
|
|
206
|
+
for (let i = 0; i + 1 < tls.length; i += 2) {
|
|
207
|
+
const name = tls[i] ?? "";
|
|
208
|
+
try {
|
|
209
|
+
http.validateHeaderValue(name, tls[i + 1] ?? "");
|
|
210
|
+
} catch (error) {
|
|
211
|
+
throw new BootConfigurationError(`[@nodefony/http] en-tête « ${name} » refusé : ${error instanceof Error ? error.message : String(error)}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
this.#transportHeaders = {
|
|
215
|
+
http: http1,
|
|
216
|
+
https: tls
|
|
217
|
+
};
|
|
218
|
+
this.#healthHeaders = {
|
|
219
|
+
http: [...http1, ...HEALTH_HEADERS],
|
|
220
|
+
https: [...tls, ...HEALTH_HEADERS]
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* En-têtes du transport d'une réponse, figés au démarrage — ce que
|
|
225
|
+
* `HttpResponse` place en tête de sa réserve.
|
|
226
|
+
*
|
|
227
|
+
* @param type - famille du serveur qui a reçu la requête.
|
|
228
|
+
* @returns la liste plate `[nom, valeur, …]`, noms en minuscules.
|
|
229
|
+
*/
|
|
230
|
+
transportHeaders(type) {
|
|
231
|
+
return type === "https" ? this.#transportHeaders.https : this.#transportHeaders.http;
|
|
160
232
|
}
|
|
161
233
|
/**
|
|
162
234
|
* Ce que ce kernel ÉMET réellement comme en-têtes de sécurité transport.
|
|
163
235
|
*
|
|
164
236
|
* Ces trois en-têtes (HSTS, X-Frame-Options, X-Content-Type-Options) sont posés
|
|
165
|
-
*
|
|
237
|
+
* par `@nodefony/http` sur TOUTE réponse (en tête de la réserve de `HttpResponse`),
|
|
238
|
+
* statiques et pages d'erreur comprises
|
|
166
239
|
* — pas par `@nodefony/security`, dont les clés homonymes sont inertes. Toute
|
|
167
240
|
* console d'administration doit donc lire CE point, sinon elle affiche une
|
|
168
241
|
* intention de configuration au lieu de l'état appliqué.
|
|
@@ -189,6 +262,7 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
189
262
|
*/
|
|
190
263
|
onConfigChanged() {
|
|
191
264
|
this.computeSecurityHeaderCaches();
|
|
265
|
+
this.firewall?.onTransportHeadersChanged?.();
|
|
192
266
|
this._trustProxyChecker = null;
|
|
193
267
|
this._wsOriginPolicy = Object.create(null);
|
|
194
268
|
this.trustedHosts = this.options.trustedHosts;
|
|
@@ -233,7 +307,7 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
233
307
|
* Le message nomme l'action, ce qu'elle a rendu et le geste qui convient
|
|
234
308
|
* (`unsentResponseWarning`).
|
|
235
309
|
*
|
|
236
|
-
* Appelé au timeout, AVANT que le
|
|
310
|
+
* Appelé au timeout, AVANT que le 503 parte — après, `sended` est vrai et
|
|
237
311
|
* plus rien ne se signalerait — et au démontage, pour un client parti avant.
|
|
238
312
|
* Gratuit en production : rien n'est lu au-delà de `waitAsync`.
|
|
239
313
|
*
|
|
@@ -296,9 +370,9 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
296
370
|
* @param response - réponse serveur brute (HTTP/1 ou H2 compat)
|
|
297
371
|
* @param ok - `true` → 200, `false` → 503 (readiness pendant boot/shutdown)
|
|
298
372
|
*/
|
|
299
|
-
#respondHealth(response, ok) {
|
|
373
|
+
#respondHealth(response, ok, type) {
|
|
300
374
|
const res = response;
|
|
301
|
-
res.writeHead(ok ? 200 : 503,
|
|
375
|
+
res.writeHead(ok ? 200 : 503, type === "https" ? this.#healthHeaders.https : this.#healthHeaders.http);
|
|
302
376
|
res.end(ok ? HEALTH_BODY_OK : HEALTH_BODY_UNREADY);
|
|
303
377
|
}
|
|
304
378
|
async init() {
|
|
@@ -320,13 +394,10 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
320
394
|
});
|
|
321
395
|
this.once("onTerminate", () => {
|
|
322
396
|
this.rateLimitGc?.stop();
|
|
397
|
+
this.#h2Receive.http?.close();
|
|
398
|
+
this.#h2Receive.https?.close();
|
|
323
399
|
});
|
|
324
|
-
this.kernel?.once("
|
|
325
|
-
this.kernel?.prependOnceListener("onTerminate", async () => {
|
|
326
|
-
this.#terminating = true;
|
|
327
|
-
if (this.#healthShutdownDelay > 0) await new Promise((r) => setTimeout(r, this.#healthShutdownDelay));
|
|
328
|
-
});
|
|
329
|
-
});
|
|
400
|
+
this.kernel?.once("onDrain", () => this.drainTraffic());
|
|
330
401
|
this.kernel?.prependOnceListener("onBoot", () => {
|
|
331
402
|
this.router = this.get("router");
|
|
332
403
|
this.firewall = this.get("firewall");
|
|
@@ -419,7 +490,7 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
419
490
|
const secure = context.serverType === "wss";
|
|
420
491
|
const raw = context.origin;
|
|
421
492
|
if (this.isWebsocketOriginAllowed(raw, context.domain, secure)) return;
|
|
422
|
-
const error =
|
|
493
|
+
const error = HttpError.withoutStack(`WebSocket Origin "${raw}" not allowed (CSWSH protection)`, 1008, context);
|
|
423
494
|
error.type = "origin";
|
|
424
495
|
throw error;
|
|
425
496
|
}
|
|
@@ -523,7 +594,7 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
523
594
|
}
|
|
524
595
|
throw resolver.exception;
|
|
525
596
|
}
|
|
526
|
-
throw
|
|
597
|
+
throw HttpError.withoutStack("Not Found", 404, context);
|
|
527
598
|
}
|
|
528
599
|
/**
|
|
529
600
|
* {@link prepareFrontController} + instanciation immédiate du controller
|
|
@@ -608,10 +679,14 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
608
679
|
if (!context.response.isHeaderSent()) {
|
|
609
680
|
if (context.isRedirect) {
|
|
610
681
|
context.isRedirect = false;
|
|
611
|
-
context.response.
|
|
682
|
+
context.response.removeHeader("Location");
|
|
612
683
|
}
|
|
684
|
+
context.response.closeUnfinishedRequest();
|
|
685
|
+
const headers = context.request.headers;
|
|
686
|
+
const wire = grpcWireOf(headers, context.method === "GET" && context.request.url.searchParams.get("connect") === "v1");
|
|
613
687
|
try {
|
|
614
|
-
await context.render(result.body);
|
|
688
|
+
if (wire === 0) await context.render(result.body);
|
|
689
|
+
else await context.send(prepareGrpcRefusal(context.response, wire, result.status, context.authenticatedUser() !== null, headers["content-type"]));
|
|
615
690
|
} catch (e) {
|
|
616
691
|
this.log(e, "CRITIC");
|
|
617
692
|
throw e;
|
|
@@ -678,19 +753,14 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
678
753
|
* pipeline.
|
|
679
754
|
*/
|
|
680
755
|
dispatchHttpRequest(request, response, type) {
|
|
681
|
-
const headerServer = this.options.headerServer;
|
|
682
|
-
if (headerServer != null && headerServer !== "") response.setHeader("Server", headerServer);
|
|
683
|
-
if (this.secContentTypeOptions !== null) response.setHeader("X-Content-Type-Options", this.secContentTypeOptions);
|
|
684
|
-
if (this.secFrameOptions !== null) response.setHeader("X-Frame-Options", this.secFrameOptions);
|
|
685
|
-
if (this.secHsts !== null && type === "https") response.setHeader("Strict-Transport-Security", this.secHsts);
|
|
686
756
|
if (this.#healthPaths !== null) {
|
|
687
757
|
const url = request.url;
|
|
688
758
|
if (url === this.#healthPaths.liveness) {
|
|
689
|
-
this.#respondHealth(response, true);
|
|
759
|
+
this.#respondHealth(response, true, type);
|
|
690
760
|
return;
|
|
691
761
|
}
|
|
692
762
|
if (url === this.#healthPaths.readiness) {
|
|
693
|
-
this.#respondHealth(response,
|
|
763
|
+
this.#respondHealth(response, this.kernel?.isTerminating !== true && this.kernel?.servable === true, type);
|
|
694
764
|
return;
|
|
695
765
|
}
|
|
696
766
|
}
|
|
@@ -704,13 +774,19 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
704
774
|
if (verdict.limited) {
|
|
705
775
|
response.setHeader("Retry-After", verdict.retryAfterS);
|
|
706
776
|
const res = response;
|
|
707
|
-
|
|
708
|
-
|
|
777
|
+
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
778
|
+
response.setHeader("Content-Length", TOO_MANY_REQUESTS_LENGTH);
|
|
779
|
+
res.writeHead(429, type === "https" ? this.#transportHeaders.https : this.#transportHeaders.http);
|
|
780
|
+
res.end(TOO_MANY_REQUESTS_BODY);
|
|
709
781
|
return;
|
|
710
782
|
}
|
|
711
783
|
}
|
|
712
784
|
}
|
|
713
|
-
if (this.listenerCount("onServerRequest"))
|
|
785
|
+
if (this.listenerCount("onServerRequest")) {
|
|
786
|
+
const transport = type === "https" ? this.#transportHeaders.https : this.#transportHeaders.http;
|
|
787
|
+
for (let i = 0; i + 1 < transport.length; i += 2) response.setHeader(transport[i] ?? "", transport[i + 1] ?? "");
|
|
788
|
+
return this.fireAsync("onServerRequest", request, response, type).then(() => this.handle(request, response, type));
|
|
789
|
+
}
|
|
714
790
|
return this.handle(request, response, type);
|
|
715
791
|
}
|
|
716
792
|
async initServers() {
|
|
@@ -850,13 +926,29 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
850
926
|
* @returns `undefined` quand le démontage est terminé ; sinon la promesse de
|
|
851
927
|
* sa fin, qui ne rejette pas.
|
|
852
928
|
*/
|
|
929
|
+
/**
|
|
930
|
+
* Fixe le code d'un appel RPC au `close` : « réponse finie » est la règle
|
|
931
|
+
* unique du noyau ({@link responseEnded}, celle du 499), jamais
|
|
932
|
+
* `writableFinished`, qui dit « finie » sur une annulation HTTP/2. Une cause
|
|
933
|
+
* rendue par l'appel devient l'erreur de la requête : la ligne d'accès la
|
|
934
|
+
* porte, le canal la publie — une ligne par appel en erreur (D10 point 3).
|
|
935
|
+
*
|
|
936
|
+
* @param context - le contexte de la requête.
|
|
937
|
+
* @param rpc - son appel RPC.
|
|
938
|
+
*/
|
|
939
|
+
settleRpc(context, rpc) {
|
|
940
|
+
const raw = context.response.response;
|
|
941
|
+
const cause = rpc.settle(raw !== null && responseEnded(raw));
|
|
942
|
+
if (cause !== null && !context.error) context.error = cause;
|
|
943
|
+
}
|
|
853
944
|
teardownHttp(context, scope) {
|
|
854
945
|
if (context.finished) return void 0;
|
|
855
946
|
try {
|
|
856
947
|
context.stopClock();
|
|
857
948
|
if (context.waitAsync) this.warnUnsentResponse(context);
|
|
949
|
+
if (context.rpc !== null) this.settleRpc(context, context.rpc);
|
|
858
950
|
context.logRequest();
|
|
859
|
-
if (context.requestTrace !== null) closeRequestTrace(context, context.response.statusCode, context.error);
|
|
951
|
+
if (context.requestTrace !== null) closeRequestTrace(context, context.response.statusCode, context.error, context.rpc);
|
|
860
952
|
context.logPhasesVerbose();
|
|
861
953
|
if (this.shouldProfile(context.url)) this.profiler?.collect(context);
|
|
862
954
|
const afterResponse = context._runAfterResponse();
|
|
@@ -946,7 +1038,7 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
946
1038
|
if (!isPromise(served)) return served;
|
|
947
1039
|
return served.catch((e) => this.failHttp(e, created, scope));
|
|
948
1040
|
} catch (e) {
|
|
949
|
-
return this.failHttp(e, context, scope);
|
|
1041
|
+
return this.failHttp(e, context, scope, response);
|
|
950
1042
|
}
|
|
951
1043
|
}
|
|
952
1044
|
/**
|
|
@@ -986,9 +1078,19 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
986
1078
|
/**
|
|
987
1079
|
* Chemin d'erreur de {@link handleHttp} : libère le scope d'un contexte qui
|
|
988
1080
|
* n'a pas pu se construire, puis rend l'erreur au client (`onError`).
|
|
1081
|
+
*
|
|
1082
|
+
* Sans contexte, `onError` n'a rien sur quoi écrire : la réponse BRUTE
|
|
1083
|
+
* reçoit alors un 500 nu. Sinon HTTP/1.1 coupait la connexion sans un octet
|
|
1084
|
+
* et HTTP/2 laissait le flux pendu.
|
|
989
1085
|
*/
|
|
990
|
-
failHttp(e, context, scope) {
|
|
991
|
-
if (context === null)
|
|
1086
|
+
failHttp(e, context, scope, response) {
|
|
1087
|
+
if (context === null) {
|
|
1088
|
+
this.container?.leaveScope(scope);
|
|
1089
|
+
if (response && !response.headersSent) {
|
|
1090
|
+
response.statusCode = 500;
|
|
1091
|
+
response.end();
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
992
1094
|
return this.onError(e, context);
|
|
993
1095
|
}
|
|
994
1096
|
/**
|
|
@@ -1020,6 +1122,7 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
1020
1122
|
*/
|
|
1021
1123
|
routeHttpRequest(httpContext, request, response) {
|
|
1022
1124
|
if (this.reverseProxy !== null && this.reverseProxy.mounts !== null && (httpContext.validDomain || !this.domainCheck)) {
|
|
1125
|
+
if (this.reverseProxy.match(request.url) !== void 0) httpContext.response.commitHeaders();
|
|
1023
1126
|
const relayed = this.reverseProxy.forward(request, response, httpContext.scheme);
|
|
1024
1127
|
if (relayed !== void 0) {
|
|
1025
1128
|
httpContext.relayAccessLog = this.reverseProxy.match(request.url)?.accessLog ?? "all";
|
|
@@ -1029,21 +1132,64 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
1029
1132
|
httpContext.phaseStart("resolve");
|
|
1030
1133
|
httpContext.resolver = this.router ? this.router.resolve(httpContext) : null;
|
|
1031
1134
|
httpContext.phaseEnd("resolve");
|
|
1032
|
-
this.firewall
|
|
1135
|
+
if (this.firewall) httpContext.response.headerPolicy = this.firewall;
|
|
1033
1136
|
if (this.serverStatic && httpContext.resolver?.resolve !== true && !httpContext.resolver?.exception && (asServersConfig(this.kernel?.options.servers)?.statics || this.kernel?.options.statics || this.serverStatic.hasMounts())) {
|
|
1034
|
-
response.removeHeader("Content-Type");
|
|
1035
|
-
|
|
1137
|
+
httpContext.response.removeHeader("Content-Type");
|
|
1138
|
+
httpContext.response.commitBeforeNativeWrite();
|
|
1139
|
+
return this.serverStatic.handle(request, response).catch(() => void 0).then(() => {
|
|
1140
|
+
httpContext.response.disarmNativeWrite();
|
|
1141
|
+
return this.serveHttpRequest(httpContext);
|
|
1142
|
+
});
|
|
1036
1143
|
}
|
|
1037
1144
|
return this.serveHttpRequest(httpContext);
|
|
1038
1145
|
}
|
|
1039
1146
|
/**
|
|
1147
|
+
* L'échéance de réception HTTP/2 du serveur qui sert (`requestTimeout` de sa
|
|
1148
|
+
* section), créée au premier corps HTTP/2 lu ; `null` si le délai vaut 0.
|
|
1149
|
+
*
|
|
1150
|
+
* À l'échéance, la réception en cours est interrompue (`abortBody`) et la
|
|
1151
|
+
* requête rendue en 408 par le chemin d'erreur habituel — comme un corps qui
|
|
1152
|
+
* dépasse sa limite (413).
|
|
1153
|
+
*/
|
|
1154
|
+
http2ReceiveDeadline(serverType) {
|
|
1155
|
+
const key = serverType === "https" ? "https" : "http";
|
|
1156
|
+
let deadline = this.#h2Receive[key];
|
|
1157
|
+
if (deadline === void 0) {
|
|
1158
|
+
const timeoutMs = this.options[key].requestTimeout;
|
|
1159
|
+
deadline = timeoutMs > 0 ? new ReceiveDeadline(timeoutMs, (context) => {
|
|
1160
|
+
if (context.finished) return;
|
|
1161
|
+
context.request.abortBody(HttpError.withoutStack("Request Timeout", 408, context));
|
|
1162
|
+
}) : null;
|
|
1163
|
+
this.#h2Receive[key] = deadline;
|
|
1164
|
+
}
|
|
1165
|
+
return deadline;
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1040
1168
|
* Le corps — lu seulement s'il y en a un à lire —, puis les gardes
|
|
1041
1169
|
* ({@link onRequestEnd}) et l'action. Chacun n'est attendu que s'il attend.
|
|
1042
1170
|
*/
|
|
1043
1171
|
serveHttpRequest(httpContext) {
|
|
1044
|
-
const
|
|
1172
|
+
const headers = httpContext.request.headers;
|
|
1173
|
+
if (isUnsupportedTransferCoding(headers["transfer-encoding"])) return Promise.reject(HttpError.withoutStack("Transfer-Encoding not implemented", 501, httpContext));
|
|
1174
|
+
if (hasConflictingAuthority(headers)) return Promise.reject(HttpError.withoutStack("Host differs from :authority", 400, httpContext));
|
|
1175
|
+
const resolver = httpContext.resolver;
|
|
1176
|
+
if (hasGrpcRoutes() && resolver?.resolve === true && httpContext.method === "POST" && resolver.route?.grpc !== true && grpcWireOf(headers) === 1) resolver.resolve = false;
|
|
1177
|
+
const skipBody = !(resolver?.resolve === true) || resolver.route?.bodyStream === true;
|
|
1045
1178
|
httpContext.phaseStart("parse");
|
|
1046
|
-
|
|
1179
|
+
let receiving = null;
|
|
1180
|
+
let receivingSeq = 0;
|
|
1181
|
+
const parsed = finallyMaybe(() => {
|
|
1182
|
+
if (skipBody) return void 0;
|
|
1183
|
+
const pending = httpContext.request.initialize();
|
|
1184
|
+
if (pending !== void 0 && (httpContext.alpnProtocol === "h2" || httpContext.alpnProtocol === "h2c")) {
|
|
1185
|
+
receiving = this.http2ReceiveDeadline(httpContext.serverType);
|
|
1186
|
+
if (receiving !== null) receivingSeq = receiving.track(httpContext);
|
|
1187
|
+
}
|
|
1188
|
+
return pending;
|
|
1189
|
+
}, () => {
|
|
1190
|
+
httpContext.phaseEnd("parse");
|
|
1191
|
+
if (receiving !== null) receiving.release(receivingSeq, httpContext);
|
|
1192
|
+
});
|
|
1047
1193
|
return thenMaybe(parsed, () => thenMaybe(this.onRequestEnd(httpContext), (guarded) => {
|
|
1048
1194
|
guarded.phaseStart("action");
|
|
1049
1195
|
return finallyMaybe(() => guarded.handle(), () => guarded.phaseEnd("action"));
|
|
@@ -1098,8 +1244,8 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
1098
1244
|
if (this.listenerCount("afterAuth")) await this.fireAsync("afterAuth", context);
|
|
1099
1245
|
} catch (authError) {
|
|
1100
1246
|
if (context.resolver?.exception) {
|
|
1101
|
-
context.response.
|
|
1102
|
-
context.response.
|
|
1247
|
+
context.response.removeHeader("Upgrade");
|
|
1248
|
+
context.response.removeHeader("Connection");
|
|
1103
1249
|
}
|
|
1104
1250
|
await this.fireAsync("onAuthFailure", context, authError).catch((e) => this.log(e, "ERROR", "onAuthFailure"));
|
|
1105
1251
|
throw authError;
|
|
@@ -1424,17 +1570,17 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
1424
1570
|
return true;
|
|
1425
1571
|
}
|
|
1426
1572
|
/**
|
|
1427
|
-
* Inscrit un flux
|
|
1573
|
+
* Inscrit un flux (événements ou octets) à identité révocable : il sera fermé dans la fenêtre de
|
|
1428
1574
|
* re-validation qui suit la mort de son identité (déconnexion, session
|
|
1429
1575
|
* révoquée, jeton expiré ou annulé) — le même registre et la même règle que
|
|
1430
1576
|
* les sockets WebSocket.
|
|
1431
1577
|
*
|
|
1432
|
-
* @internal — appelé par le flux
|
|
1578
|
+
* @internal — appelé par le cycle de vie du flux (`StreamLifecycle`).
|
|
1433
1579
|
*/
|
|
1434
1580
|
watchStream(stream) {
|
|
1435
1581
|
(this.#streams ??= new RevocationWatch({
|
|
1436
1582
|
isValid: (entry) => entry.revalidate(),
|
|
1437
|
-
revoke: (entry) => void entry.
|
|
1583
|
+
revoke: (entry) => void entry.revoke(),
|
|
1438
1584
|
identityOf: (entry) => entry.identifier ?? void 0
|
|
1439
1585
|
})).register(stream);
|
|
1440
1586
|
}
|
|
@@ -1443,7 +1589,118 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
1443
1589
|
this.#streams?.unregister(stream);
|
|
1444
1590
|
}
|
|
1445
1591
|
/**
|
|
1446
|
-
*
|
|
1592
|
+
* Inscrit au balayage de blocage un flux dont un tiers tient les écritures :
|
|
1593
|
+
* un client qui ne lit plus y est coupé (`StreamLifecycle.sweepStall`).
|
|
1594
|
+
*
|
|
1595
|
+
* @internal — appelé par le cycle de vie du flux (`StreamLifecycle`).
|
|
1596
|
+
*/
|
|
1597
|
+
sweepStall(stream) {
|
|
1598
|
+
(this.#stallSweep ??= new StallSweep()).add(stream);
|
|
1599
|
+
}
|
|
1600
|
+
/** Retire un flux du balayage de blocage (à sa fermeture). @internal */
|
|
1601
|
+
unsweepStall(stream) {
|
|
1602
|
+
this.#stallSweep?.delete(stream);
|
|
1603
|
+
}
|
|
1604
|
+
/**
|
|
1605
|
+
* Inscrit un flux sans fin à lui (SSE, flux RPC, octets vivants) au registre
|
|
1606
|
+
* de l'arrêt : il sera terminé AVANT le drain ({@link shutdownStreams}).
|
|
1607
|
+
* Ouvert pendant l'arrêt, il l'est au tour suivant — une fois son appel RPC
|
|
1608
|
+
* relié, qui ne l'est qu'après l'ouverture.
|
|
1609
|
+
*
|
|
1610
|
+
* @internal — appelé par le cycle de vie du flux (`StreamLifecycle`).
|
|
1611
|
+
*/
|
|
1612
|
+
trackStream(stream) {
|
|
1613
|
+
(this.#openStreams ??= new ShutdownRegistry()).add(stream);
|
|
1614
|
+
}
|
|
1615
|
+
/** Retire un flux du registre de l'arrêt (à sa fermeture). @internal */
|
|
1616
|
+
untrackStream(stream) {
|
|
1617
|
+
this.#openStreams?.delete(stream);
|
|
1618
|
+
}
|
|
1619
|
+
/**
|
|
1620
|
+
* Inscrit une étape de l'arrêt du trafic, appelée par {@link drainTraffic}.
|
|
1621
|
+
*
|
|
1622
|
+
* Un serveur s'inscrit à sa création au lieu d'écouter le noyau : l'ordre
|
|
1623
|
+
* (congé des connexions longues AVANT le drain, ADR-0026 D8) vit alors dans
|
|
1624
|
+
* une seule fonction, plus dans des moments d'inscription.
|
|
1625
|
+
*
|
|
1626
|
+
* @param step - `dismiss` : congédier les connexions longues (WebSocket
|
|
1627
|
+
* 1001) ; `drain` : cesser d'accepter et laisser finir les requêtes.
|
|
1628
|
+
* @param action - l'étape ; une promesse rendue est attendue.
|
|
1629
|
+
*
|
|
1630
|
+
* @internal
|
|
1631
|
+
*/
|
|
1632
|
+
addShutdownStep(step, action) {
|
|
1633
|
+
this.#shutdownSteps ??= {
|
|
1634
|
+
dismiss: [],
|
|
1635
|
+
drain: []
|
|
1636
|
+
};
|
|
1637
|
+
this.#shutdownSteps[step].push(action);
|
|
1638
|
+
}
|
|
1639
|
+
/**
|
|
1640
|
+
* L'arrêt du trafic (ADR-0027 D2), écouteur unique d'`onDrain` :
|
|
1641
|
+
*
|
|
1642
|
+
* 1. si le pod a été annoncé disponible, attendre `health.shutdownDelay` —
|
|
1643
|
+
* la sonde répond déjà 503 (`kernel.isTerminating`), la répartition
|
|
1644
|
+
* retire le pod ;
|
|
1645
|
+
* 2. congédier ensemble les connexions longues : WebSocket (1001) et flux
|
|
1646
|
+
* longs (fin au format de leur protocole, {@link shutdownStreams}) ;
|
|
1647
|
+
* 3. drainer ensemble les serveurs HTTP, HTTPS et h2c (ports distincts).
|
|
1648
|
+
*
|
|
1649
|
+
* Une étape qui échoue est journalisée sans arrêter les autres.
|
|
1650
|
+
*
|
|
1651
|
+
* @internal
|
|
1652
|
+
*/
|
|
1653
|
+
async drainTraffic() {
|
|
1654
|
+
if (this.kernel?.postReady === true && this.#healthShutdownDelay > 0) await new Promise((r) => setTimeout(r, this.#healthShutdownDelay));
|
|
1655
|
+
const steps = this.#shutdownSteps;
|
|
1656
|
+
await this.#runShutdownSteps([...steps?.dismiss ?? [], () => this.shutdownStreams()]);
|
|
1657
|
+
if (steps !== null) await this.#runShutdownSteps(steps.drain);
|
|
1658
|
+
}
|
|
1659
|
+
/** Lance des étapes d'arrêt ensemble, attend la dernière, journalise les échecs. */
|
|
1660
|
+
async #runShutdownSteps(actions) {
|
|
1661
|
+
const settled = await Promise.allSettled(actions.map(async (action) => action()));
|
|
1662
|
+
for (const outcome of settled) if (outcome.status === "rejected") this.log(outcome.reason, "ERROR", "TERMINATE");
|
|
1663
|
+
}
|
|
1664
|
+
/**
|
|
1665
|
+
* L'arrêt du serveur : termine chaque flux long ouvert, puis attend leur
|
|
1666
|
+
* fermeture, au plus `waitMs` (ADR-0026 D8).
|
|
1667
|
+
*
|
|
1668
|
+
* Un appel RPC se termine au format de son protocole (gRPC : 14
|
|
1669
|
+
* `UNAVAILABLE`, le client le rejoue ailleurs), un flux SSE ou d'octets
|
|
1670
|
+
* vivant par une fin simple (`EventSource` se reconnecte). Un gestionnaire
|
|
1671
|
+
* qui n'entend rien laisse l'attente aller à son terme : sa connexion tombe
|
|
1672
|
+
* ensuite au drain, à `shutdownTimeout`.
|
|
1673
|
+
*
|
|
1674
|
+
* Appelé par {@link drainTraffic}, avec les fermetures 1001, avant le
|
|
1675
|
+
* drain : le client lit la fin du flux AVANT le GOAWAY définitif. Un appel unaire en vol n'est pas touché — il finit
|
|
1676
|
+
* pendant le drain.
|
|
1677
|
+
*
|
|
1678
|
+
* @param waitMs - attente maximale des fermetures.
|
|
1679
|
+
* @returns la promesse de l'attente, ou rien quand aucun flux n'est ouvert.
|
|
1680
|
+
*
|
|
1681
|
+
* @internal
|
|
1682
|
+
*/
|
|
1683
|
+
shutdownStreams(waitMs = SHUTDOWN_STREAMS_WAIT_MS) {
|
|
1684
|
+
const registry = this.#openStreams ??= new ShutdownRegistry();
|
|
1685
|
+
const count = registry.size;
|
|
1686
|
+
const left = registry.shutdown(waitMs);
|
|
1687
|
+
if (count === 0) return void 0;
|
|
1688
|
+
if (typeof left === "number") {
|
|
1689
|
+
this.#logStreamsShutdown(count, left, waitMs);
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
return left.then((still) => this.#logStreamsShutdown(count, still, waitMs));
|
|
1693
|
+
}
|
|
1694
|
+
/** Une ligne pour l'arrêt des flux longs ; un avertissement s'il en reste. */
|
|
1695
|
+
#logStreamsShutdown(count, still, waitMs) {
|
|
1696
|
+
if (still === 0) {
|
|
1697
|
+
this.log(`Arrêt : ${count} flux long(s) terminé(s)`, "INFO");
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
this.log(`Arrêt : ${still} flux long(s) sur ${count} toujours ouvert(s) après ${waitMs} ms — gestionnaire qui n'écoute pas son signal ; coupé(s) par le drain à shutdownTimeout`, "WARNING");
|
|
1701
|
+
}
|
|
1702
|
+
/**
|
|
1703
|
+
* Re-valide tous les flux en cours et ferme ceux dont l'identité est morte.
|
|
1447
1704
|
* Publique pour qu'un banc la déclenche sans attendre le tick.
|
|
1448
1705
|
*/
|
|
1449
1706
|
revalidateStreams() {
|