@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
|
@@ -21,6 +21,85 @@ const COLON = 58;
|
|
|
21
21
|
* @returns les composants découpés, ou `null` (bail-out → vrai `new URL`).
|
|
22
22
|
*/
|
|
23
23
|
function splitTarget(target) {
|
|
24
|
+
return splitTargetFast(target);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Origine fixe d'un request-target analysé SANS requête HTTP (pont
|
|
28
|
+
* `api.request`) : le chemin y est CONCATÉNÉ, jamais résolu comme relatif — un
|
|
29
|
+
* `//autre/x` reste un pathname `//autre/x`, comme le verrait la porte HTTP,
|
|
30
|
+
* au lieu de devenir une autorité.
|
|
31
|
+
*/
|
|
32
|
+
const DETACHED_ORIGIN = "http://request-target.invalid";
|
|
33
|
+
const HASH = 35;
|
|
34
|
+
const AT = 64;
|
|
35
|
+
/**
|
|
36
|
+
* Découpe un request-target en forme absolue (`http://hôte:port/chemin?q`) :
|
|
37
|
+
* la forme qu'un client envoie à un proxy, et qu'un serveur d'origine DOIT
|
|
38
|
+
* accepter (RFC 9112 §3.2.2). Il DOIT alors ignorer l'en-tête `Host` et prendre
|
|
39
|
+
* l'hôte de la cible — le recoller à `Host` fabriquait un hôte qui n'existe pas.
|
|
40
|
+
*
|
|
41
|
+
* Seuls `http` et `https` sont reconnus (RFC 6455 §4.2.1 vaut aussi pour un
|
|
42
|
+
* handshake WebSocket) ; le schéma de la cible ne remplace jamais celui de la
|
|
43
|
+
* connexion. Une autorité porteuse d'identifiants (`user@hôte`, interdite par
|
|
44
|
+
* RFC 9110 §4.2.4) n'est pas reconnue : `Host` garde la main.
|
|
45
|
+
*
|
|
46
|
+
* Chemin de chaque requête : un seul test de caractère pour la forme origine,
|
|
47
|
+
* aucune allocation.
|
|
48
|
+
*
|
|
49
|
+
* @param target - le request-target brut, tel que reçu.
|
|
50
|
+
* @returns l'autorité et la forme origine, ou `null` pour toute autre forme.
|
|
51
|
+
*/
|
|
52
|
+
function splitAbsoluteForm(target) {
|
|
53
|
+
if (target === void 0 || target.charCodeAt(0) === SLASH) return null;
|
|
54
|
+
const sep = target.indexOf("://");
|
|
55
|
+
if (sep !== 4 && sep !== 5) return null;
|
|
56
|
+
const scheme = target.slice(0, sep).toLowerCase();
|
|
57
|
+
if (scheme !== "http" && scheme !== "https") return null;
|
|
58
|
+
const start = sep + 3;
|
|
59
|
+
let end = start;
|
|
60
|
+
while (end < target.length) {
|
|
61
|
+
const code = target.charCodeAt(end);
|
|
62
|
+
if (code === SLASH || code === QMARK || code === HASH) break;
|
|
63
|
+
if (code === AT) return null;
|
|
64
|
+
end++;
|
|
65
|
+
}
|
|
66
|
+
if (end === start) return null;
|
|
67
|
+
const rest = target.slice(end);
|
|
68
|
+
return {
|
|
69
|
+
authority: target.slice(start, end),
|
|
70
|
+
target: rest.charCodeAt(0) === SLASH ? rest : `/${rest}`
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Analyse un request-target origin-form (`/chemin[?query]`) dans la forme
|
|
75
|
+
* NORMALISÉE que la porte HTTP donne au routeur (`HttpRequest`) : découpe exacte
|
|
76
|
+
* quand le parse WHATWG serait l'identité ({@link splitTarget}), vrai parse
|
|
77
|
+
* WHATWG sinon — dot-segments résolus, `\` replié, percent-encoding posé.
|
|
78
|
+
*
|
|
79
|
+
* Pour qui reçoit un chemin hors d'une requête HTTP — le pont WebSocket
|
|
80
|
+
* `api.request` — et doit router exactement comme HTTP : la même chaîne, deux
|
|
81
|
+
* transports, une seule forme.
|
|
82
|
+
*
|
|
83
|
+
* @param target - le chemin reçu ; il doit commencer par `/`.
|
|
84
|
+
* @returns pathname et search normalisés, ou `null` si `target` n'est pas un
|
|
85
|
+
* chemin origin-form analysable.
|
|
86
|
+
*/
|
|
87
|
+
function parseRequestTarget(target) {
|
|
88
|
+
if (target.charCodeAt(0) !== SLASH) return null;
|
|
89
|
+
const fast = splitTargetFast(target);
|
|
90
|
+
if (fast !== null) return fast;
|
|
91
|
+
let url;
|
|
92
|
+
try {
|
|
93
|
+
url = new URL(`${DETACHED_ORIGIN}${target}`);
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
pathname: url.pathname,
|
|
99
|
+
search: url.search
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function splitTargetFast(target) {
|
|
24
103
|
if (typeof target !== "string" || target.length === 0) return null;
|
|
25
104
|
if (target.charCodeAt(0) !== SLASH) return null;
|
|
26
105
|
let prev = 0;
|
|
@@ -123,4 +202,4 @@ function isDigitsFrom(s, from) {
|
|
|
123
202
|
}
|
|
124
203
|
return true;
|
|
125
204
|
}
|
|
126
|
-
export { isCanonicalAuthority, splitTarget };
|
|
205
|
+
export { isCanonicalAuthority, parseRequestTarget, splitAbsoluteForm, splitTarget };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { HOP_BY_HOP } from "../../proxy/forward.js";
|
|
2
|
+
import { Readable } from "node:stream";
|
|
3
|
+
// nodefony/src/context/http/webResponse.ts
|
|
4
|
+
/**
|
|
5
|
+
* En-têtes qu'une réponse RELAYÉE (issue de `fetch`) ne garde pas. Son corps a
|
|
6
|
+
* déjà été DÉCODÉ par `fetch` : `content-encoding` et `content-length`
|
|
7
|
+
* décrivent encore les octets compressés de l'amont, et les recopier
|
|
8
|
+
* annoncerait un corps qui n'est pas celui qu'on envoie. Ses cookies
|
|
9
|
+
* appartiennent à l'amont : les relayer les poserait sous le domaine de
|
|
10
|
+
* l'application.
|
|
11
|
+
*/
|
|
12
|
+
const UPSTREAM_ONLY = /* @__PURE__ */ new Set([
|
|
13
|
+
"content-encoding",
|
|
14
|
+
"content-length",
|
|
15
|
+
"set-cookie"
|
|
16
|
+
]);
|
|
17
|
+
/** Corps d'une `Response` sans corps (`new Response(null, { status: 204 })`). */
|
|
18
|
+
const emptyBody = () => Readable.from([], { objectMode: false });
|
|
19
|
+
/**
|
|
20
|
+
* Traduit une `Response` Web standard en statut, en-têtes et corps pour le flux
|
|
21
|
+
* d'octets — sans la lire : le corps reste un flux.
|
|
22
|
+
*
|
|
23
|
+
* Deux origines, deux règles, que `response.type` distingue :
|
|
24
|
+
* - **construite par l'application** (`new Response(…)`, `Response.json(…)`,
|
|
25
|
+
* `Response.redirect(…)` — `type === "default"`) : ses en-têtes sont les
|
|
26
|
+
* siens, cookies compris (`getSetCookie()`), et partent tels quels ;
|
|
27
|
+
* - **relayée** (issue de `fetch` — `"basic"`, `"cors"`…) : `content-encoding`,
|
|
28
|
+
* `content-length` et `set-cookie` de l'amont sont écartés (voir
|
|
29
|
+
* `UPSTREAM_ONLY`).
|
|
30
|
+
*
|
|
31
|
+
* Les en-têtes de connexion (RFC 9110 §7.6.1 — la liste du relais,
|
|
32
|
+
* `HOP_BY_HOP`) ne partent jamais : ils décrivent un saut, et HTTP/2 les
|
|
33
|
+
* refuse.
|
|
34
|
+
*
|
|
35
|
+
* @param response - une `Response` dont le corps n'a pas été consommé.
|
|
36
|
+
* @returns statut, en-têtes filtrés et corps.
|
|
37
|
+
*/
|
|
38
|
+
function webResponseParts(response) {
|
|
39
|
+
const relayed = response.type !== "default";
|
|
40
|
+
const headers = {};
|
|
41
|
+
for (const [name, value] of response.headers) {
|
|
42
|
+
if (name === "set-cookie" || HOP_BY_HOP.has(name)) continue;
|
|
43
|
+
if (relayed && UPSTREAM_ONLY.has(name)) continue;
|
|
44
|
+
headers[name] = value;
|
|
45
|
+
}
|
|
46
|
+
if (!relayed) {
|
|
47
|
+
const cookies = response.headers.getSetCookie();
|
|
48
|
+
if (cookies.length > 0) headers["set-cookie"] = cookies;
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
status: response.status,
|
|
52
|
+
headers,
|
|
53
|
+
source: response.body ?? emptyBody()
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export { webResponseParts };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import HttpResponse from "../http/Response.js";
|
|
2
|
+
import { closeStreamAfterGrace } from "../../servers/http2StreamGrace.js";
|
|
2
3
|
import { thenMaybe } from "nodefony";
|
|
3
4
|
import http from "node:http";
|
|
4
5
|
import http2 from "node:http2";
|
|
@@ -10,8 +11,9 @@ const emptyHeaders = {
|
|
|
10
11
|
getHeader: () => void 0
|
|
11
12
|
};
|
|
12
13
|
/**
|
|
13
|
-
* Compose les en-têtes d'une réponse HTTP/2 : ceux
|
|
14
|
-
* ceux
|
|
14
|
+
* Compose les en-têtes d'une réponse HTTP/2 : ceux qu'un tiers a écrits sur la
|
|
15
|
+
* réponse Node, puis ceux de la réserve de la réponse, puis ceux passés à
|
|
16
|
+
* `writeHead` — chacun l'emporte sur le précédent.
|
|
15
17
|
*
|
|
16
18
|
* Les clés sont ramenées en MINUSCULES : `getHeaders()` rend `content-type`,
|
|
17
19
|
* un contrôleur passe `Content-Type` — fusionnées telles quelles, les deux
|
|
@@ -27,11 +29,13 @@ const emptyHeaders = {
|
|
|
27
29
|
* @param statusMessage - message de statut, recopié en `x-status-message`
|
|
28
30
|
* @param current - la réponse Node qui porte les en-têtes déjà posés
|
|
29
31
|
* @param extra - en-têtes passés à `writeHead` (objet, ou liste plate clé/valeur)
|
|
32
|
+
* @param stored - la réserve de la réponse, liste plate aux noms en minuscules
|
|
30
33
|
* @returns les en-têtes à transmettre à `respond()`
|
|
31
34
|
*/
|
|
32
|
-
function mergeResponseHeaders(statusMessage, current, extra) {
|
|
35
|
+
function mergeResponseHeaders(statusMessage, current, extra, stored) {
|
|
33
36
|
const merged = { "x-status-message": statusMessage };
|
|
34
37
|
for (const name of current.getHeaderNames()) merged[name] = current.getHeader(name);
|
|
38
|
+
if (stored) for (let i = 0; i + 1 < stored.length; i += 2) merged[String(stored[i])] = stored[i + 1];
|
|
35
39
|
if (Array.isArray(extra)) for (let i = 0; i + 1 < extra.length; i += 2) merged[String(extra[i]).toLowerCase()] = extra[i + 1];
|
|
36
40
|
else if (extra) for (const key in extra) {
|
|
37
41
|
const value = extra[key];
|
|
@@ -66,15 +70,13 @@ var Http2Response = class extends HttpResponse {
|
|
|
66
70
|
if (this.statusCode > 599) this.statusCode = 500;
|
|
67
71
|
}
|
|
68
72
|
this.statusMessage = this.getStatusMessage();
|
|
73
|
+
this.putCorrelationHeaders();
|
|
69
74
|
this.setLength();
|
|
70
75
|
this.ensureContentTypeHeader();
|
|
71
|
-
this.
|
|
76
|
+
this.applyHeaderPolicy(this.contentTypeForPolicy());
|
|
77
|
+
this.headers = mergeResponseHeaders(this.statusMessage, this.response ?? emptyHeaders, headers, this.headerList());
|
|
72
78
|
this.headers[HTTP2_HEADER_STATUS] = this.statusCode;
|
|
73
|
-
|
|
74
|
-
if (requestId) this.headers["x-request-id"] = requestId;
|
|
75
|
-
const traceparent = this.context.traceparent;
|
|
76
|
-
if (traceparent) this.headers["traceparent"] = traceparent;
|
|
77
|
-
this.stream.respond(this.headers, { endStream: false });
|
|
79
|
+
this.stream.respond(this.headers, { endStream: this.headers["grpc-status"] !== void 0 });
|
|
78
80
|
} else this.log("Headers already sent !!", "WARNING");
|
|
79
81
|
} else return super.writeHead(statusCode, headers);
|
|
80
82
|
}
|
|
@@ -126,8 +128,9 @@ var Http2Response = class extends HttpResponse {
|
|
|
126
128
|
return super.end(chunk, encoding);
|
|
127
129
|
}
|
|
128
130
|
/**
|
|
129
|
-
* Termine la réponse de compatibilité d'un HEAD,
|
|
130
|
-
*
|
|
131
|
+
* Termine la réponse de compatibilité d'un HEAD, ou d'une réponse gRPC
|
|
132
|
+
* _Trailers-Only_ (statut gRPC dans les en-têtes), dont le flux s'est clos
|
|
133
|
+
* au `respond()`.
|
|
131
134
|
*
|
|
132
135
|
* Sans ce geste, `Http2ServerResponse` n'émet ni `finish` ni `close` : le
|
|
133
136
|
* démontage de la requête, branché sur ce `close` (ligne d'accès, profil,
|
|
@@ -138,7 +141,7 @@ var Http2Response = class extends HttpResponse {
|
|
|
138
141
|
*/
|
|
139
142
|
settleHeadResponse() {
|
|
140
143
|
const raw = this.response;
|
|
141
|
-
if (raw !== null && this.context.method === "HEAD" && this.stream?.destroyed === false && !raw.writableEnded) raw.end();
|
|
144
|
+
if (raw !== null && (this.context.method === "HEAD" || this.headers["grpc-status"] !== void 0) && this.stream?.destroyed === false && !raw.writableEnded) raw.end();
|
|
142
145
|
}
|
|
143
146
|
getStatusMessage(code) {
|
|
144
147
|
if (code) {
|
|
@@ -153,5 +156,45 @@ var Http2Response = class extends HttpResponse {
|
|
|
153
156
|
message: ""
|
|
154
157
|
};
|
|
155
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* Règle le sort d'un corps non lu quand la réponse part avant qu'il soit
|
|
161
|
+
* consommé (408 délai, 413 trop gros, refus précoce) : les octets en tampon
|
|
162
|
+
* sont jetés, puis — une fois la réponse écrite — le flux est fermé en
|
|
163
|
+
* `NO_ERROR` si le client ÉMET ENCORE.
|
|
164
|
+
*
|
|
165
|
+
* RFC 9113 §8.1 : après une réponse complète, le serveur PEUT demander au
|
|
166
|
+
* client d'arrêter d'émettre par un `RST_STREAM` `NO_ERROR`. `node:http2` ne
|
|
167
|
+
* le fait pas : le flux resterait ouvert tant que le client émet, et un
|
|
168
|
+
* client lent garderait sa place de flux sur la session. Attendre `finish`
|
|
169
|
+
* (fin d'écriture) garantit que la réponse elle-même n'est pas tronquée.
|
|
170
|
+
*
|
|
171
|
+
* « Émet encore » se décide après une GRÂCE d'un aller-retour
|
|
172
|
+
* ({@link closeStreamAfterGrace} : un `PING` par session pour tous les
|
|
173
|
+
* flux qui attendent, puis l'état du flux). Fini → le flux se ferme seul,
|
|
174
|
+
* sans `RST_STREAM` ; sinon — client qui émet encore, ou muet — fermé.
|
|
175
|
+
* Jamais de coupe avant la grâce : un `RST_STREAM` soumis pendant que la
|
|
176
|
+
* réponse attend dans la file de nghttp2 la fait jeter, et le client reçoit
|
|
177
|
+
* une fin de flux sans statut. grpc-go, Go `net/http2` et nginx coupent AUSSITÔT : Node, lui,
|
|
178
|
+
* répond avant de lire les trames DATA déjà en route, et la coupe
|
|
179
|
+
* immédiate frappait des clients qui finissaient d'émettre — le client
|
|
180
|
+
* HTTP/2 de Go la lit en erreur (ADR-0026 D13 point 1, constaté par la
|
|
181
|
+
* suite de conformité de Connect). `request.complete` ne dit rien ici : il
|
|
182
|
+
* ne passe à `true` qu'une fois le corps CONSOMMÉ.
|
|
183
|
+
*
|
|
184
|
+
* Chemin d'erreur seulement : un `PING` par aller-retour et par session,
|
|
185
|
+
* quel que soit le nombre de refus en attente.
|
|
186
|
+
*
|
|
187
|
+
* Sans effet si le corps est entièrement consommé, ou le flux déjà fermé.
|
|
188
|
+
* Peut s'appeler avant comme après l'écriture de la réponse.
|
|
189
|
+
*/
|
|
190
|
+
closeUnfinishedRequest() {
|
|
191
|
+
const stream = this.stream;
|
|
192
|
+
if (stream === null || stream.closed || stream.destroyed) return;
|
|
193
|
+
const request = this.context.request.request;
|
|
194
|
+
if (request.complete) return;
|
|
195
|
+
request.resume();
|
|
196
|
+
if (stream.writableFinished) closeStreamAfterGrace(stream);
|
|
197
|
+
else stream.once("finish", () => closeStreamAfterGrace(stream));
|
|
198
|
+
}
|
|
156
199
|
};
|
|
157
200
|
export { Http2Response as default, mergeResponseHeaders };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { rpcOutcomeOf } from "./rpcOutcome.js";
|
|
1
2
|
import diagnosticsChannel from "node:diagnostics_channel";
|
|
2
3
|
// nodefony/src/context/requestTrace.ts
|
|
3
4
|
/**
|
|
@@ -46,7 +47,8 @@ function openRequestTrace(context, protocol) {
|
|
|
46
47
|
module: null,
|
|
47
48
|
statusCode: null,
|
|
48
49
|
durationMs: null,
|
|
49
|
-
error: null
|
|
50
|
+
error: null,
|
|
51
|
+
rpc: null
|
|
50
52
|
};
|
|
51
53
|
context.requestTrace = trace;
|
|
52
54
|
return trace;
|
|
@@ -82,8 +84,9 @@ function runTraced(trace, fn, thisArg, ...args) {
|
|
|
82
84
|
* @param context - le contexte de la requête
|
|
83
85
|
* @param statusCode - le statut rendu au client, `null` s'il n'y en a pas eu
|
|
84
86
|
* @param error - l'exception attrapée par le pipeline, `null` ou `undefined` sinon
|
|
87
|
+
* @param rpc - l'appel RPC de la requête, `null` hors RPC : copié, jamais publié tel quel
|
|
85
88
|
*/
|
|
86
|
-
function closeRequestTrace(context, statusCode, error) {
|
|
89
|
+
function closeRequestTrace(context, statusCode, error, rpc = null) {
|
|
87
90
|
const trace = context.requestTrace;
|
|
88
91
|
if (trace === null) return;
|
|
89
92
|
context.requestTrace = null;
|
|
@@ -93,6 +96,7 @@ function closeRequestTrace(context, statusCode, error) {
|
|
|
93
96
|
trace.routePath = path === void 0 ? null : path === "" ? "/" : path;
|
|
94
97
|
trace.module = context.logModule ?? route?.module?.name ?? null;
|
|
95
98
|
trace.statusCode = statusCode;
|
|
99
|
+
trace.rpc = rpcOutcomeOf(rpc);
|
|
96
100
|
trace.durationMs = context.stopClock();
|
|
97
101
|
if (error !== null && error !== void 0) {
|
|
98
102
|
trace.error = error;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// nodefony/src/context/rpcOutcome.ts
|
|
2
|
+
/**
|
|
3
|
+
* Copie le résultat d'un appel RPC — les quatre champs, jamais la fiche
|
|
4
|
+
* d'appel entière, qui porte le contrôleur et le contexte (ADR-0026 D10). La
|
|
5
|
+
* forme est fixe : un objet par appel, alloué seulement par qui publie (audit
|
|
6
|
+
* JSON, canal de traçage avec abonné).
|
|
7
|
+
*
|
|
8
|
+
* @param call - l'appel RPC de la requête, `null` hors RPC.
|
|
9
|
+
* @returns sa copie, `null` hors RPC.
|
|
10
|
+
*/
|
|
11
|
+
function rpcOutcomeOf(call) {
|
|
12
|
+
if (call === null) return null;
|
|
13
|
+
return {
|
|
14
|
+
system: call.system,
|
|
15
|
+
method: call.method,
|
|
16
|
+
code: call.code,
|
|
17
|
+
codeName: call.codeName
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export { rpcOutcomeOf };
|
|
@@ -42,4 +42,23 @@ function alpnProtocolOf(request) {
|
|
|
42
42
|
function requestAuthority(headers) {
|
|
43
43
|
return headers[":authority"] ?? headers.host;
|
|
44
44
|
}
|
|
45
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Une requête HTTP/2 nomme-t-elle deux hôtes — `:authority` ET un `Host`
|
|
47
|
+
* différent ? RFC 9113 §8.3.1 : « SHOULD treat a request as malformed ». Le
|
|
48
|
+
* serveur lit `:authority` ({@link requestAuthority}), mais un code qui lit
|
|
49
|
+
* `headers.host` verrait un autre hôte que celui que le routage et le
|
|
50
|
+
* pare-feu ont jugé.
|
|
51
|
+
*
|
|
52
|
+
* Chemin de chaque requête : une lecture d'en-tête ; la comparaison n'a lieu
|
|
53
|
+
* que si les deux sont présents (un navigateur n'envoie pas `Host` en HTTP/2).
|
|
54
|
+
*
|
|
55
|
+
* @param headers - en-têtes de la requête Node.
|
|
56
|
+
* @returns `true` quand les deux autorités diffèrent (casse ignorée).
|
|
57
|
+
*/
|
|
58
|
+
function hasConflictingAuthority(headers) {
|
|
59
|
+
const authority = headers[":authority"];
|
|
60
|
+
if (authority === void 0) return false;
|
|
61
|
+
const host = headers.host;
|
|
62
|
+
return host !== void 0 && host.toLowerCase() !== authority.toLowerCase();
|
|
63
|
+
}
|
|
64
|
+
export { alpnProtocolOf, hasConflictingAuthority, protocolNameOf, requestAuthority };
|
|
@@ -194,13 +194,23 @@ var WebsocketResponse = class {
|
|
|
194
194
|
handshakeHeaders = null;
|
|
195
195
|
/**
|
|
196
196
|
* Retient un en-tête du handshake (cf {@link handshakeHeaders}) ; sans effet
|
|
197
|
-
* une fois la socket ouverte, où il n'y a plus de réponse HTTP
|
|
197
|
+
* une fois la socket ouverte, où il n'y a plus de réponse HTTP — sauf
|
|
198
|
+
* `Retry-After` posé par une action invoquée par le pont `api.request` : il
|
|
199
|
+
* va au puits de CETTE invocation, et le pont le sert avec le refus. Le même
|
|
200
|
+
* code de contrôleur dit donc « réessaie dans N s » sur les deux transports.
|
|
201
|
+
* Seul cet en-tête passe : un `Set-Cookie` ou un `WWW-Authenticate` n'a
|
|
202
|
+
* aucun sens sur une trame, et rien d'autre ne doit fuir par là.
|
|
198
203
|
*
|
|
199
204
|
* @param name - nom de l'en-tête
|
|
200
205
|
* @param value - valeur
|
|
201
206
|
* @returns toujours `true`
|
|
202
207
|
*/
|
|
203
208
|
setHeader(name, value) {
|
|
209
|
+
const sink = RequestContext.get()?.renderSink;
|
|
210
|
+
if (sink) {
|
|
211
|
+
if (name.toLowerCase() === "retry-after" && typeof value !== "object") sink.retryAfter = String(value);
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
204
214
|
if (this.connection !== null) return true;
|
|
205
215
|
this.handshakeHeaders ??= {};
|
|
206
216
|
this.handshakeHeaders[name] = typeof value === "object" ? value.join(", ") : value;
|
|
@@ -215,8 +225,10 @@ var WebsocketResponse = class {
|
|
|
215
225
|
return true;
|
|
216
226
|
}
|
|
217
227
|
addCookie(cookie) {
|
|
218
|
-
if (cookie instanceof Cookie)
|
|
219
|
-
|
|
228
|
+
if (cookie instanceof Cookie) {
|
|
229
|
+
cookie.assertGrammar();
|
|
230
|
+
this.cookies[cookie.name] = cookie;
|
|
231
|
+
} else throw new Error("Response addCookies not valid cookies");
|
|
220
232
|
}
|
|
221
233
|
setCookies() {}
|
|
222
234
|
setCookie(_cookie) {}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { hasForwardingHeaders, resolveForwarded } from "../forwarded.js";
|
|
2
|
-
import { closeRequestTrace } from "../requestTrace.js";
|
|
3
1
|
import Context from "../Context.js";
|
|
4
|
-
import { toWsCloseReason } from "./wsCloseReason.js";
|
|
5
2
|
import WebsocketResponse from "./Response.js";
|
|
3
|
+
import { toWsCloseReason } from "./wsCloseReason.js";
|
|
6
4
|
import { sanitizeRequestId } from "../requestId.js";
|
|
7
5
|
import { FrameProfile } from "../../profiler/FrameProfile.js";
|
|
8
|
-
import { formatWsLogContent } from "./wsLogContent.js";
|
|
6
|
+
import { WS_LOG_CONTENT_CAP, formatWsLogContent } from "./wsLogContent.js";
|
|
7
|
+
import { admitFrame, readFrameRateBound, readFrameRateCloseAfter } from "./wsFrameRate.js";
|
|
8
|
+
import { closeRequestTrace } from "../requestTrace.js";
|
|
9
|
+
import { hasForwardingHeaders, resolveForwarded } from "../forwarded.js";
|
|
9
10
|
import { parseWebsocketUrl } from "./websocketUrl.js";
|
|
10
|
-
import { RequestContext, frameRequestId, logColor } from "nodefony";
|
|
11
|
+
import { IDENTITY_DEAD_CLOSE_CODE, NOT_FOUND_CLOSE_CODE, ORIGIN_REFUSED_CLOSE_CODE, RequestContext, frameRequestId, logColor } from "nodefony";
|
|
11
12
|
import { AsyncResource } from "node:async_hooks";
|
|
12
13
|
import url, { URL } from "node:url";
|
|
13
14
|
import Ws from "ws";
|
|
@@ -51,7 +52,7 @@ function toWsCloseCode(code) {
|
|
|
51
52
|
if (code === 1e3 || code >= 1001 && code <= 1003 || code >= 1007 && code <= 1011 || code >= 3e3 && code <= 4999) return code;
|
|
52
53
|
if (code >= 500 && code < 600) return 1011;
|
|
53
54
|
if (code === 401 || code === 403 || code === 421) return 1008;
|
|
54
|
-
if (code >= 400 && code < 500) return
|
|
55
|
+
if (code >= 400 && code < 500) return NOT_FOUND_CLOSE_CODE;
|
|
55
56
|
return 1011;
|
|
56
57
|
}
|
|
57
58
|
/**
|
|
@@ -72,11 +73,11 @@ function toHandshakeStatus(code) {
|
|
|
72
73
|
case 1003:
|
|
73
74
|
case 1007: return 400;
|
|
74
75
|
case 1008:
|
|
75
|
-
case
|
|
76
|
-
case
|
|
76
|
+
case ORIGIN_REFUSED_CLOSE_CODE: return 403;
|
|
77
|
+
case IDENTITY_DEAD_CLOSE_CODE: return 401;
|
|
77
78
|
case 1009: return 413;
|
|
78
79
|
case 1013: return 503;
|
|
79
|
-
case
|
|
80
|
+
case NOT_FOUND_CLOSE_CODE: return 404;
|
|
80
81
|
default: return 500;
|
|
81
82
|
}
|
|
82
83
|
}
|
|
@@ -106,15 +107,27 @@ function handshakeRefusalStatus(context, error) {
|
|
|
106
107
|
* le gate court-circuite AVANT toute allocation/concat (0 surcoût hot path).
|
|
107
108
|
*/
|
|
108
109
|
let wsContentLogging = null;
|
|
109
|
-
var WebsocketContext = class extends Context {
|
|
110
|
+
var WebsocketContext = class WebsocketContext extends Context {
|
|
110
111
|
request;
|
|
111
112
|
response = null;
|
|
112
113
|
acceptedProtocol;
|
|
114
|
+
/**
|
|
115
|
+
* Codec des trames BINAIRES de cette connexion, posé par la couche qui l'a négocié
|
|
116
|
+
* (le temps réel, ADR-0022 §2). Hors production, le traceur s'en sert pour
|
|
117
|
+
* afficher une trame binaire décodée plutôt que sa seule taille ; jamais lu en
|
|
118
|
+
* production. `null` = octets bruts, résumés à leur taille.
|
|
119
|
+
*/
|
|
120
|
+
frameCodec = null;
|
|
113
121
|
port;
|
|
114
122
|
rejected = false;
|
|
115
123
|
teardownWired = false;
|
|
116
124
|
connection = null;
|
|
117
125
|
/**
|
|
126
|
+
* Fenêtre de la borne de débit entrante — `null` tant que la borne est
|
|
127
|
+
* désactivée ou qu'aucune trame n'est arrivée (créée au premier usage).
|
|
128
|
+
*/
|
|
129
|
+
frameRate = null;
|
|
130
|
+
/**
|
|
118
131
|
* Le verdict du handshake encore à rendre : posé par le noyau tant que le
|
|
119
132
|
* pipeline décide (pare-feu, garde de l'action), `null` dès que la socket
|
|
120
133
|
* est acceptée ou refusée. Tant qu'il est là, AUCUN `101` n'est parti.
|
|
@@ -271,7 +284,7 @@ var WebsocketContext = class extends Context {
|
|
|
271
284
|
}
|
|
272
285
|
});
|
|
273
286
|
} else if (!this.isRejected()) {
|
|
274
|
-
this.reject(
|
|
287
|
+
this.reject(NOT_FOUND_CLOSE_CODE, "Not Found");
|
|
275
288
|
this.rejected = true;
|
|
276
289
|
this.webSocketState = "error";
|
|
277
290
|
}
|
|
@@ -284,7 +297,7 @@ var WebsocketContext = class extends Context {
|
|
|
284
297
|
}
|
|
285
298
|
async send(data, encoding) {
|
|
286
299
|
if (this.response) {
|
|
287
|
-
const payload = data ?? this.response
|
|
300
|
+
const payload = data ?? WebsocketContext.takeRenderedBody(this.response);
|
|
288
301
|
const sink = RequestContext.get()?.renderSink;
|
|
289
302
|
if (sink) {
|
|
290
303
|
sink.body = payload;
|
|
@@ -297,9 +310,21 @@ var WebsocketContext = class extends Context {
|
|
|
297
310
|
}
|
|
298
311
|
throw new Error("No response found");
|
|
299
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* Rend le corps rendu (`setBody`) en le RETIRANT de la réponse : une trame
|
|
315
|
+
* envoyée n'a plus à vivre aussi longtemps que la connexion.
|
|
316
|
+
*
|
|
317
|
+
* @param response - la réponse de ce contexte
|
|
318
|
+
* @returns le corps rendu, ou `null` s'il n'y en a pas
|
|
319
|
+
*/
|
|
320
|
+
static takeRenderedBody(response) {
|
|
321
|
+
const body = response.body;
|
|
322
|
+
response.body = null;
|
|
323
|
+
return body;
|
|
324
|
+
}
|
|
300
325
|
broadcast(data, encoding) {
|
|
301
326
|
if (this.response) {
|
|
302
|
-
const payload = data ?? this.response
|
|
327
|
+
const payload = data ?? WebsocketContext.takeRenderedBody(this.response);
|
|
303
328
|
if (payload) {
|
|
304
329
|
this.logMessageContent("BROADCAST", payload);
|
|
305
330
|
this.fire("onMessage", payload, this, "BROADCAST");
|
|
@@ -311,6 +336,28 @@ var WebsocketContext = class extends Context {
|
|
|
311
336
|
return null;
|
|
312
337
|
}
|
|
313
338
|
/**
|
|
339
|
+
* Le contenu des messages est-il journalisé ? Hors production seulement —
|
|
340
|
+
* décidé une fois pour le processus (P8 : runtime ∈ {development, production},
|
|
341
|
+
* `resolveRuntimeEnv`).
|
|
342
|
+
*/
|
|
343
|
+
tracesMessageContent() {
|
|
344
|
+
wsContentLogging ??= this.kernel?.environment !== "production";
|
|
345
|
+
return wsContentLogging;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Traceur des trames SORTANTES écrites hors de {@link send} — le temps réel
|
|
349
|
+
* écrit droit sur la socket. À demander UNE fois par connexion : `null` en
|
|
350
|
+
* production, donc l'appelant choisit son chemin d'écriture à l'ouverture et
|
|
351
|
+
* ne paie rien par trame.
|
|
352
|
+
*
|
|
353
|
+
* @returns une fonction qui journalise une trame comme `send` (`WS SEND`),
|
|
354
|
+
* ou `null` quand le contenu des messages n'est pas journalisé.
|
|
355
|
+
*/
|
|
356
|
+
outboundTracer() {
|
|
357
|
+
if (!this.tracesMessageContent()) return null;
|
|
358
|
+
return (data) => this.logMessageContent("SEND", data);
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
314
361
|
* Logge le CONTENU d'un message WS (corrélé `requestId` via l'override `log`),
|
|
315
362
|
* gaté hors prod et borné — le Suivi de requête (Studio) le surface alors par
|
|
316
363
|
* direction. Hot path : le gate booléen court-circuite en prod AVANT toute
|
|
@@ -320,9 +367,8 @@ var WebsocketContext = class extends Context {
|
|
|
320
367
|
* @param data - charge utile (string, Buffer binaire, objet, ou null).
|
|
321
368
|
*/
|
|
322
369
|
logMessageContent(dir, data) {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
this.log(formatWsLogContent(data), "DEBUG", `WS ${dir}`);
|
|
370
|
+
if (!this.tracesMessageContent()) return;
|
|
371
|
+
this.log(formatWsLogContent(data, WS_LOG_CONTENT_CAP, this.frameCodec), "DEBUG", `WS ${dir}`);
|
|
326
372
|
}
|
|
327
373
|
/**
|
|
328
374
|
* Ouvre le profil d'**une invocation** du pont RPC (une frame `api.request`).
|
|
@@ -390,11 +436,33 @@ var WebsocketContext = class extends Context {
|
|
|
390
436
|
}
|
|
391
437
|
super.phaseEnd(name);
|
|
392
438
|
}
|
|
439
|
+
/**
|
|
440
|
+
* Applique la borne de débit entrante à une trame : `true` = l'admettre.
|
|
441
|
+
*
|
|
442
|
+
* Écartée → comptée (`frameRateRejectedTotal`) ; solde de refus atteint →
|
|
443
|
+
* connexion fermée en `1008` (RFC 6455 §7.4.1, violation de politique).
|
|
444
|
+
*
|
|
445
|
+
* @param bound - trames admises par seconde (`> 0`)
|
|
446
|
+
* @returns `true` si la trame poursuit son chemin
|
|
447
|
+
*/
|
|
448
|
+
admitInboundFrame(bound) {
|
|
449
|
+
const now = Date.now();
|
|
450
|
+
this.frameRate ??= {
|
|
451
|
+
start: now,
|
|
452
|
+
count: 0,
|
|
453
|
+
balance: 0
|
|
454
|
+
};
|
|
455
|
+
const verdict = admitFrame(this.frameRate, now, bound, readFrameRateCloseAfter(this.server));
|
|
456
|
+
if (verdict === "send") return true;
|
|
457
|
+
if (verdict === "close") this.connection?.close(1008, "frame rate");
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
393
460
|
async handleMessage(data, isBinary) {
|
|
461
|
+
const bound = readFrameRateBound(this.server);
|
|
462
|
+
if (bound > 0 && !this.admitInboundFrame(bound)) return void 0;
|
|
394
463
|
this.webSocketState = "message";
|
|
395
464
|
const message = isBinary ? data : data.toString();
|
|
396
465
|
this.logMessageContent("RECEIVE", message);
|
|
397
|
-
if (this.response) this.response.body = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
398
466
|
try {
|
|
399
467
|
if (!this.resolver) this.resolver = this.router?.resolve(this) ?? null;
|
|
400
468
|
else this.resolver.match(this.resolver.route, this);
|
|
@@ -406,7 +474,7 @@ var WebsocketContext = class extends Context {
|
|
|
406
474
|
} } });
|
|
407
475
|
return callAsPromise(this.resolver, [message]);
|
|
408
476
|
} else if (!this.rejected) {
|
|
409
|
-
this.reject(
|
|
477
|
+
this.reject(NOT_FOUND_CLOSE_CODE, "Not Found");
|
|
410
478
|
this.rejected = true;
|
|
411
479
|
}
|
|
412
480
|
return;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { splitAbsoluteForm } from "../http/urlFastPath.js";
|
|
1
2
|
// nodefony/src/context/websocket/websocketUrl.ts
|
|
2
3
|
/**
|
|
3
4
|
* URL d'une requête d'upgrade WebSocket, telle que le pipeline la lit.
|
|
@@ -8,6 +9,10 @@
|
|
|
8
9
|
* un hôte de l'autre — et une route acceptée par l'une serait refusée par
|
|
9
10
|
* l'autre.
|
|
10
11
|
*
|
|
12
|
+
* Une cible en forme absolue (`http://hôte/chemin`, RFC 6455 §4.2.1) donne son
|
|
13
|
+
* hôte, et l'en-tête `Host` est ignoré (RFC 9112 §3.2.2) — la même règle que
|
|
14
|
+
* `HttpRequest.getHost`.
|
|
15
|
+
*
|
|
11
16
|
* @param scheme - `ws` ou `wss`
|
|
12
17
|
* @param host - en-tête `Host` de la requête (`localhost` s'il manque)
|
|
13
18
|
* @param rawUrl - cible brute de la requête (`/` si elle manque)
|
|
@@ -15,6 +20,8 @@
|
|
|
15
20
|
* @throws TypeError si l'hôte ou la cible ne forment pas une URL
|
|
16
21
|
*/
|
|
17
22
|
function parseWebsocketUrl(scheme, host, rawUrl) {
|
|
23
|
+
const absolute = splitAbsoluteForm(rawUrl);
|
|
24
|
+
if (absolute !== null) return new URL(`${scheme}://${absolute.authority}${absolute.target}`);
|
|
18
25
|
return new URL(`${scheme}://${host ?? "localhost"}${rawUrl ?? "/"}`);
|
|
19
26
|
}
|
|
20
27
|
export { parseWebsocketUrl };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TRY_AGAIN_LATER_CLOSE_CODE, decideBackpressure, nextDropBalance } from "nodefony";
|
|
1
2
|
// nodefony/src/context/websocket/wsBackpressure.ts
|
|
2
3
|
/**
|
|
3
4
|
* Relit le seuil + la politique depuis les options du `WebSocketServer`.
|
|
@@ -29,27 +30,26 @@ const readBackpressureOptions = (wss) => {
|
|
|
29
30
|
* « Try Again Later » → le client peut back-off + reconnecter). NE FAIT PAS le `send()`
|
|
30
31
|
* — le caller émet uniquement si le retour vaut `"send"`.
|
|
31
32
|
*
|
|
33
|
+
* La DÉCISION est celle du cœur (`decideBackpressure` / `nextDropBalance`, partagée
|
|
34
|
+
* avec `NodefonySocket`) ; cette fonction n'en porte que l'état (compteurs sur la
|
|
35
|
+
* socket) et le geste serveur (fermeture `1013`).
|
|
36
|
+
*
|
|
32
37
|
* @param ws - la socket destinataire
|
|
33
38
|
* @param max - seuil d'octets (`<= 0` → désactivé)
|
|
34
39
|
* @param policy - action au dépassement
|
|
40
|
+
* @param closeAfterDrops - solde de refus qui ferme (`0` = jamais)
|
|
35
41
|
* @returns `"send"` (émettre) · `"drop"` (sauter cette frame) · `"close"` (socket fermée)
|
|
36
42
|
*/
|
|
37
43
|
const decideSend = (ws, max, policy, closeAfterDrops = 0) => {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
sock._nfDrops = (sock._nfDrops ?? 0) + 1;
|
|
44
|
-
const streak = sock._nfDropStreak = (sock._nfDropStreak ?? 0) + 1;
|
|
45
|
-
if (policy === "close") {
|
|
46
|
-
ws.close(1013, "backpressure");
|
|
47
|
-
return "close";
|
|
48
|
-
}
|
|
49
|
-
if (closeAfterDrops > 0 && streak >= closeAfterDrops) {
|
|
50
|
-
ws.close(1013, "backpressure");
|
|
51
|
-
return "close";
|
|
44
|
+
const balance = ws._nfDropStreak ?? 0;
|
|
45
|
+
const verdict = decideBackpressure(ws.bufferedAmount ?? 0, max, policy, closeAfterDrops, balance);
|
|
46
|
+
if (verdict === "send") {
|
|
47
|
+
if (balance > 0) ws._nfDropStreak = balance - 1;
|
|
48
|
+
return verdict;
|
|
52
49
|
}
|
|
53
|
-
|
|
50
|
+
ws._nfDrops = (ws._nfDrops ?? 0) + 1;
|
|
51
|
+
ws._nfDropStreak = nextDropBalance(verdict, balance);
|
|
52
|
+
if (verdict === "close") ws.close(TRY_AGAIN_LATER_CLOSE_CODE, "backpressure");
|
|
53
|
+
return verdict;
|
|
54
54
|
};
|
|
55
55
|
export { decideSend, readBackpressureOptions };
|