@nodefony/http 10.0.0-beta.6 → 10.0.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_virtual/{_@oxc-project_runtime@0.157.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.157.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +3 -2
- package/dist/nodefony/config/config.js +25 -2
- package/dist/nodefony/grpc/GrpcCall.js +298 -0
- package/dist/nodefony/grpc/GrpcEndpoint.js +528 -0
- package/dist/nodefony/grpc/index.js +4 -0
- package/dist/nodefony/interfaces/IRpcCall.js +1 -0
- package/dist/nodefony/service/audit-logger.js +52 -6
- package/dist/nodefony/service/error-renderer.js +41 -9
- package/dist/nodefony/service/http-kernel.js +198 -19
- package/dist/nodefony/service/pretty-request-logger.js +5 -3
- package/dist/nodefony/service/request-logger.js +21 -6
- package/dist/nodefony/service/reverse-proxy.js +1 -1
- package/dist/nodefony/service/servers/clientError.js +26 -1
- package/dist/nodefony/service/servers/server-h2c.js +3 -3
- package/dist/nodefony/service/servers/server-http.js +3 -3
- package/dist/nodefony/service/servers/server-https.js +4 -4
- package/dist/nodefony/service/servers/server-websocket-secure.js +3 -3
- package/dist/nodefony/service/servers/server-websocket.js +3 -3
- package/dist/nodefony/service/servers/serverShutdown.js +14 -36
- package/dist/nodefony/service/sessions/sessions-service.js +6 -6
- package/dist/nodefony/service/upload/upload-service.js +2 -2
- package/dist/nodefony/src/context/Context.js +12 -1
- package/dist/nodefony/src/context/http/ByteStream.js +9 -4
- package/dist/nodefony/src/context/http/HttpContext.js +86 -8
- package/dist/nodefony/src/context/http/Request.js +96 -16
- package/dist/nodefony/src/context/http/Response.js +120 -17
- package/dist/nodefony/src/context/http/SseStream.js +4 -4
- package/dist/nodefony/src/context/http/StreamLifecycle.js +328 -19
- package/dist/nodefony/src/context/http/parser.js +29 -11
- package/dist/nodefony/src/context/http/webResponse.js +56 -0
- package/dist/nodefony/src/context/http2/Response.js +47 -8
- package/dist/nodefony/src/context/requestTrace.js +6 -2
- package/dist/nodefony/src/context/rpcOutcome.js +20 -0
- package/dist/nodefony/src/context/websocket/Response.js +11 -1
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +15 -4
- package/dist/nodefony/src/cookies/cookie.js +3 -1
- package/dist/nodefony/src/errors/httpError.js +58 -25
- package/dist/nodefony/src/grpc/cors.js +59 -0
- package/dist/nodefony/src/grpc/limits.js +16 -0
- package/dist/nodefony/src/grpc/refusal.js +271 -0
- package/dist/nodefony/src/profiler/aggregate.js +2 -2
- package/dist/nodefony/src/proxy/forward.js +1 -1
- package/dist/nodefony/src/proxy/generateProxyConfig.js +91 -15
- package/dist/nodefony/src/servers/http2Goaway.js +76 -0
- package/dist/nodefony/src/servers/http2Limits.js +41 -15
- package/dist/nodefony/src/servers/http2StreamGrace.js +100 -0
- package/dist/nodefony/src/servers/receiveDeadline.js +177 -0
- package/dist/types/index.d.ts +5 -2
- package/dist/types/nodefony/config/config.d.ts +10 -0
- package/dist/types/nodefony/grpc/GrpcCall.d.ts +142 -0
- package/dist/types/nodefony/grpc/GrpcEndpoint.d.ts +98 -0
- package/dist/types/nodefony/grpc/index.d.ts +15 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +9 -0
- package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +14 -0
- package/dist/types/nodefony/interfaces/IRequest.d.ts +2 -1
- package/dist/types/nodefony/interfaces/IRequestTrace.d.ts +10 -0
- package/dist/types/nodefony/interfaces/IRouting.d.ts +19 -0
- package/dist/types/nodefony/interfaces/IRpcCall.d.ts +83 -0
- package/dist/types/nodefony/interfaces/IUpload.d.ts +51 -4
- package/dist/types/nodefony/interfaces/index.d.ts +2 -1
- package/dist/types/nodefony/service/audit-logger.d.ts +33 -3
- package/dist/types/nodefony/service/error-renderer.d.ts +22 -1
- package/dist/types/nodefony/service/http-kernel.d.ts +21 -1
- package/dist/types/nodefony/service/request-logger.d.ts +10 -0
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +2 -2
- package/dist/types/nodefony/src/context/Context.d.ts +9 -1
- package/dist/types/nodefony/src/context/http/ByteStream.d.ts +1 -1
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +56 -2
- package/dist/types/nodefony/src/context/http/Request.d.ts +38 -8
- package/dist/types/nodefony/src/context/http/Response.d.ts +61 -7
- package/dist/types/nodefony/src/context/http/SseStream.d.ts +1 -1
- package/dist/types/nodefony/src/context/http/StreamLifecycle.d.ts +53 -0
- package/dist/types/nodefony/src/context/http/parser.d.ts +17 -3
- package/dist/types/nodefony/src/context/http/webResponse.d.ts +31 -0
- package/dist/types/nodefony/src/context/http2/Response.d.ts +35 -2
- package/dist/types/nodefony/src/context/requestTrace.d.ts +3 -1
- package/dist/types/nodefony/src/context/rpcOutcome.d.ts +11 -0
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +6 -1
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +8 -0
- package/dist/types/nodefony/src/errors/httpError.d.ts +29 -4
- package/dist/types/nodefony/src/grpc/cors.d.ts +32 -0
- package/dist/types/nodefony/src/grpc/limits.d.ts +14 -0
- package/dist/types/nodefony/src/grpc/refusal.d.ts +136 -0
- package/dist/types/nodefony/src/proxy/forward.d.ts +7 -0
- package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +15 -2
- package/dist/types/nodefony/src/servers/http2Goaway.d.ts +41 -0
- package/dist/types/nodefony/src/servers/http2Limits.d.ts +27 -14
- package/dist/types/nodefony/src/servers/http2StreamGrace.d.ts +34 -0
- package/dist/types/nodefony/src/servers/receiveDeadline.d.ts +77 -0
- package/docs/byte-stream.md +52 -17
- package/docs/diagnostics-channel.md +45 -1
- package/docs/servers.md +57 -44
- package/docs/upload.md +43 -17
- package/package.json +26 -6
|
@@ -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
|
};
|
|
@@ -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";
|
|
@@ -69,16 +70,13 @@ var Http2Response = class extends HttpResponse {
|
|
|
69
70
|
if (this.statusCode > 599) this.statusCode = 500;
|
|
70
71
|
}
|
|
71
72
|
this.statusMessage = this.getStatusMessage();
|
|
73
|
+
this.putCorrelationHeaders();
|
|
72
74
|
this.setLength();
|
|
73
75
|
this.ensureContentTypeHeader();
|
|
74
76
|
this.applyHeaderPolicy(this.contentTypeForPolicy());
|
|
75
77
|
this.headers = mergeResponseHeaders(this.statusMessage, this.response ?? emptyHeaders, headers, this.headerList());
|
|
76
78
|
this.headers[HTTP2_HEADER_STATUS] = this.statusCode;
|
|
77
|
-
|
|
78
|
-
if (requestId) this.headers["x-request-id"] = requestId;
|
|
79
|
-
const traceparent = this.context.traceparent;
|
|
80
|
-
if (traceparent) this.headers["traceparent"] = traceparent;
|
|
81
|
-
this.stream.respond(this.headers, { endStream: false });
|
|
79
|
+
this.stream.respond(this.headers, { endStream: this.headers["grpc-status"] !== void 0 });
|
|
82
80
|
} else this.log("Headers already sent !!", "WARNING");
|
|
83
81
|
} else return super.writeHead(statusCode, headers);
|
|
84
82
|
}
|
|
@@ -130,8 +128,9 @@ var Http2Response = class extends HttpResponse {
|
|
|
130
128
|
return super.end(chunk, encoding);
|
|
131
129
|
}
|
|
132
130
|
/**
|
|
133
|
-
* Termine la réponse de compatibilité d'un HEAD,
|
|
134
|
-
*
|
|
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()`.
|
|
135
134
|
*
|
|
136
135
|
* Sans ce geste, `Http2ServerResponse` n'émet ni `finish` ni `close` : le
|
|
137
136
|
* démontage de la requête, branché sur ce `close` (ligne d'accès, profil,
|
|
@@ -142,7 +141,7 @@ var Http2Response = class extends HttpResponse {
|
|
|
142
141
|
*/
|
|
143
142
|
settleHeadResponse() {
|
|
144
143
|
const raw = this.response;
|
|
145
|
-
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();
|
|
146
145
|
}
|
|
147
146
|
getStatusMessage(code) {
|
|
148
147
|
if (code) {
|
|
@@ -157,5 +156,45 @@ var Http2Response = class extends HttpResponse {
|
|
|
157
156
|
message: ""
|
|
158
157
|
};
|
|
159
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
|
+
}
|
|
160
199
|
};
|
|
161
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 };
|
|
@@ -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;
|
|
@@ -107,7 +107,7 @@ function handshakeRefusalStatus(context, error) {
|
|
|
107
107
|
* le gate court-circuite AVANT toute allocation/concat (0 surcoût hot path).
|
|
108
108
|
*/
|
|
109
109
|
let wsContentLogging = null;
|
|
110
|
-
var WebsocketContext = class extends Context {
|
|
110
|
+
var WebsocketContext = class WebsocketContext extends Context {
|
|
111
111
|
request;
|
|
112
112
|
response = null;
|
|
113
113
|
acceptedProtocol;
|
|
@@ -297,7 +297,7 @@ var WebsocketContext = class extends Context {
|
|
|
297
297
|
}
|
|
298
298
|
async send(data, encoding) {
|
|
299
299
|
if (this.response) {
|
|
300
|
-
const payload = data ?? this.response
|
|
300
|
+
const payload = data ?? WebsocketContext.takeRenderedBody(this.response);
|
|
301
301
|
const sink = RequestContext.get()?.renderSink;
|
|
302
302
|
if (sink) {
|
|
303
303
|
sink.body = payload;
|
|
@@ -310,9 +310,21 @@ var WebsocketContext = class extends Context {
|
|
|
310
310
|
}
|
|
311
311
|
throw new Error("No response found");
|
|
312
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
|
+
}
|
|
313
325
|
broadcast(data, encoding) {
|
|
314
326
|
if (this.response) {
|
|
315
|
-
const payload = data ?? this.response
|
|
327
|
+
const payload = data ?? WebsocketContext.takeRenderedBody(this.response);
|
|
316
328
|
if (payload) {
|
|
317
329
|
this.logMessageContent("BROADCAST", payload);
|
|
318
330
|
this.fire("onMessage", payload, this, "BROADCAST");
|
|
@@ -451,7 +463,6 @@ var WebsocketContext = class extends Context {
|
|
|
451
463
|
this.webSocketState = "message";
|
|
452
464
|
const message = isBinary ? data : data.toString();
|
|
453
465
|
this.logMessageContent("RECEIVE", message);
|
|
454
|
-
if (this.response) this.response.body = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
455
466
|
try {
|
|
456
467
|
if (!this.resolver) this.resolver = this.router?.resolve(this) ?? null;
|
|
457
468
|
else this.resolver.match(this.resolver.route, this);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { extend } from "nodefony";
|
|
2
|
-
import crypto from "node:crypto";
|
|
3
2
|
import { parseCookie, stringifySetCookie } from "cookie";
|
|
4
3
|
import MS from "ms";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
5
|
// nodefony/src/cookies/cookie.ts
|
|
6
6
|
const encode = encodeURIComponent;
|
|
7
7
|
/**
|
|
@@ -43,6 +43,7 @@ function cookiesParser(context) {
|
|
|
43
43
|
if (cookies) {
|
|
44
44
|
const obj = parseCookie(cookies, RAW_VALUES);
|
|
45
45
|
for (const cookie in obj) {
|
|
46
|
+
if (cookie === "") continue;
|
|
46
47
|
co = new Cookie(cookie, obj[cookie]);
|
|
47
48
|
context.addRequestCookie(co);
|
|
48
49
|
}
|
|
@@ -54,6 +55,7 @@ function cookiesParser(context) {
|
|
|
54
55
|
if (cookieHeader) {
|
|
55
56
|
const parsed = parseCookie(cookieHeader, RAW_VALUES);
|
|
56
57
|
for (const name in parsed) {
|
|
58
|
+
if (name === "") continue;
|
|
57
59
|
co = new Cookie(name, parsed[name]);
|
|
58
60
|
context.addRequestCookie(co);
|
|
59
61
|
}
|
|
@@ -1,41 +1,51 @@
|
|
|
1
1
|
import { clc, nodefonyError } from "nodefony";
|
|
2
2
|
// nodefony/src/errors/httpError.ts
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
/** Références vivantes (circulaires) jamais sérialisées, quel que soit l'environnement. */
|
|
4
|
+
const EXCLUDED_KEYS = /* @__PURE__ */ new Set([
|
|
5
|
+
"context",
|
|
6
|
+
"resolver",
|
|
7
|
+
"container",
|
|
8
|
+
"secure",
|
|
9
|
+
"request",
|
|
10
|
+
"response"
|
|
11
|
+
]);
|
|
12
|
+
/**
|
|
13
|
+
* Sérialise les propriétés propres d'une erreur HTTP, sans jamais LIRE celles
|
|
14
|
+
* qu'on écarte.
|
|
15
|
+
*
|
|
16
|
+
* Ne pas lire compte : `stack` est un accesseur paresseux de V8, et sa première
|
|
17
|
+
* lecture FORMATE la pile (`prepareStackTrace`, plusieurs µs). Lire puis
|
|
18
|
+
* supprimer, c'est payer ce formatage pour le jeter — ce que faisait le rendu
|
|
19
|
+
* d'erreur en production, sur chaque 404.
|
|
20
|
+
*
|
|
21
|
+
* @param error - l'erreur à sérialiser (en pratique un `HttpError`).
|
|
22
|
+
* @param omit - clés à écarter EN PLUS des références vivantes, ou `null`.
|
|
23
|
+
* @returns un objet plat, prêt pour `JSON.stringify`.
|
|
24
|
+
*/
|
|
25
|
+
function serializeHttpError(error, omit = null) {
|
|
26
|
+
const alt = {};
|
|
27
|
+
for (const key of Object.getOwnPropertyNames(error)) {
|
|
28
|
+
if (EXCLUDED_KEYS.has(key) || omit?.has(key)) continue;
|
|
29
|
+
alt[key] = Reflect.get(error, key);
|
|
30
|
+
}
|
|
31
|
+
return alt;
|
|
32
|
+
}
|
|
11
33
|
const jsonHttpError = {
|
|
12
34
|
configurable: true,
|
|
13
35
|
writable: true,
|
|
14
36
|
value() {
|
|
15
|
-
|
|
16
|
-
const storeKey = function(key) {
|
|
17
|
-
if (key in exclude) return;
|
|
18
|
-
alt[key] = this[key];
|
|
19
|
-
};
|
|
20
|
-
Object.getOwnPropertyNames(this).forEach(storeKey, this);
|
|
21
|
-
return alt;
|
|
37
|
+
return serializeHttpError(this);
|
|
22
38
|
}
|
|
23
39
|
};
|
|
24
|
-
var HttpError = class extends nodefonyError {
|
|
25
|
-
context;
|
|
26
|
-
response;
|
|
27
|
-
request;
|
|
40
|
+
var HttpError = class HttpError extends nodefonyError {
|
|
28
41
|
url;
|
|
29
42
|
controller;
|
|
30
43
|
action;
|
|
31
44
|
jsonResponse;
|
|
32
45
|
constructor(message, code, context) {
|
|
33
46
|
super(message, code);
|
|
34
|
-
this.context = context;
|
|
35
47
|
const res = context?.response;
|
|
36
|
-
this.
|
|
37
|
-
this.request = context?.request ?? void 0;
|
|
38
|
-
this.url = this.context?.url;
|
|
48
|
+
this.url = context?.url;
|
|
39
49
|
if (res && code) res.statusCode = code;
|
|
40
50
|
if (!this.message && res) this.message = res.body?.toString() || res.statusMessage || res.getStatusMessage();
|
|
41
51
|
const resolver = context?.resolver;
|
|
@@ -43,8 +53,31 @@ var HttpError = class extends nodefonyError {
|
|
|
43
53
|
this.action = resolver?.actionName ?? void 0;
|
|
44
54
|
if (res) this.jsonResponse = `${res.statusCode} ${res.statusMessage}`.trim();
|
|
45
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Construit un VERDICT de protocole (404, 405, 426…) sans capturer de pile.
|
|
58
|
+
*
|
|
59
|
+
* La pile d'un verdict pointe toujours au même endroit du noyau ou du routeur
|
|
60
|
+
* et ne dit rien de la requête ; la capturer coûte ~3,6 µs à profondeur
|
|
61
|
+
* réelle (`micro-error-path.mjs`), payés par chaque requête d'un scanner.
|
|
62
|
+
* Réservé à ce que le framework DÉCIDE — une panne garde sa pile. `stack` ne
|
|
63
|
+
* porte que l'en-tête (`HttpError: Not Found`).
|
|
64
|
+
*
|
|
65
|
+
* @param message - message du verdict.
|
|
66
|
+
* @param code - statut HTTP (ou code de fermeture WebSocket).
|
|
67
|
+
* @param context - contexte de la requête, comme pour le constructeur.
|
|
68
|
+
* @returns l'erreur, sans pile.
|
|
69
|
+
*/
|
|
70
|
+
static withoutStack(message, code, context) {
|
|
71
|
+
const limit = Error.stackTraceLimit;
|
|
72
|
+
Error.stackTraceLimit = 0;
|
|
73
|
+
try {
|
|
74
|
+
return new HttpError(message, code, context);
|
|
75
|
+
} finally {
|
|
76
|
+
Error.stackTraceLimit = limit;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
46
79
|
toString() {
|
|
47
|
-
if (this.
|
|
80
|
+
if (this.url !== void 0) return `${clc.red(this.message)}
|
|
48
81
|
${clc.blue("Name :")} ${this.name}
|
|
49
82
|
${clc.blue("Type :")} ${this.errorType}
|
|
50
83
|
${clc.green("Controller :")} ${this.controller}
|
|
@@ -65,4 +98,4 @@ var HttpError = class extends nodefonyError {
|
|
|
65
98
|
}
|
|
66
99
|
};
|
|
67
100
|
Object.defineProperty(HttpError.prototype, "toJSON", jsonHttpError);
|
|
68
|
-
export { HttpError as default };
|
|
101
|
+
export { HttpError as default, serializeHttpError };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { hasGrpcRoutes } from "./refusal.js";
|
|
2
|
+
// nodefony/src/grpc/cors.ts
|
|
3
|
+
/**
|
|
4
|
+
* Les listes de Connect (`cors` de `@connectrpc/connect`, `connect-cors.md`),
|
|
5
|
+
* recopiées : `@nodefony/http` principal ne charge jamais Connect, pair
|
|
6
|
+
* facultatif (ADR-0026 D12). La copie est COMPARÉE à l'original par un test
|
|
7
|
+
* (`grpcCors.test.ts`) : une version de Connect qui change ses listes le fait
|
|
8
|
+
* tomber.
|
|
9
|
+
*/
|
|
10
|
+
const GRPC_CORS = Object.freeze({
|
|
11
|
+
allowedMethods: Object.freeze(["POST", "GET"]),
|
|
12
|
+
allowedHeaders: Object.freeze([
|
|
13
|
+
"Content-Type",
|
|
14
|
+
"Connect-Protocol-Version",
|
|
15
|
+
"Connect-Timeout-Ms",
|
|
16
|
+
"Connect-Content-Encoding",
|
|
17
|
+
"Connect-Accept-Encoding",
|
|
18
|
+
"Content-Encoding",
|
|
19
|
+
"Accept-Encoding",
|
|
20
|
+
"Grpc-Message-Type",
|
|
21
|
+
"X-Grpc-Web",
|
|
22
|
+
"X-User-Agent",
|
|
23
|
+
"Grpc-Timeout"
|
|
24
|
+
]),
|
|
25
|
+
exposedHeaders: Object.freeze([
|
|
26
|
+
"Grpc-Status",
|
|
27
|
+
"Grpc-Message",
|
|
28
|
+
"Grpc-Status-Details-Bin",
|
|
29
|
+
"Content-Encoding",
|
|
30
|
+
"Connect-Content-Encoding"
|
|
31
|
+
])
|
|
32
|
+
});
|
|
33
|
+
/**
|
|
34
|
+
* `Access-Control-Expose-Headers` d'une réponse RPC, composé une fois au
|
|
35
|
+
* chargement.
|
|
36
|
+
*/
|
|
37
|
+
const RPC_EXPOSED_HEADERS = GRPC_CORS.exposedHeaders.join(", ");
|
|
38
|
+
/**
|
|
39
|
+
* Les listes CORS que demandent les protocoles RPC — dès qu'un service gRPC
|
|
40
|
+
* est déclaré dans le processus, `null` sinon. Le pare-feu les ajoute à la
|
|
41
|
+
* politique de l'application au démarrage (`onReady`) : le préflight ne
|
|
42
|
+
* connaît pas la route, l'union est la seule voie (ADR-0026 D9 point 2).
|
|
43
|
+
*
|
|
44
|
+
* @returns les listes, ou `null` sans service gRPC déclaré.
|
|
45
|
+
* @experimental Surface gRPC, ADR-0026 D11.
|
|
46
|
+
*/
|
|
47
|
+
function rpcCorsLists() {
|
|
48
|
+
return hasGrpcRoutes() ? GRPC_CORS : null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Les listes de Connect telles que recopiées, sans condition — réservé au test
|
|
52
|
+
* qui les compare à l'original.
|
|
53
|
+
*
|
|
54
|
+
* @internal
|
|
55
|
+
*/
|
|
56
|
+
function grpcCorsCopy() {
|
|
57
|
+
return GRPC_CORS;
|
|
58
|
+
}
|
|
59
|
+
export { RPC_EXPOSED_HEADERS, grpcCorsCopy, rpcCorsLists };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// nodefony/src/grpc/limits.ts
|
|
2
|
+
/**
|
|
3
|
+
* Borne par défaut d'un message gRPC, reçu ou émis : 4 Mio (ADR-0026 D4,
|
|
4
|
+
* points 1 et 2). C'est la borne de réception des implémentations de
|
|
5
|
+
* référence (`grpc-js`, `grpc-go`) ; l'adaptateur Connect, lui, n'en pose
|
|
6
|
+
* aucune (`limit-io.js` : `0xffffffff`). Seule source de cette valeur : le
|
|
7
|
+
* schéma de la section `grpc` et le point d'entrée gRPC la lisent ici.
|
|
8
|
+
*/
|
|
9
|
+
const GRPC_DEFAULT_MAX_MESSAGE_BYTES = 4194304;
|
|
10
|
+
/**
|
|
11
|
+
* Plus grande borne qu'accepte l'adaptateur Connect pour `readMaxBytes` et
|
|
12
|
+
* `writeMaxBytes` (`limit-io.js#validateReadWriteMaxBytes`) : la longueur
|
|
13
|
+
* d'une enveloppe tient sur 32 bits.
|
|
14
|
+
*/
|
|
15
|
+
const GRPC_MAX_MESSAGE_BYTES = 4294967295;
|
|
16
|
+
export { GRPC_DEFAULT_MAX_MESSAGE_BYTES, GRPC_MAX_MESSAGE_BYTES };
|