@nodefony/http 10.0.0-alpha.9 → 10.0.0-beta.1
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 +7 -0
- package/dist/index.js +4 -2
- package/dist/nodefony/command/assetsPublishCommand.js +1 -1
- package/dist/nodefony/command/certificatesCommand.js +1 -1
- package/dist/nodefony/command/networkCommand.js +1 -1
- package/dist/nodefony/command/proxyGenerateCommand.js +1 -1
- package/dist/nodefony/config/config.js +22 -1
- package/dist/nodefony/interfaces/IRouting.js +1 -0
- package/dist/nodefony/interfaces/ISecurity.js +1 -0
- package/dist/nodefony/service/ProfilerAdminApi.js +2 -1
- package/dist/nodefony/service/audit-logger.js +16 -13
- package/dist/nodefony/service/certificates.js +25 -13
- package/dist/nodefony/service/error-renderer.js +11 -8
- package/dist/nodefony/service/http-kernel.js +340 -163
- package/dist/nodefony/service/pretty-request-logger.js +3 -10
- package/dist/nodefony/service/servers/clientError.js +2 -2
- package/dist/nodefony/service/servers/server-http.js +20 -20
- package/dist/nodefony/service/servers/server-https.js +38 -39
- package/dist/nodefony/service/servers/server-static.js +13 -17
- package/dist/nodefony/service/servers/server-websocket-secure.js +8 -7
- package/dist/nodefony/service/servers/server-websocket.js +8 -7
- package/dist/nodefony/service/servers/wsHeartbeat.js +2 -2
- package/dist/nodefony/service/sessions/sessions-service.js +19 -12
- package/dist/nodefony/service/trace.js +1 -1
- package/dist/nodefony/service/upload/upload-service.js +8 -7
- package/dist/nodefony/src/assets/prebuiltUi.js +1 -1
- package/dist/nodefony/src/context/Context.js +34 -19
- package/dist/nodefony/src/context/forwarded.js +4 -4
- package/dist/nodefony/src/context/http/HttpContext.js +133 -88
- package/dist/nodefony/src/context/http/Request.js +93 -44
- package/dist/nodefony/src/context/http/Response.js +107 -51
- package/dist/nodefony/src/context/http/parser.js +9 -10
- package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
- package/dist/nodefony/src/context/http2/Response.js +53 -25
- package/dist/nodefony/src/context/metaData.js +1 -1
- package/dist/nodefony/src/context/trustProxy.js +4 -3
- package/dist/nodefony/src/context/websocket/Response.js +13 -6
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +59 -26
- package/dist/nodefony/src/context/websocket/wsCloseReason.js +41 -0
- package/dist/nodefony/src/cookies/cookie.js +3 -3
- package/dist/nodefony/src/errors/httpError.js +6 -6
- package/dist/nodefony/src/errors/thrownMessage.js +27 -0
- package/dist/nodefony/src/profiler/FrameProfile.js +4 -3
- package/dist/nodefony/src/profiler/Profiler.js +4 -4
- package/dist/nodefony/src/servers/kernelServers.js +29 -0
- package/dist/nodefony/src/servers/portBinder.js +8 -21
- package/dist/nodefony/src/session/session.js +18 -10
- package/dist/nodefony/src/session/sessionStoreFailure.js +2 -1
- package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +24 -9
- package/dist/types/index.d.ts +16 -0
- package/dist/types/nodefony/config/config.d.ts +22 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +24 -7
- package/dist/types/nodefony/interfaces/ICookie.d.ts +18 -18
- package/dist/types/nodefony/interfaces/IHttpKernel.d.ts +10 -4
- package/dist/types/nodefony/interfaces/IResponse.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IRouting.d.ts +87 -0
- package/dist/types/nodefony/interfaces/ISecurity.d.ts +45 -0
- package/dist/types/nodefony/interfaces/ISession.d.ts +8 -8
- package/dist/types/nodefony/interfaces/IUpload.d.ts +9 -9
- package/dist/types/nodefony/interfaces/index.d.ts +2 -0
- package/dist/types/nodefony/service/audit-logger.d.ts +14 -10
- package/dist/types/nodefony/service/certificates.d.ts +10 -10
- package/dist/types/nodefony/service/http-kernel.d.ts +192 -42
- package/dist/types/nodefony/service/servers/server-http.d.ts +4 -1
- package/dist/types/nodefony/service/servers/server-https.d.ts +4 -1
- package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +4 -1
- package/dist/types/nodefony/service/servers/server-websocket.d.ts +4 -1
- package/dist/types/nodefony/service/sessions/sessions-service.d.ts +6 -2
- package/dist/types/nodefony/service/upload/upload-service.d.ts +1 -1
- package/dist/types/nodefony/src/context/Context.d.ts +21 -10
- package/dist/types/nodefony/src/context/forwarded.d.ts +3 -3
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +75 -18
- package/dist/types/nodefony/src/context/http/Request.d.ts +39 -2
- package/dist/types/nodefony/src/context/http/Response.d.ts +47 -7
- package/dist/types/nodefony/src/context/http/parser.d.ts +1 -1
- package/dist/types/nodefony/src/context/http2/Response.d.ts +32 -2
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +4 -4
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +11 -2
- package/dist/types/nodefony/src/context/websocket/wsCloseReason.d.ts +21 -0
- package/dist/types/nodefony/src/cookies/cookie.d.ts +9 -9
- package/dist/types/nodefony/src/errors/httpError.d.ts +7 -7
- package/dist/types/nodefony/src/errors/thrownMessage.d.ts +12 -0
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +28 -28
- package/dist/types/nodefony/src/servers/kernelServers.d.ts +20 -0
- package/dist/types/nodefony/src/servers/portBinder.d.ts +6 -19
- package/dist/types/nodefony/src/session/session.d.ts +12 -12
- package/dist/types/nodefony/src/session/storage/RevocationGuardStorage.d.ts +1 -1
- package/docs/cookies.md +20 -20
- package/docs/observabilite.md +25 -25
- package/docs/rate-limit.md +18 -18
- package/docs/servers.md +52 -52
- package/docs/session.md +65 -60
- package/docs/upload.md +36 -30
- package/package.json +7 -7
|
@@ -141,7 +141,7 @@ var Context = class extends Service {
|
|
|
141
141
|
scheme;
|
|
142
142
|
webSocketState = null;
|
|
143
143
|
constructor(container, type) {
|
|
144
|
-
super(
|
|
144
|
+
super(type, container);
|
|
145
145
|
if (PERF_PROBE_SUB) {
|
|
146
146
|
const p = globalThis.__nfPerfProbe;
|
|
147
147
|
if (p && p.t0 !== 0n) p.svcNs += Number(process.hrtime.bigint() - p.t0);
|
|
@@ -227,7 +227,7 @@ var Context = class extends Service {
|
|
|
227
227
|
}
|
|
228
228
|
phaseStart(name) {
|
|
229
229
|
if (!this._timingEnabled) return;
|
|
230
|
-
|
|
230
|
+
this._phaseIndex ??= /* @__PURE__ */ new Map();
|
|
231
231
|
const idx = this.phases.length;
|
|
232
232
|
this.phases.push({
|
|
233
233
|
name,
|
|
@@ -239,8 +239,8 @@ var Context = class extends Service {
|
|
|
239
239
|
if (!this._timingEnabled || this._phaseIndex === null) return;
|
|
240
240
|
const idx = this._phaseIndex.get(name);
|
|
241
241
|
if (idx === void 0) return;
|
|
242
|
-
const p = this.phases
|
|
243
|
-
if (p.endMs !== void 0) return;
|
|
242
|
+
const p = this.phases.at(idx);
|
|
243
|
+
if (p === void 0 || p.endMs !== void 0) return;
|
|
244
244
|
p.endMs = performance.now();
|
|
245
245
|
p.durationMs = p.endMs - p.startMs;
|
|
246
246
|
}
|
|
@@ -250,15 +250,28 @@ var Context = class extends Service {
|
|
|
250
250
|
Promise.resolve().then(() => boundFn(this)).catch((e) => this.log(e, "ERROR", "onAfterResponse(late)"));
|
|
251
251
|
return;
|
|
252
252
|
}
|
|
253
|
-
|
|
253
|
+
this._afterResponseFns ??= [];
|
|
254
254
|
this._afterResponseFns.push(boundFn);
|
|
255
255
|
}
|
|
256
|
-
|
|
257
|
-
|
|
256
|
+
/**
|
|
257
|
+
* Exécute les hooks `onAfterResponse`, une seule fois, dans l'ordre
|
|
258
|
+
* d'enregistrement ; un hook qui lève est journalisé sans arrêter les suivants.
|
|
259
|
+
*
|
|
260
|
+
* @returns `undefined` quand aucun hook n'est enregistré (le cas nominal) ou
|
|
261
|
+
* qu'ils ont déjà tourné : rien à attendre, le démontage continue sans
|
|
262
|
+
* suspension. Une `Promise` sinon, réglée quand le dernier hook a fini — elle
|
|
263
|
+
* ne rejette jamais.
|
|
264
|
+
*/
|
|
265
|
+
_runAfterResponse() {
|
|
266
|
+
if (this._afterResponseFired) return void 0;
|
|
258
267
|
this._afterResponseFired = true;
|
|
259
268
|
const fns = this._afterResponseFns;
|
|
260
|
-
if (fns === null || fns.length === 0) return;
|
|
269
|
+
if (fns === null || fns.length === 0) return void 0;
|
|
261
270
|
this._afterResponseFns = null;
|
|
271
|
+
return this.runAfterResponseHooks(fns);
|
|
272
|
+
}
|
|
273
|
+
/** Boucle des hooks `onAfterResponse` — séquentielle, erreurs journalisées. */
|
|
274
|
+
async runAfterResponseHooks(fns) {
|
|
262
275
|
for (const fn of fns) try {
|
|
263
276
|
await fn(this);
|
|
264
277
|
} catch (e) {
|
|
@@ -298,7 +311,7 @@ var Context = class extends Service {
|
|
|
298
311
|
* notable → INFO (EVENT_SEVERITY), event technique → DEBUG.
|
|
299
312
|
*/
|
|
300
313
|
logEvent(event) {
|
|
301
|
-
|
|
314
|
+
lifecycleEventLogging ??= this.kernel?.environment !== "production";
|
|
302
315
|
if (!lifecycleEventLogging) return;
|
|
303
316
|
this.log(`${colorLogEvent()} ${event}`, EVENT_SEVERITY[event] || "DEBUG");
|
|
304
317
|
}
|
|
@@ -327,7 +340,9 @@ var Context = class extends Service {
|
|
|
327
340
|
if (logger.shouldSample && !logger.shouldSample(this, err)) return;
|
|
328
341
|
const entry = logger.renderHttp(this, err);
|
|
329
342
|
return this.log(entry.text, entry.severity, entry.msgid);
|
|
330
|
-
} catch {
|
|
343
|
+
} catch {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
331
346
|
}
|
|
332
347
|
/**
|
|
333
348
|
* P3.7 — Émet au teardown un log DEBUG détaillant la durée de chaque phase du
|
|
@@ -339,18 +354,18 @@ var Context = class extends Service {
|
|
|
339
354
|
logPhasesVerbose() {
|
|
340
355
|
if (!this._timingVerbose) return;
|
|
341
356
|
const phases = this.phases;
|
|
342
|
-
|
|
343
|
-
if (n === 0) return;
|
|
357
|
+
if (phases.length === 0) return;
|
|
344
358
|
let line = "";
|
|
345
359
|
let total = 0;
|
|
346
|
-
|
|
347
|
-
|
|
360
|
+
let i = 0;
|
|
361
|
+
for (const p of phases) {
|
|
348
362
|
const d = p.durationMs ?? (p.endMs !== void 0 ? p.endMs - p.startMs : void 0);
|
|
349
363
|
if (i > 0) line += " · ";
|
|
350
364
|
if (d !== void 0) {
|
|
351
365
|
line += `${p.name}=${d.toFixed(2)}ms`;
|
|
352
366
|
total += d;
|
|
353
367
|
} else line += `${p.name}=…`;
|
|
368
|
+
i++;
|
|
354
369
|
}
|
|
355
370
|
this.log(`TRACE phases [Σ ${total.toFixed(2)}ms] ${line}`, "DEBUG", `${this.type} TIMING`);
|
|
356
371
|
}
|
|
@@ -363,11 +378,12 @@ var Context = class extends Service {
|
|
|
363
378
|
}
|
|
364
379
|
}
|
|
365
380
|
getRequestCookies(name) {
|
|
366
|
-
if (name) return this.cookies[name]
|
|
381
|
+
if (name) return Object.hasOwn(this.cookies, name) ? this.cookies[name] ?? null : null;
|
|
367
382
|
return this.cookies;
|
|
368
383
|
}
|
|
369
384
|
setCookie(cookie) {
|
|
370
|
-
|
|
385
|
+
const received = cookie;
|
|
386
|
+
if (received) return this.response?.addCookie(received);
|
|
371
387
|
}
|
|
372
388
|
getRequest() {
|
|
373
389
|
return this.request;
|
|
@@ -387,8 +403,7 @@ var Context = class extends Service {
|
|
|
387
403
|
return Boolean(this.cookieSession);
|
|
388
404
|
}
|
|
389
405
|
getCookieSession(name) {
|
|
390
|
-
|
|
391
|
-
return null;
|
|
406
|
+
return Object.hasOwn(this.cookies, name) ? this.cookies[name] ?? null : null;
|
|
392
407
|
}
|
|
393
408
|
/**
|
|
394
409
|
* Nom effectif du cookie de session pour CE transport. Sur TLS (https/wss) on
|
|
@@ -401,7 +416,7 @@ var Context = class extends Service {
|
|
|
401
416
|
*/
|
|
402
417
|
getSessionCookieName() {
|
|
403
418
|
const base = this.sessionService?.defaultSessionName ?? "nodefony";
|
|
404
|
-
const mode = (this.sessionService?.options
|
|
419
|
+
const mode = (this.sessionService?.options.cookie)?.hostPrefix ?? "auto";
|
|
405
420
|
const tls = this.scheme === "https" || this.scheme === "wss";
|
|
406
421
|
return mode === true || mode === "auto" && tls ? `__Host-${base}` : base;
|
|
407
422
|
}
|
|
@@ -8,7 +8,7 @@ import { isIPv4 } from "node:net";
|
|
|
8
8
|
* un point de coupe. Les `\"` échappés à l'intérieur des guillemets sont préservés.
|
|
9
9
|
*/
|
|
10
10
|
function splitTopLevel(input, sep) {
|
|
11
|
-
if (input.
|
|
11
|
+
if (!input.includes("\"")) return input.split(sep);
|
|
12
12
|
const out = [];
|
|
13
13
|
let buf = "";
|
|
14
14
|
let inQuote = false;
|
|
@@ -43,7 +43,7 @@ function splitTopLevel(input, sep) {
|
|
|
43
43
|
/** Retire les guillemets d'une `quoted-string` et déséchappe `\x` (RFC 7230 §3.2.6). */
|
|
44
44
|
function unquote(value) {
|
|
45
45
|
const v = value.trim();
|
|
46
|
-
if (v.length >= 2 && v
|
|
46
|
+
if (v.length >= 2 && v.startsWith("\"") && v.endsWith("\"")) return v.slice(1, -1).replace(/\\(.)/gu, "$1");
|
|
47
47
|
return v;
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
@@ -104,11 +104,11 @@ function forwardedNodeIp(node) {
|
|
|
104
104
|
if (!node) return null;
|
|
105
105
|
const n = node.trim();
|
|
106
106
|
if (!n) return null;
|
|
107
|
-
if (n
|
|
107
|
+
if (n.startsWith("[")) {
|
|
108
108
|
const end = n.indexOf("]");
|
|
109
109
|
return end > 1 ? n.slice(1, end) : null;
|
|
110
110
|
}
|
|
111
|
-
if (n === "unknown" || n
|
|
111
|
+
if (n === "unknown" || n.startsWith("_")) return null;
|
|
112
112
|
const colon = n.indexOf(":");
|
|
113
113
|
const host = colon === -1 ? n : n.slice(0, colon);
|
|
114
114
|
return isIPv4(host) ? host : host || null;
|
|
@@ -7,7 +7,7 @@ import HttpResponse from "./Response.js";
|
|
|
7
7
|
import Http2Request from "../http2/Request.js";
|
|
8
8
|
import Http2Response from "../http2/Response.js";
|
|
9
9
|
import { describeSessionStoreFailure } from "../../session/sessionStoreFailure.js";
|
|
10
|
-
import { extend, typeOf } from "nodefony";
|
|
10
|
+
import { extend, finallyMaybe, isPromise, thenMaybe, typeOf } from "nodefony";
|
|
11
11
|
import url, { URL } from "node:url";
|
|
12
12
|
//#region nodefony/src/context/http/HttpContext.ts
|
|
13
13
|
const socketActiveContext = /* @__PURE__ */ new WeakMap();
|
|
@@ -20,7 +20,6 @@ var HttpContext = class extends Context {
|
|
|
20
20
|
request;
|
|
21
21
|
response;
|
|
22
22
|
uploadService;
|
|
23
|
-
csrf;
|
|
24
23
|
constructor(container, request, response, type) {
|
|
25
24
|
super(container, type);
|
|
26
25
|
this.uploadService = this.get("upload");
|
|
@@ -97,18 +96,28 @@ var HttpContext = class extends Context {
|
|
|
97
96
|
if (req.hasParsedUrl) return req.url.protocol.replace(":", "");
|
|
98
97
|
return req.scheme;
|
|
99
98
|
}
|
|
100
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Sert la requête : délai de réponse, hooks `onRequest`, puis l'action
|
|
101
|
+
* résolue et son rendu (`resolver.callController()`).
|
|
102
|
+
*
|
|
103
|
+
* Pas `async` : synchrone quand l'action et son rendu le sont, rien n'y est
|
|
104
|
+
* alors attendu (#505).
|
|
105
|
+
*
|
|
106
|
+
* @returns ce que rend `callController()` — le résultat du rendu, pas le
|
|
107
|
+
* contexte (comportement historique, conservé) —, ou sa promesse. Sur une
|
|
108
|
+
* redirection déjà posée : le contexte, une fois l'envoi fait.
|
|
109
|
+
* @throws HttpError 404, de façon SYNCHRONE, quand aucune route ne résout.
|
|
110
|
+
*/
|
|
111
|
+
handle() {
|
|
101
112
|
this.setTimeout();
|
|
102
|
-
if (this.isRedirect)
|
|
103
|
-
await this.send();
|
|
104
|
-
return this;
|
|
105
|
-
}
|
|
113
|
+
if (this.isRedirect) return thenMaybe(this.send(), () => this);
|
|
106
114
|
this.fire("onRequest", this);
|
|
107
|
-
this.kernel
|
|
115
|
+
const kernel = this.kernel;
|
|
116
|
+
if (kernel && (kernel.debug || kernel.listenerCount("onRequest") > 0)) kernel.fire("onRequest", this);
|
|
108
117
|
if (!this.resolver && this.router) this.resolver = this.router.resolve(this);
|
|
109
|
-
if (this.resolver
|
|
118
|
+
if (this.resolver?.resolve) {
|
|
110
119
|
this.setMetaData();
|
|
111
|
-
return
|
|
120
|
+
return this.resolver.callController();
|
|
112
121
|
}
|
|
113
122
|
throw new HttpError("", 404, this);
|
|
114
123
|
}
|
|
@@ -117,7 +126,7 @@ var HttpContext = class extends Context {
|
|
|
117
126
|
if (!res) return;
|
|
118
127
|
if (this.response.stream) {
|
|
119
128
|
res.setTimeout(this.response.timeout, () => {
|
|
120
|
-
if (!this.response
|
|
129
|
+
if (!this.response.response?.writableEnded) this._onTimeout();
|
|
121
130
|
});
|
|
122
131
|
return;
|
|
123
132
|
}
|
|
@@ -130,27 +139,28 @@ var HttpContext = class extends Context {
|
|
|
130
139
|
socketTimeoutArmed.add(socket);
|
|
131
140
|
socket.on("timeout", () => {
|
|
132
141
|
const ctx = socketActiveContext.get(socket);
|
|
133
|
-
if (ctx && !ctx.response
|
|
142
|
+
if (ctx && !ctx.response.response?.writableEnded) ctx._onTimeout();
|
|
134
143
|
});
|
|
135
144
|
}
|
|
136
145
|
}
|
|
137
|
-
|
|
146
|
+
/**
|
|
147
|
+
* Sérialise la donnée selon son type (JSON, HTML, texte), pose statut et
|
|
148
|
+
* en-têtes, puis l'envoie ({@link send}). Synchrone quand l'envoi l'est (#505).
|
|
149
|
+
*
|
|
150
|
+
* @returns la réponse, ou sa promesse.
|
|
151
|
+
* @throws de façon SYNCHRONE ce que lèvent la sérialisation ou l'envoi.
|
|
152
|
+
*/
|
|
153
|
+
render(chunk, encoding, status, headers) {
|
|
138
154
|
let data = chunk;
|
|
139
|
-
|
|
140
|
-
|
|
155
|
+
if (this.isJson) data = JSON.stringify(chunk);
|
|
156
|
+
else {
|
|
157
|
+
const type = typeOf(chunk);
|
|
158
|
+
if (type === "object") {
|
|
159
|
+
this.setContextJson();
|
|
141
160
|
data = JSON.stringify(chunk);
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
case "object":
|
|
146
|
-
this.setContextJson();
|
|
147
|
-
data = JSON.stringify(chunk);
|
|
148
|
-
break;
|
|
149
|
-
case "string":
|
|
150
|
-
if (this.response.contentType === "application/octet-stream") this.setContextHtml();
|
|
151
|
-
break;
|
|
152
|
-
default: if (this.response.contentType === "application/octet-stream") this.response.setContentType("text");
|
|
153
|
-
}
|
|
161
|
+
} else if (type === "string") {
|
|
162
|
+
if (this.response.contentType === "application/octet-stream") this.setContextHtml();
|
|
163
|
+
} else if (this.response.contentType === "application/octet-stream") this.response.setContentType("text");
|
|
154
164
|
}
|
|
155
165
|
if (headers) this.response.setHeaders(headers);
|
|
156
166
|
if (status) this.response.setStatusCode(status);
|
|
@@ -161,83 +171,118 @@ var HttpContext = class extends Context {
|
|
|
161
171
|
this.log(describeSessionStoreFailure(e).message, "CRITIC", "SESSION-STORE");
|
|
162
172
|
return null;
|
|
163
173
|
}).then(async (_session) => {
|
|
164
|
-
return this.close()
|
|
165
|
-
throw e;
|
|
166
|
-
});
|
|
174
|
+
return this.close();
|
|
167
175
|
});
|
|
168
176
|
}
|
|
169
|
-
|
|
177
|
+
/**
|
|
178
|
+
* Envoie la réponse : session sauvée, hook `onSend`, en-têtes, corps, puis
|
|
179
|
+
* fin de la réponse.
|
|
180
|
+
*
|
|
181
|
+
* Synchrone dans le cas nominal — aucune session ouverte, aucun écouteur
|
|
182
|
+
* `onSend`, réponse unique : rien n'y est à attendre (#505). Une promesse
|
|
183
|
+
* seulement quand une étape attend réellement (écriture du store de session,
|
|
184
|
+
* hook asynchrone, streaming chunké, flux HTTP/2).
|
|
185
|
+
*
|
|
186
|
+
* @param chunk - le corps ; absent, le corps déjà posé sur la réponse.
|
|
187
|
+
* @param encoding - l'encodage d'un corps texte.
|
|
188
|
+
* @returns la réponse, ou sa promesse.
|
|
189
|
+
* @throws de façon SYNCHRONE `Response Already sended` quand la réponse est
|
|
190
|
+
* déjà partie, et ce que lève une étape synchrone de l'envoi.
|
|
191
|
+
*/
|
|
192
|
+
send(chunk, encoding) {
|
|
170
193
|
if (this.finished && !this.sended) {
|
|
171
194
|
this.log("send() on finished context — client disconnected", "DEBUG");
|
|
172
195
|
return this.response;
|
|
173
196
|
}
|
|
174
|
-
if (this.sended || this.finished || this.response.isHeaderSent())
|
|
175
|
-
|
|
176
|
-
});
|
|
177
|
-
if (!this.timingEnabled) return this.#doSend(chunk, encoding);
|
|
197
|
+
if (this.sended || this.finished || this.response.isHeaderSent()) throw new Error("Response Already sended");
|
|
198
|
+
if (!this.timingEnabled) return this.doSend(chunk, encoding);
|
|
178
199
|
this.phaseStart("send");
|
|
179
|
-
|
|
180
|
-
return await this.#doSend(chunk, encoding);
|
|
181
|
-
} finally {
|
|
182
|
-
this.phaseEnd("send");
|
|
183
|
-
}
|
|
200
|
+
return finallyMaybe(() => this.doSend(chunk, encoding), () => this.phaseEnd("send"));
|
|
184
201
|
}
|
|
185
|
-
|
|
202
|
+
/**
|
|
203
|
+
* Le travail de {@link send}, journalisé en ERROR quand il échoue — de façon
|
|
204
|
+
* synchrone ou par rejet, selon l'étape qui échoue.
|
|
205
|
+
*/
|
|
206
|
+
doSend(chunk, encoding) {
|
|
207
|
+
let sent;
|
|
186
208
|
try {
|
|
187
|
-
|
|
188
|
-
if (this.session != null) try {
|
|
189
|
-
await this.saveSession();
|
|
190
|
-
} catch (e) {
|
|
191
|
-
const failure = describeSessionStoreFailure(e);
|
|
192
|
-
this.log(failure.message, "CRITIC", "SESSION-STORE");
|
|
193
|
-
this.response.statusCode = failure.statusCode;
|
|
194
|
-
body = failure.body;
|
|
195
|
-
}
|
|
196
|
-
if (body) this.response?.setBody(body);
|
|
197
|
-
if (this.listenerCount("onSend") > 0) await this.fireAsync("onSend", this.response, this);
|
|
198
|
-
try {
|
|
199
|
-
this.writeHead();
|
|
200
|
-
} catch (e) {
|
|
201
|
-
this.log(e, "WARNING");
|
|
202
|
-
}
|
|
203
|
-
if (this.isRedirect) return await this.close();
|
|
204
|
-
return await this.write(body, encoding);
|
|
209
|
+
sent = this.sendSteps(chunk, encoding);
|
|
205
210
|
} catch (error) {
|
|
206
211
|
this.log(error, "ERROR");
|
|
207
212
|
throw error;
|
|
208
213
|
}
|
|
214
|
+
if (!isPromise(sent)) return sent;
|
|
215
|
+
return sent.catch((error) => {
|
|
216
|
+
this.log(error, "ERROR");
|
|
217
|
+
throw error;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
/** Session, puis hook `onSend`, puis en-têtes et corps — chacun attendu seulement s'il attend. */
|
|
221
|
+
sendSteps(chunk, encoding) {
|
|
222
|
+
if (this.session != null) return this.saveSession().then(() => this.emitAndWrite(chunk, encoding), (e) => {
|
|
223
|
+
const failure = describeSessionStoreFailure(e);
|
|
224
|
+
this.log(failure.message, "CRITIC", "SESSION-STORE");
|
|
225
|
+
this.response.statusCode = failure.statusCode;
|
|
226
|
+
return this.emitAndWrite(failure.body, encoding);
|
|
227
|
+
});
|
|
228
|
+
return this.emitAndWrite(chunk, encoding);
|
|
229
|
+
}
|
|
230
|
+
/** Hook `onSend` (attendu seulement s'il a un écouteur), puis en-têtes et corps. */
|
|
231
|
+
emitAndWrite(body, encoding) {
|
|
232
|
+
if (body) this.response.setBody(body);
|
|
233
|
+
if (this.listenerCount("onSend") > 0) return this.fireAsync("onSend", this.response, this).then(() => this.writeHeadAndBody(body, encoding));
|
|
234
|
+
return this.writeHeadAndBody(body, encoding);
|
|
235
|
+
}
|
|
236
|
+
/** En-têtes, puis corps — ou la seule fin de réponse pour une redirection. */
|
|
237
|
+
writeHeadAndBody(body, encoding) {
|
|
238
|
+
try {
|
|
239
|
+
this.writeHead();
|
|
240
|
+
} catch (e) {
|
|
241
|
+
this.log(e, "WARNING");
|
|
242
|
+
}
|
|
243
|
+
if (this.isRedirect) return this.close();
|
|
244
|
+
return this.write(body, encoding);
|
|
209
245
|
}
|
|
210
246
|
writeHead(statusCode, headers) {
|
|
211
|
-
if (this.
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
this.response.writeHead(statusCode, headers);
|
|
220
|
-
}
|
|
247
|
+
if (this.csrfToken) this.setCookie(new Cookie("csrf-token", this.csrfToken, {
|
|
248
|
+
httpOnly: false,
|
|
249
|
+
sameSite: "Strict",
|
|
250
|
+
secure: this.scheme === "https",
|
|
251
|
+
path: "/"
|
|
252
|
+
}));
|
|
253
|
+
this.response.setCookies();
|
|
254
|
+
this.response.writeHead(statusCode, headers);
|
|
221
255
|
}
|
|
222
|
-
|
|
223
|
-
|
|
256
|
+
/**
|
|
257
|
+
* Écrit le corps, puis termine la requête ({@link close}). Synchrone pour une
|
|
258
|
+
* réponse unique (#505).
|
|
259
|
+
*
|
|
260
|
+
* @returns la réponse, ou sa promesse quand l'écriture attend.
|
|
261
|
+
*/
|
|
262
|
+
write(chunk, encoding, _flush = false) {
|
|
263
|
+
return thenMaybe(this.response.send(chunk, encoding || this.response.encoding), () => {
|
|
224
264
|
this.sended = true;
|
|
225
|
-
|
|
226
|
-
throw e;
|
|
227
|
-
});
|
|
228
|
-
return this.close().catch((e) => {
|
|
229
|
-
throw e;
|
|
265
|
+
return this.close();
|
|
230
266
|
});
|
|
231
267
|
}
|
|
232
268
|
flush(chunk, encoding) {
|
|
233
269
|
return this.response.flush(chunk, encoding);
|
|
234
270
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
271
|
+
/**
|
|
272
|
+
* Termine la requête : hook `onClose`, puis fin de la réponse si l'envoi ne
|
|
273
|
+
* l'a pas déjà faite. Synchrone sans écouteur `onClose` (#505).
|
|
274
|
+
*
|
|
275
|
+
* @returns la réponse, ou sa promesse quand le hook attend.
|
|
276
|
+
*/
|
|
277
|
+
close() {
|
|
278
|
+
if (this.finished) return this.response;
|
|
279
|
+
if (this.listenerCount("onClose") > 0) return this.fireAsync("onClose", this).then(() => this.endResponse());
|
|
280
|
+
return this.endResponse();
|
|
281
|
+
}
|
|
282
|
+
/** Fin de la réponse, sauf si l'envoi l'a déjà terminée. */
|
|
283
|
+
endResponse() {
|
|
284
|
+
if (this.response.response?.writableEnded) return this.response;
|
|
285
|
+
return thenMaybe(this.response.end(), () => this.response);
|
|
241
286
|
}
|
|
242
287
|
redirect(Url, status, headers) {
|
|
243
288
|
if (typeof Url === "object") return this.response.redirect(url.format(Url), status, headers);
|
|
@@ -280,25 +325,25 @@ var HttpContext = class extends Context {
|
|
|
280
325
|
return this.redirect(newUrl, status, headers);
|
|
281
326
|
}
|
|
282
327
|
getHostName() {
|
|
283
|
-
return this.request
|
|
328
|
+
return this.request.getHostName();
|
|
284
329
|
}
|
|
285
330
|
getRemoteAddress() {
|
|
286
|
-
return this.request
|
|
331
|
+
return this.request.getRemoteAddress();
|
|
287
332
|
}
|
|
288
333
|
getHost() {
|
|
289
|
-
return this.request
|
|
334
|
+
return this.request.getHost();
|
|
290
335
|
}
|
|
291
336
|
getUserAgent() {
|
|
292
|
-
return this.request
|
|
337
|
+
return this.request.getUserAgent();
|
|
293
338
|
}
|
|
294
339
|
getMethod() {
|
|
295
|
-
return this.request
|
|
340
|
+
return this.request.getMethod();
|
|
296
341
|
}
|
|
297
342
|
setContentType(type, encoding) {
|
|
298
343
|
return this.response.setContentType(type, encoding);
|
|
299
344
|
}
|
|
300
345
|
resolveIsHtml() {
|
|
301
|
-
return this.request
|
|
346
|
+
return this.request.acceptHtml;
|
|
302
347
|
}
|
|
303
348
|
setDefaultContentType() {
|
|
304
349
|
if (this.isHtml) this.response.setContentType("html", "utf-8");
|