@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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { asServersConfig, configuredServerPort } from "../../src/servers/kernelServers.js";
|
|
2
2
|
import { startHeartbeat } from "./wsHeartbeat.js";
|
|
3
3
|
import { attachUpgradeDispatch, createNodefonyWebSocketServer } from "../../src/servers/upgradeDispatch.js";
|
|
4
|
-
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.
|
|
5
|
-
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.
|
|
4
|
+
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorateParam.js";
|
|
5
|
+
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorate.js";
|
|
6
6
|
import { Inject, Service } from "nodefony";
|
|
7
7
|
import Ws from "ws";
|
|
8
8
|
// nodefony/service/servers/server-websocket.ts
|
|
@@ -50,7 +50,7 @@ let Websocket = class Websocket extends Service {
|
|
|
50
50
|
this.httpKernel.onWebsocketUpgrade(req, admission, this.type).catch(() => {});
|
|
51
51
|
});
|
|
52
52
|
this.heartbeatTimer = startHeartbeat(this.server, this.options);
|
|
53
|
-
this.
|
|
53
|
+
this.httpKernel.addShutdownStep("dismiss", () => this.terminate());
|
|
54
54
|
this.ready = true;
|
|
55
55
|
this.module.fire("onServersReady", this.type, this);
|
|
56
56
|
return resolve(this.server);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { ANNOUNCE_CAP_MS, announceSessionClose } from "../../src/servers/http2Goaway.js";
|
|
1
2
|
import http from "node:http";
|
|
2
|
-
import { constants } from "node:http2";
|
|
3
3
|
import https from "node:https";
|
|
4
4
|
import tls from "node:tls";
|
|
5
5
|
// nodefony/service/servers/serverShutdown.ts
|
|
@@ -21,8 +21,8 @@ import tls from "node:tls";
|
|
|
21
21
|
*
|
|
22
22
|
* ⚠️ Les sockets WebSocket upgradées (sans réponse HTTP en cours) sont détruites
|
|
23
23
|
* SANS frame Close → les serveurs WS doivent fermer leurs clients (close 1001)
|
|
24
|
-
* AVANT ce drain. Garanti par
|
|
25
|
-
*
|
|
24
|
+
* AVANT ce drain. Garanti par `HttpKernel.drainTraffic` (ADR-0027 D2) : l'étape
|
|
25
|
+
* `dismiss` (1001) est attendue avant l'étape `drain`.
|
|
26
26
|
*
|
|
27
27
|
* @param server - serveur Node à drainer, créé AVANT son `listen` (le suivi des
|
|
28
28
|
* connexions commence ici)
|
|
@@ -84,27 +84,19 @@ function createDrainTerminator(server, shutdownTimeout) {
|
|
|
84
84
|
return terminating;
|
|
85
85
|
} };
|
|
86
86
|
}
|
|
87
|
-
/** Plus long délai d'annonce (ms) : un pair qui ne répond pas au PING ne retient pas l'arrêt. */
|
|
88
|
-
const ANNOUNCE_CAP_MS = 1e3;
|
|
89
|
-
/** Plus grand identifiant de flux (RFC 9113 §6.8) : « aucun flux n'est encore écarté ». */
|
|
90
|
-
const LAST_POSSIBLE_STREAM = 2 ** 31 - 1;
|
|
91
87
|
/**
|
|
92
|
-
* Annonce l'arrêt à chaque session HTTP/2 SANS écarter un flux déjà en route
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* RFC 9113 §6.8 : « SHOULD send an initial GOAWAY frame with the last stream
|
|
98
|
-
* identifier set to 2^31-1 […] After allowing time for any in-flight stream
|
|
99
|
-
* creation (at least one round-trip time) ». Sans annonce, un flux croisé avec
|
|
100
|
-
* le GOAWAY est refusé alors qu'il aurait pu être servi.
|
|
88
|
+
* Annonce l'arrêt à chaque session HTTP/2 SANS écarter un flux déjà en route —
|
|
89
|
+
* l'annonce en deux temps de RFC 9113 §6.8, session par session
|
|
90
|
+
* ({@link announceSessionClose}, la même règle que la fermeture d'une session
|
|
91
|
+
* inactive). Le GOAWAY définitif (`session.close()`) suit dans `done`. Un pair
|
|
92
|
+
* qui ne répond pas au PING est réglé au délai `cap`.
|
|
101
93
|
*
|
|
102
94
|
* @param sessions - sessions HTTP/2 ouvertes, `null` pour un serveur HTTP/1.1
|
|
103
95
|
* @param cap - délai maximal d'attente des échos (ms)
|
|
104
96
|
* @param done - appelé UNE fois, quand tous les échos sont revenus ou au délai
|
|
105
97
|
*/
|
|
106
98
|
function announceShutdown(sessions, cap, done) {
|
|
107
|
-
let pending =
|
|
99
|
+
let pending = 1;
|
|
108
100
|
let finished = false;
|
|
109
101
|
const finish = () => {
|
|
110
102
|
if (finished) return;
|
|
@@ -112,30 +104,16 @@ function announceShutdown(sessions, cap, done) {
|
|
|
112
104
|
clearTimeout(timer);
|
|
113
105
|
done();
|
|
114
106
|
};
|
|
107
|
+
const settle = () => {
|
|
108
|
+
if (--pending === 0) finish();
|
|
109
|
+
};
|
|
115
110
|
const timer = setTimeout(finish, cap);
|
|
116
111
|
if (sessions !== null) for (const session of sessions) {
|
|
117
112
|
if (session.closed || session.destroyed) continue;
|
|
118
|
-
let settled = false;
|
|
119
|
-
let wait = null;
|
|
120
|
-
const settle = () => {
|
|
121
|
-
if (settled) return;
|
|
122
|
-
settled = true;
|
|
123
|
-
if (wait !== null) clearTimeout(wait);
|
|
124
|
-
session.removeListener("close", settle);
|
|
125
|
-
if (--pending === 0) finish();
|
|
126
|
-
};
|
|
127
113
|
pending++;
|
|
128
|
-
session
|
|
129
|
-
if (!session.ping((error, rttMs) => {
|
|
130
|
-
if (error !== null || settled || session.closed) return settle();
|
|
131
|
-
session.goaway(constants.NGHTTP2_NO_ERROR, LAST_POSSIBLE_STREAM);
|
|
132
|
-
wait = setTimeout(settle, Math.max(1, Math.ceil(rttMs)));
|
|
133
|
-
})) {
|
|
134
|
-
session.goaway(constants.NGHTTP2_NO_ERROR, LAST_POSSIBLE_STREAM);
|
|
135
|
-
settle();
|
|
136
|
-
}
|
|
114
|
+
announceSessionClose(session, settle);
|
|
137
115
|
}
|
|
138
|
-
|
|
116
|
+
settle();
|
|
139
117
|
}
|
|
140
118
|
function track(set, socket) {
|
|
141
119
|
set.add(socket);
|
|
@@ -2,8 +2,8 @@ import Session from "../../src/session/session.js";
|
|
|
2
2
|
import MemorySessionStorage from "../../src/session/storage/MemorySessionStorage.js";
|
|
3
3
|
import RevocationGuardStorage from "../../src/session/storage/RevocationGuardStorage.js";
|
|
4
4
|
import { SESSION_FACETS } from "../../src/session/storage/sessionFilters.js";
|
|
5
|
-
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.
|
|
6
|
-
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.
|
|
5
|
+
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorateParam.js";
|
|
6
|
+
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorate.js";
|
|
7
7
|
import { AUTO_STORE, EMPTY_INFRA, GcScheduler, Inject, Nodefony, RequestContext, Service, countFacets, extend, injectable, readStoreConnector, readStoreLocation, resolveAutoStore, storeConnectivity } from "nodefony";
|
|
8
8
|
import { createHash, createHmac } from "node:crypto";
|
|
9
9
|
// nodefony/service/sessions/sessions-service.ts
|
|
@@ -125,10 +125,6 @@ let SessionsService = class SessionsService extends Service {
|
|
|
125
125
|
this.module = module;
|
|
126
126
|
this.certificates = this.get("certificates");
|
|
127
127
|
this.defaultSessionName = this.options.name;
|
|
128
|
-
this.once("onTerminate", () => {
|
|
129
|
-
this.gcScheduler?.stop();
|
|
130
|
-
if (this.storage) this.storage.close();
|
|
131
|
-
});
|
|
132
128
|
}
|
|
133
129
|
async init() {
|
|
134
130
|
this.secret = this.createSecret();
|
|
@@ -184,6 +180,10 @@ let SessionsService = class SessionsService extends Service {
|
|
|
184
180
|
onError: (e) => this.log(e, "WARNING", "SESSION-GC")
|
|
185
181
|
});
|
|
186
182
|
this.gcScheduler.start();
|
|
183
|
+
this.kernel?.once("onTerminate", () => {
|
|
184
|
+
this.gcScheduler?.stop();
|
|
185
|
+
this.storage?.close();
|
|
186
|
+
});
|
|
187
187
|
});
|
|
188
188
|
return this.storage;
|
|
189
189
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorateParam.js";
|
|
2
|
+
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorate.js";
|
|
3
3
|
import { Cli, FileClass, Inject, Service } from "nodefony";
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import fsp from "node:fs/promises";
|
|
@@ -3,9 +3,9 @@ import { protocolNameOf } from "./transport.js";
|
|
|
3
3
|
import { resolveTimingMode } from "./timingMode.js";
|
|
4
4
|
import Cookie, { cookiesParser } from "../cookies/cookie.js";
|
|
5
5
|
import { RequestContext, Service, logColor } from "nodefony";
|
|
6
|
+
import { AsyncResource } from "node:async_hooks";
|
|
6
7
|
import { randomFillSync, randomUUID } from "node:crypto";
|
|
7
8
|
import { performance } from "node:perf_hooks";
|
|
8
|
-
import { AsyncResource } from "node:async_hooks";
|
|
9
9
|
// nodefony/src/context/Context.ts
|
|
10
10
|
const colorLogEvent = () => logColor.cyanBgBlack("EVENT CONTEXT");
|
|
11
11
|
const PERF_PROBE_SUB = process.env.NF_PERF_PROBE === "1";
|
|
@@ -356,6 +356,17 @@ var Context = class extends Service {
|
|
|
356
356
|
holdRequestStore(store) {
|
|
357
357
|
this.requestStore = store;
|
|
358
358
|
}
|
|
359
|
+
/**
|
|
360
|
+
* L'utilisateur authentifié de l'unité de travail, lu sur son magasin et
|
|
361
|
+
* non sur l'ALS : valable là où la bulle n'est plus active (rendu d'une
|
|
362
|
+
* erreur, démontage). Même règle que `RequestContext.getAuthenticatedUser`.
|
|
363
|
+
*
|
|
364
|
+
* @returns l'utilisateur connecté, ou `null` (anonyme, hors zone, nettoyé).
|
|
365
|
+
*/
|
|
366
|
+
authenticatedUser() {
|
|
367
|
+
const store = this.requestStore;
|
|
368
|
+
return store === null ? null : RequestContext.authenticatedUserOf(store);
|
|
369
|
+
}
|
|
359
370
|
clean() {
|
|
360
371
|
this.cleaned = true;
|
|
361
372
|
this.httpKernel = null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import HttpError from "../../errors/httpError.js";
|
|
2
|
-
import "./SseStream.js";
|
|
2
|
+
import { SSE_STALL_TIMEOUT_MS } from "./SseStream.js";
|
|
3
3
|
import { MAX_TIMER_MS, checkStreamTimer, openStreamLifecycle } from "./StreamLifecycle.js";
|
|
4
4
|
import http from "node:http";
|
|
5
5
|
import { AsyncResource } from "node:async_hooks";
|
|
@@ -330,8 +330,9 @@ function openByteStream(context, source, options = {}) {
|
|
|
330
330
|
settings = settingsOf(context, options);
|
|
331
331
|
opened = openStreamLifecycle(context, {
|
|
332
332
|
label: "Flux",
|
|
333
|
-
stallTimeout: options.stallTimeout ??
|
|
334
|
-
maxDuration: options.maxDuration ?? false
|
|
333
|
+
stallTimeout: options.stallTimeout ?? SSE_STALL_TIMEOUT_MS,
|
|
334
|
+
maxDuration: options.maxDuration ?? false,
|
|
335
|
+
endsAtShutdown: settings.live
|
|
335
336
|
});
|
|
336
337
|
} catch (error) {
|
|
337
338
|
readable.on("error", IGNORE_LATE_ERROR);
|
|
@@ -343,7 +344,11 @@ function openByteStream(context, source, options = {}) {
|
|
|
343
344
|
stream.start();
|
|
344
345
|
return stream.done;
|
|
345
346
|
};
|
|
346
|
-
|
|
347
|
+
if (opened instanceof Promise) {
|
|
348
|
+
readable.on("error", IGNORE_LATE_ERROR);
|
|
349
|
+
return opened.then(start);
|
|
350
|
+
}
|
|
351
|
+
return start(opened);
|
|
347
352
|
}
|
|
348
353
|
/** Les réglages du flux, défauts résolus sur le contexte. */
|
|
349
354
|
function settingsOf(context, options) {
|
|
@@ -11,6 +11,7 @@ import { responseEnded } from "../responseEnded.js";
|
|
|
11
11
|
import { alpnProtocolOf } from "../transport.js";
|
|
12
12
|
import { openSseStream } from "./SseStream.js";
|
|
13
13
|
import { openByteStream } from "./ByteStream.js";
|
|
14
|
+
import { webResponseParts } from "./webResponse.js";
|
|
14
15
|
import { finallyMaybe, isPromise, thenMaybe, typeOf } from "nodefony";
|
|
15
16
|
import net from "node:net";
|
|
16
17
|
import url, { URL } from "node:url";
|
|
@@ -33,7 +34,7 @@ function switchUrlScheme(from, scheme, port) {
|
|
|
33
34
|
const socketActiveContext = /* @__PURE__ */ new WeakMap();
|
|
34
35
|
/**
|
|
35
36
|
* Route le délai d'inactivité d'une socket HTTP/1.1 vers la requête qu'elle
|
|
36
|
-
* porte :
|
|
37
|
+
* porte : 503 si une requête est en cours, fermeture si la connexion
|
|
37
38
|
* persistante est inactive. Écouteur `timeout` du SERVEUR — un seul pour
|
|
38
39
|
* toutes ses connexions —, que Node appelle avec la socket en argument
|
|
39
40
|
* (`socketOnTimeout` → `server.emit("timeout", socket)`).
|
|
@@ -49,7 +50,8 @@ const socketActiveContext = /* @__PURE__ */ new WeakMap();
|
|
|
49
50
|
* serveur supprime aussi une fermeture et une entrée de registre par connexion.
|
|
50
51
|
*
|
|
51
52
|
* @param socket - la socket dont le délai a expiré ; une session HTTP/2 (même
|
|
52
|
-
* événement sur un serveur `allowHTTP1`) est
|
|
53
|
+
* événement sur un serveur `allowHTTP1`) est fermée par l'écouteur que pose
|
|
54
|
+
* `setHttp2SessionTimeout` (`src/servers/http2Limits.ts`)
|
|
53
55
|
*/
|
|
54
56
|
function routeSocketTimeout(socket) {
|
|
55
57
|
if (!(socket instanceof net.Socket)) return;
|
|
@@ -72,6 +74,15 @@ var HttpContext = class extends Context {
|
|
|
72
74
|
* son montage. `null` = servie par l'application (cas de toute requête).
|
|
73
75
|
*/
|
|
74
76
|
relayAccessLog = null;
|
|
77
|
+
/**
|
|
78
|
+
* L'appel RPC servi sur cette requête (gRPC, gRPC-Web, Connect), `null`
|
|
79
|
+
* hors appel RPC (ADR-0026 D10) : un champ, aucune allocation. Posé par le
|
|
80
|
+
* point d'entrée gRPC ; à l'échéance du délai de réponse, {@link _onTimeout}
|
|
81
|
+
* le termine au lieu d'écrire un 503.
|
|
82
|
+
*
|
|
83
|
+
* @experimental Surface gRPC, ADR-0026 D11.
|
|
84
|
+
*/
|
|
85
|
+
rpc = null;
|
|
75
86
|
request;
|
|
76
87
|
response;
|
|
77
88
|
uploadService;
|
|
@@ -142,11 +153,34 @@ var HttpContext = class extends Context {
|
|
|
142
153
|
_onTimeout() {
|
|
143
154
|
if (this.listenerCount("onTimeout")) this.fire("onTimeout", this);
|
|
144
155
|
this._abortIfPending("Request timeout");
|
|
156
|
+
if (this.rpc !== null) {
|
|
157
|
+
this.rpc.expire();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
145
160
|
this.httpKernel?.warnUnsentResponse(this);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
161
|
+
this.httpKernel?.onError(new HttpError("Service Unavailable", 503, this), this);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Ré-arme le délai de réponse de la requête à la valeur propre de sa route
|
|
165
|
+
* (`responseTimeout` d'une option de route), une fois celle-ci résolue.
|
|
166
|
+
*
|
|
167
|
+
* Même mécanisme que {@link setTimeout} — délai d'inactivité de la socket
|
|
168
|
+
* (HTTP/1.1) ou du flux (HTTP/2) —, sans second écouteur : sous HTTP/2,
|
|
169
|
+
* `setTimeout(ms)` sans rappel remplace le minuteur et garde celui posé par
|
|
170
|
+
* {@link setTimeout}. `0` désactive le délai pour la requête.
|
|
171
|
+
*
|
|
172
|
+
* @param ms - délai de la route, en millisecondes.
|
|
173
|
+
*/
|
|
174
|
+
rearmTimeout(ms) {
|
|
175
|
+
this.response.setTimeout(ms);
|
|
176
|
+
const res = this.response.response;
|
|
177
|
+
if (!res) return;
|
|
178
|
+
if (this.response.stream) {
|
|
179
|
+
res.setTimeout(ms);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const socket = this.activeSocket;
|
|
183
|
+
if (socket && socket.timeout !== ms) socket.setTimeout(ms);
|
|
150
184
|
}
|
|
151
185
|
setScheme() {
|
|
152
186
|
const req = this.request;
|
|
@@ -173,6 +207,8 @@ var HttpContext = class extends Context {
|
|
|
173
207
|
if (kernel && (kernel.debug || kernel.listenerCount("onRequest") > 0)) kernel.fire("onRequest", this);
|
|
174
208
|
if (!this.resolver && this.router) this.resolver = this.router.resolve(this);
|
|
175
209
|
if (this.resolver?.resolve) {
|
|
210
|
+
const routeTimeout = this.resolver.route?.responseTimeout;
|
|
211
|
+
if (routeTimeout !== void 0 && routeTimeout !== null) this.rearmTimeout(routeTimeout);
|
|
176
212
|
this.setMetaData();
|
|
177
213
|
return this.resolver.callController();
|
|
178
214
|
}
|
|
@@ -199,17 +235,32 @@ var HttpContext = class extends Context {
|
|
|
199
235
|
* réellement rendu (le relais écrit la réponse brute, pas celle du
|
|
200
236
|
* contexte) ; un montage `accessLog: "errors"` ne l'écrit qu'en échec.
|
|
201
237
|
*
|
|
238
|
+
* Le statut natif n'est recopié que si la réponse a été TERMINÉE
|
|
239
|
+
* ({@link responseEnded}) : sinon il n'a jamais atteint le client (200 par
|
|
240
|
+
* défaut de Node), et le statut du contexte — 499 d'un client parti — dit
|
|
241
|
+
* vrai. Règle unique pour toute réponse écrite par un tiers (#695, critère
|
|
242
|
+
* 38).
|
|
243
|
+
*
|
|
202
244
|
* @param httpError - l'erreur de la requête, s'il y en a une
|
|
203
245
|
*/
|
|
204
246
|
logRequest(httpError) {
|
|
247
|
+
if (this.rpc !== null) this.adoptNativeStatus();
|
|
205
248
|
if (this.relayAccessLog !== null) {
|
|
206
|
-
|
|
207
|
-
if (raw !== null) this.response.statusCode = raw.statusCode;
|
|
249
|
+
this.adoptNativeStatus();
|
|
208
250
|
if (this.relayAccessLog === "errors" && this.response.statusCode < 400 && !httpError && !this.error) return;
|
|
209
251
|
}
|
|
210
252
|
return super.logRequest(httpError);
|
|
211
253
|
}
|
|
212
254
|
/**
|
|
255
|
+
* Reprend le statut de la réponse native, écrite par un tiers (relais,
|
|
256
|
+
* adaptateur RPC) — si la réponse est TERMINÉE seulement : sinon ce statut
|
|
257
|
+
* n'a jamais atteint le client, et celui du contexte (499) dit vrai.
|
|
258
|
+
*/
|
|
259
|
+
adoptNativeStatus() {
|
|
260
|
+
const raw = this.response.response;
|
|
261
|
+
if (raw !== null && responseEnded(raw)) this.response.statusCode = raw.statusCode;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
213
264
|
* Nettoie le contexte, et le DÉTACHE de sa socket h1 : la connexion
|
|
214
265
|
* persistante survit à la requête, et l'entrée de `socketActiveContext`
|
|
215
266
|
* retiendrait sinon tout le contexte jusqu'à sa fermeture. Seule l'entrée qui
|
|
@@ -382,6 +433,33 @@ var HttpContext = class extends Context {
|
|
|
382
433
|
openStream(source, options) {
|
|
383
434
|
return openByteStream(this, source, options);
|
|
384
435
|
}
|
|
436
|
+
/**
|
|
437
|
+
* Répond par une `Response` Web standard — celle que rend `fetch()`, ou une
|
|
438
|
+
* réponse construite (`new Response(…)`, `Response.json(…)`) : son statut,
|
|
439
|
+
* ses en-têtes, et son corps EN FLUX, jamais lu d'avance.
|
|
440
|
+
*
|
|
441
|
+
* C'est ce que fait le framework quand une action renvoie une `Response`.
|
|
442
|
+
* Une réponse relayée (issue de `fetch`) perd `content-encoding`,
|
|
443
|
+
* `content-length` et `set-cookie` de l'amont ; une réponse construite garde
|
|
444
|
+
* les siens. Les en-têtes de connexion ne partent jamais. Le reste — arrêt au
|
|
445
|
+
* départ du client, contre-pression, délais, HEAD sans lire le corps — est
|
|
446
|
+
* celui de {@link HttpContext.openStream}.
|
|
447
|
+
*
|
|
448
|
+
* @param response - une `Response` dont le corps n'a pas été consommé.
|
|
449
|
+
* @param options - délais et écouteur de fin ; statut et en-têtes viennent de la réponse.
|
|
450
|
+
* @returns la raison de la fin et le nombre d'octets ; ne rejette jamais.
|
|
451
|
+
* @throws HttpError 502 pour `Response.error()` (statut 0, aucune réponse) ;
|
|
452
|
+
* les cas de {@link HttpContext.openStream} sinon.
|
|
453
|
+
*/
|
|
454
|
+
openWebResponse(response, options) {
|
|
455
|
+
if (response.type === "error") throw new HttpError("Réponse Web en erreur : Response.error() ne porte aucune réponse", 502, this);
|
|
456
|
+
const parts = webResponseParts(response);
|
|
457
|
+
return openByteStream(this, parts.source, {
|
|
458
|
+
...options,
|
|
459
|
+
status: parts.status,
|
|
460
|
+
headers: parts.headers
|
|
461
|
+
});
|
|
462
|
+
}
|
|
385
463
|
flush(chunk, encoding) {
|
|
386
464
|
return this.response.flush(chunk, encoding);
|
|
387
465
|
}
|
|
@@ -23,6 +23,18 @@ const reg = /(.*)[[][\]]$/u;
|
|
|
23
23
|
const unlinkQuiet = (p) => fsp.unlink(p).catch(() => void 0);
|
|
24
24
|
var BodyHandled = class {};
|
|
25
25
|
const BODY_DONE = new BodyHandled();
|
|
26
|
+
/**
|
|
27
|
+
* Une réception multipart EN COURS, interruptible de l'extérieur : `fail` rompt
|
|
28
|
+
* la lecture comme le ferait un dépassement de limite (temporaires nettoyés).
|
|
29
|
+
* Posée dans `request.parser` le temps de la lecture seulement — l'allocation
|
|
30
|
+
* est réservée au multipart, déjà lourd.
|
|
31
|
+
*/
|
|
32
|
+
var MultipartReception = class {
|
|
33
|
+
fail;
|
|
34
|
+
constructor(fail) {
|
|
35
|
+
this.fail = fail;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
26
38
|
const CHARSET_ALIASES = {
|
|
27
39
|
"utf-8": "utf8",
|
|
28
40
|
utf8: "utf8",
|
|
@@ -128,6 +140,9 @@ var HttpRequest = class {
|
|
|
128
140
|
* temporaire — lus par `@UploadedFile()` / `@UploadedFiles()`. Les champs
|
|
129
141
|
* texte du même corps sont dans {@link body}.
|
|
130
142
|
*
|
|
143
|
+
* Ces temporaires sont EFFACÉS à la fin de la requête : pour garder un
|
|
144
|
+
* fichier, l'action le déplace (`moveAsync`) avant de répondre.
|
|
145
|
+
*
|
|
131
146
|
* Lecture seule : une requête sans upload rend un vide partagé et gelé,
|
|
132
147
|
* aucune allocation.
|
|
133
148
|
*/
|
|
@@ -271,6 +286,25 @@ var HttpRequest = class {
|
|
|
271
286
|
}
|
|
272
287
|
}
|
|
273
288
|
/**
|
|
289
|
+
* Lit le plafond du corps et les limites d'envoi tels que les voit CETTE
|
|
290
|
+
* requête : la configuration http, surchargée par le calque que la requête a
|
|
291
|
+
* pu poser avant la lecture du corps (`overlayConfig`, #494), puis par les
|
|
292
|
+
* bornes que la route déclare pour elle-même (option de route `upload`). La
|
|
293
|
+
* route est déjà résolue à ce moment : une route sans bornes propres
|
|
294
|
+
* n'alloue rien. Hors pipeline (aucun scope ouvert), la configuration du
|
|
295
|
+
* module telle quelle.
|
|
296
|
+
*/
|
|
297
|
+
readBodyLimits() {
|
|
298
|
+
const options = RequestContext.getScope() !== void 0 ? useConfig("@nodefony/http") : this.context.httpKernel?.module.options;
|
|
299
|
+
const base = options?.upload ?? {};
|
|
300
|
+
const route = this.context.resolver?.route?.upload;
|
|
301
|
+
this.uploadOption = route ? {
|
|
302
|
+
...base,
|
|
303
|
+
...route
|
|
304
|
+
} : base;
|
|
305
|
+
this.maxBodySize = options?.maxBodySize ?? 0;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
274
308
|
* B1 — Pré-contrôle de taille du corps via l'en-tête `Content-Length` :
|
|
275
309
|
* rejette en 413 (RFC 9110 §15.5.14 « Content Too Large ») AVANT toute lecture
|
|
276
310
|
* quand le client annonce une taille supérieure à `maxBodySize`. Rideau de
|
|
@@ -279,17 +313,6 @@ var HttpRequest = class {
|
|
|
279
313
|
*
|
|
280
314
|
* @throws {HttpError} 413 si `Content-Length` dépasse `maxBodySize`.
|
|
281
315
|
*/
|
|
282
|
-
/**
|
|
283
|
-
* Lit le plafond du corps et les limites d'envoi tels que les voit CETTE
|
|
284
|
-
* requête : la configuration http, surchargée par le calque que la requête a
|
|
285
|
-
* pu poser avant la lecture du corps (`overlayConfig`, #494). Hors pipeline
|
|
286
|
-
* (aucun scope ouvert), la configuration du module telle quelle.
|
|
287
|
-
*/
|
|
288
|
-
readBodyLimits() {
|
|
289
|
-
const options = RequestContext.getScope() !== void 0 ? useConfig("@nodefony/http") : this.context.httpKernel?.module.options;
|
|
290
|
-
this.uploadOption = options?.upload ?? {};
|
|
291
|
-
this.maxBodySize = options?.maxBodySize ?? 0;
|
|
292
|
-
}
|
|
293
316
|
enforceBodyLimit() {
|
|
294
317
|
const max = this.maxBodySize;
|
|
295
318
|
if (max <= 0) return;
|
|
@@ -366,6 +389,7 @@ var HttpRequest = class {
|
|
|
366
389
|
return this.parser;
|
|
367
390
|
}
|
|
368
391
|
const { fields, files } = await this.streamMultipart(bb);
|
|
392
|
+
this.parser = null;
|
|
369
393
|
this.body = fields;
|
|
370
394
|
for (const pf of files) await this.createFileUpload(pf.field, pf.file);
|
|
371
395
|
this.context.requestEnded = true;
|
|
@@ -385,18 +409,33 @@ var HttpRequest = class {
|
|
|
385
409
|
return new Promise((resolve, reject) => {
|
|
386
410
|
const fields = {};
|
|
387
411
|
const files = [];
|
|
412
|
+
let order = 0;
|
|
388
413
|
const pending = [];
|
|
389
414
|
const tempPaths = [];
|
|
390
415
|
const openStreams = /* @__PURE__ */ new Set();
|
|
391
416
|
let totalBytes = 0;
|
|
392
417
|
let aborted = false;
|
|
418
|
+
let received = false;
|
|
393
419
|
const abort = (err) => {
|
|
394
420
|
if (aborted) return;
|
|
395
421
|
aborted = true;
|
|
396
422
|
this.request.unpipe(bb);
|
|
397
423
|
this.request.resume();
|
|
398
|
-
|
|
399
|
-
|
|
424
|
+
const purge = () => {
|
|
425
|
+
Promise.all(tempPaths.map(unlinkQuiet)).finally(() => reject(err));
|
|
426
|
+
};
|
|
427
|
+
let closing = openStreams.size;
|
|
428
|
+
if (closing === 0) {
|
|
429
|
+
purge();
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
for (const ws of openStreams) {
|
|
433
|
+
ws.once("close", () => {
|
|
434
|
+
closing -= 1;
|
|
435
|
+
if (closing === 0) purge();
|
|
436
|
+
});
|
|
437
|
+
ws.destroy();
|
|
438
|
+
}
|
|
400
439
|
};
|
|
401
440
|
bb.on("field", (name, value) => {
|
|
402
441
|
if (aborted) return;
|
|
@@ -407,6 +446,8 @@ var HttpRequest = class {
|
|
|
407
446
|
stream.resume();
|
|
408
447
|
return;
|
|
409
448
|
}
|
|
449
|
+
const slot = order;
|
|
450
|
+
order += 1;
|
|
410
451
|
const field = reg.exec(fieldname)?.[1] ?? fieldname;
|
|
411
452
|
const ext = filename ? path.extname(filename) : "";
|
|
412
453
|
const newFilename = `${randomUUID()}${ext}`;
|
|
@@ -431,7 +472,7 @@ var HttpRequest = class {
|
|
|
431
472
|
ws.on("close", () => {
|
|
432
473
|
openStreams.delete(ws);
|
|
433
474
|
if (aborted) return res();
|
|
434
|
-
files
|
|
475
|
+
files[slot] = {
|
|
435
476
|
field,
|
|
436
477
|
file: {
|
|
437
478
|
filepath,
|
|
@@ -443,7 +484,7 @@ var HttpRequest = class {
|
|
|
443
484
|
hashAlgorithm: algo,
|
|
444
485
|
hash: hasher ? hasher.digest("hex") : null
|
|
445
486
|
}
|
|
446
|
-
}
|
|
487
|
+
};
|
|
447
488
|
res();
|
|
448
489
|
});
|
|
449
490
|
});
|
|
@@ -455,6 +496,7 @@ var HttpRequest = class {
|
|
|
455
496
|
bb.on("partsLimit", () => abort(new HttpError("too many parts", 413, this.context)));
|
|
456
497
|
bb.on("error", (e) => abort(e instanceof Error ? e : new Error(String(e))));
|
|
457
498
|
bb.on("finish", () => {
|
|
499
|
+
received = true;
|
|
458
500
|
if (aborted) return;
|
|
459
501
|
Promise.all(pending).then(() => {
|
|
460
502
|
if (!aborted) resolve({
|
|
@@ -463,10 +505,32 @@ var HttpRequest = class {
|
|
|
463
505
|
});
|
|
464
506
|
}).catch(abort);
|
|
465
507
|
});
|
|
508
|
+
this.parser = new MultipartReception((error) => {
|
|
509
|
+
if (aborted || received) return false;
|
|
510
|
+
abort(error);
|
|
511
|
+
return true;
|
|
512
|
+
});
|
|
466
513
|
this.request.pipe(bb);
|
|
467
514
|
});
|
|
468
515
|
}
|
|
469
516
|
/**
|
|
517
|
+
* Interrompt la réception du corps en cours : la promesse de `initialize()`
|
|
518
|
+
* rejette avec `error`, rendue par le chemin d'erreur habituel — comme un
|
|
519
|
+
* corps qui dépasse sa limite (413).
|
|
520
|
+
*
|
|
521
|
+
* Sert le délai de réception HTTP/2 (`requestTimeout`), que `node:http2` ne
|
|
522
|
+
* borne pas. Sans effet hors réception : corps déjà reçu, lu en flux par
|
|
523
|
+
* l'action (`bodyStream`), ou méthode sans corps.
|
|
524
|
+
*
|
|
525
|
+
* @param error - la cause rendue au client (408 au délai dépassé)
|
|
526
|
+
* @returns vrai si une réception en cours a été interrompue
|
|
527
|
+
*/
|
|
528
|
+
abortBody(error) {
|
|
529
|
+
const parser = this.parser;
|
|
530
|
+
if (parser instanceof Parser || parser instanceof MultipartReception) return parser.fail(error);
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
470
534
|
* Accumule un champ texte multipart. `foo[]` (ou répétition de `foo`) →
|
|
471
535
|
* tableau ; sinon valeur scalaire. (Anciennement délégué à formidable.)
|
|
472
536
|
*/
|
|
@@ -499,9 +563,25 @@ var HttpRequest = class {
|
|
|
499
563
|
}
|
|
500
564
|
async createFileUpload(name, file) {
|
|
501
565
|
const fileUpload = await this.context.uploadService?.createUploadFile(file, name);
|
|
502
|
-
if (fileUpload)
|
|
566
|
+
if (fileUpload) {
|
|
567
|
+
if (this.#uploads === null) {
|
|
568
|
+
this.#uploads = [];
|
|
569
|
+
this.context.onAfterResponse(() => this.#purgeUploads());
|
|
570
|
+
}
|
|
571
|
+
this.#uploads.push(fileUpload);
|
|
572
|
+
}
|
|
503
573
|
return fileUpload;
|
|
504
574
|
}
|
|
575
|
+
/**
|
|
576
|
+
* Efface, une fois la requête terminée (réponse envoyée ou client parti),
|
|
577
|
+
* les temporaires que l'action n'a pas déplacés — la règle de PHP. Un
|
|
578
|
+
* fichier déplacé (`move`/`moveAsync`) n'est plus à son chemin temporaire :
|
|
579
|
+
* l'effacement n'y trouve rien, et l'erreur est ignorée.
|
|
580
|
+
*/
|
|
581
|
+
#purgeUploads() {
|
|
582
|
+
const uploads = this.#uploads ?? NO_FILES;
|
|
583
|
+
return Promise.all(uploads.map((upload) => unlinkQuiet(upload.parsedFile.filepath))).then(() => void 0);
|
|
584
|
+
}
|
|
505
585
|
getMethod() {
|
|
506
586
|
return this.request.method;
|
|
507
587
|
}
|