@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
|
@@ -2,7 +2,7 @@ import HttpError from "../../errors/httpError.js";
|
|
|
2
2
|
import { hasForwardingHeaders, resolveForwarded } from "../forwarded.js";
|
|
3
3
|
import { Parser, ParserJson, ParserQs, ParserXml, acceptParser } from "./parser.js";
|
|
4
4
|
import { isCanonicalAuthority, splitTarget } from "./urlFastPath.js";
|
|
5
|
-
import { extend } from "nodefony";
|
|
5
|
+
import { RequestContext, extend, useConfig } from "nodefony";
|
|
6
6
|
import { createHash, randomUUID } from "node:crypto";
|
|
7
7
|
import { URL } from "node:url";
|
|
8
8
|
import fs from "node:fs";
|
|
@@ -39,6 +39,14 @@ const parse = {
|
|
|
39
39
|
PATCH: true,
|
|
40
40
|
DELETE: true
|
|
41
41
|
};
|
|
42
|
+
/**
|
|
43
|
+
* La méthode porte-t-elle un corps à lire ? La règle a UNE implémentation : le
|
|
44
|
+
* choix « lire le corps » (`parseRequest`) et le choix « rien à attendre »
|
|
45
|
+
* (`initialize`) la lisent tous deux ici.
|
|
46
|
+
*/
|
|
47
|
+
function hasRequestBody(method) {
|
|
48
|
+
return method in parse;
|
|
49
|
+
}
|
|
42
50
|
const PERF_PROBE_SUB = process.env.NF_PERF_PROBE === "1";
|
|
43
51
|
function perfMark(field) {
|
|
44
52
|
const p = globalThis.__nfPerfProbe;
|
|
@@ -129,12 +137,12 @@ var HttpRequest = class {
|
|
|
129
137
|
#acceptHtml = null;
|
|
130
138
|
/** Media-ranges de l'en-tête `Accept`, parsés au premier accès (mémoïsé). */
|
|
131
139
|
get accept() {
|
|
132
|
-
|
|
140
|
+
this.#accept ??= acceptParser(this.headers.accept);
|
|
133
141
|
return this.#accept;
|
|
134
142
|
}
|
|
135
143
|
/** `true` si le client accepte `text/html` — résolu au premier accès (mémoïsé). */
|
|
136
144
|
get acceptHtml() {
|
|
137
|
-
|
|
145
|
+
this.#acceptHtml ??= this.accepts("html");
|
|
138
146
|
return this.#acceptHtml;
|
|
139
147
|
}
|
|
140
148
|
origin;
|
|
@@ -147,8 +155,8 @@ var HttpRequest = class {
|
|
|
147
155
|
this.request.body = null;
|
|
148
156
|
this.headers = request.headers;
|
|
149
157
|
this.origin = this.headers.origin;
|
|
150
|
-
const checker = this.context
|
|
151
|
-
const socketAddress = this.request.socket
|
|
158
|
+
const checker = this.context.httpKernel?.getTrustProxyChecker();
|
|
159
|
+
const socketAddress = this.request.socket.remoteAddress;
|
|
152
160
|
this.trustedProxy = !!checker?.isTrusted(socketAddress);
|
|
153
161
|
if (this.trustedProxy && checker && hasForwardingHeaders(this.headers)) this.forwarded = resolveForwarded(this.headers, socketAddress, checker);
|
|
154
162
|
if (PERF_PROBE_SUB) perfMark("reqProxyNs");
|
|
@@ -167,11 +175,9 @@ var HttpRequest = class {
|
|
|
167
175
|
this.search = u.search;
|
|
168
176
|
}
|
|
169
177
|
if (PERF_PROBE_SUB) perfMark("reqUrlNs");
|
|
170
|
-
this.queryStringOptions = this.context
|
|
171
|
-
this.uploadOption = this.context?.httpKernel?.module.options.upload || {};
|
|
172
|
-
this.maxBodySize = this.context?.httpKernel?.module.options.maxBodySize ?? 0;
|
|
178
|
+
this.queryStringOptions = this.context.httpKernel?.module.options.queryString || {};
|
|
173
179
|
let query;
|
|
174
|
-
if (this.search) query = QS.parse(this.search.slice(1), this.queryStringOptions
|
|
180
|
+
if (this.search) query = QS.parse(this.search.slice(1), this.queryStringOptions ?? {});
|
|
175
181
|
else query = {};
|
|
176
182
|
if (this.#url !== null) this.#url.query = query;
|
|
177
183
|
this.queryGet = query;
|
|
@@ -198,32 +204,64 @@ var HttpRequest = class {
|
|
|
198
204
|
if (this.context.listenerCount("onRequestEnd") === 0) return false;
|
|
199
205
|
return this.context.fireAsync("onRequestEnd", this);
|
|
200
206
|
}
|
|
201
|
-
|
|
207
|
+
/**
|
|
208
|
+
* Lit le corps de la requête, s'il y en a un, puis émet `onRequestEnd`.
|
|
209
|
+
*
|
|
210
|
+
* Le contrat de retour dit ce qui reste à ATTENDRE :
|
|
211
|
+
* - `undefined` — la méthode ne porte pas de corps (GET, HEAD, OPTIONS…) et
|
|
212
|
+
* aucun écouteur `onRequestEnd` ne rend de promesse : tout est fait, de
|
|
213
|
+
* façon SYNCHRONE ;
|
|
214
|
+
* - une `Promise` — le corps arrive du socket (POST, PUT, PATCH, DELETE), ou
|
|
215
|
+
* un écouteur `onRequestEnd` est à attendre. Sa valeur n'est consommée par
|
|
216
|
+
* personne : elle ne sert qu'au séquençage.
|
|
217
|
+
*
|
|
218
|
+
* L'appelant n'attend donc que ce qui attend vraiment
|
|
219
|
+
* (`const pending = request.initialize(); if (pending) await pending;`) : un
|
|
220
|
+
* `await` inconditionnel reportait le pipeline d'une micro-tâche, et la
|
|
221
|
+
* chaîne de promesses coûtait ~6 évènements asynchrones à un GET qui n'avait
|
|
222
|
+
* rien à lire (#505).
|
|
223
|
+
*
|
|
224
|
+
* Ne lève jamais de façon synchrone : un corps refusé (413) ou illisible rompt
|
|
225
|
+
* la promesse rendue, comme avant.
|
|
226
|
+
*
|
|
227
|
+
* @returns `undefined` quand tout est fait, sinon la promesse à attendre.
|
|
228
|
+
*/
|
|
229
|
+
initialize() {
|
|
230
|
+
if (!hasRequestBody(this.method)) return this.endWithoutBody();
|
|
202
231
|
return this.parseRequest().then(async (parser) => {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
case parser instanceof Parser: try {
|
|
232
|
+
if (parser instanceof ParserXml || parser instanceof ParserQs || parser instanceof Parser) {
|
|
233
|
+
let requestEnd;
|
|
234
|
+
try {
|
|
207
235
|
if (this.context.finished) return;
|
|
208
236
|
await parser.parse();
|
|
209
|
-
|
|
210
|
-
} catch (error) {
|
|
211
|
-
return this.context?.httpKernel?.onError(error, this.context);
|
|
212
|
-
}
|
|
213
|
-
default: if (!parser) try {
|
|
214
|
-
if (this.context.finished) return;
|
|
215
|
-
this.context.requestEnded = true;
|
|
216
|
-
return this.fireRequestEnd();
|
|
237
|
+
requestEnd = this.fireRequestEnd();
|
|
217
238
|
} catch (error) {
|
|
218
239
|
return this.context.httpKernel?.onError(error, this.context);
|
|
219
240
|
}
|
|
241
|
+
return requestEnd;
|
|
220
242
|
}
|
|
243
|
+
if (!parser) return this.endWithoutBody();
|
|
221
244
|
return parser;
|
|
222
|
-
}).catch((e) => {
|
|
223
|
-
throw e;
|
|
224
245
|
});
|
|
225
246
|
}
|
|
226
247
|
/**
|
|
248
|
+
* Fin d'une requête SANS corps à lire : la marque terminée et émet
|
|
249
|
+
* `onRequestEnd`. Synchrone — ne rend une promesse que lorsqu'un écouteur
|
|
250
|
+
* `onRequestEnd` est à attendre, ou quand l'émission lève (celle d'`onError`).
|
|
251
|
+
*
|
|
252
|
+
* @returns `undefined` quand tout est fait, sinon la promesse à attendre.
|
|
253
|
+
*/
|
|
254
|
+
endWithoutBody() {
|
|
255
|
+
if (this.context.finished) return;
|
|
256
|
+
try {
|
|
257
|
+
this.context.requestEnded = true;
|
|
258
|
+
const requestEnd = this.fireRequestEnd();
|
|
259
|
+
return requestEnd === false ? void 0 : requestEnd;
|
|
260
|
+
} catch (error) {
|
|
261
|
+
return this.context.httpKernel?.onError(error, this.context);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
227
265
|
* B1 — Pré-contrôle de taille du corps via l'en-tête `Content-Length` :
|
|
228
266
|
* rejette en 413 (RFC 9110 §15.5.14 « Content Too Large ») AVANT toute lecture
|
|
229
267
|
* quand le client annonce une taille supérieure à `maxBodySize`. Rideau de
|
|
@@ -232,16 +270,28 @@ var HttpRequest = class {
|
|
|
232
270
|
*
|
|
233
271
|
* @throws {HttpError} 413 si `Content-Length` dépasse `maxBodySize`.
|
|
234
272
|
*/
|
|
273
|
+
/**
|
|
274
|
+
* Lit le plafond du corps et les limites d'envoi tels que les voit CETTE
|
|
275
|
+
* requête : la configuration http, surchargée par le calque que la requête a
|
|
276
|
+
* pu poser avant la lecture du corps (`overlayConfig`, #494). Hors pipeline
|
|
277
|
+
* (aucun scope ouvert), la configuration du module telle quelle.
|
|
278
|
+
*/
|
|
279
|
+
readBodyLimits() {
|
|
280
|
+
const options = RequestContext.getScope() !== void 0 ? useConfig("@nodefony/http") : this.context.httpKernel?.module.options;
|
|
281
|
+
this.uploadOption = options?.upload ?? {};
|
|
282
|
+
this.maxBodySize = options?.maxBodySize ?? 0;
|
|
283
|
+
}
|
|
235
284
|
enforceBodyLimit() {
|
|
236
285
|
const max = this.maxBodySize;
|
|
237
286
|
if (max <= 0) return;
|
|
238
287
|
const cl = this.headers["content-length"];
|
|
239
288
|
if (cl === void 0) return;
|
|
240
|
-
const declared = parseInt(
|
|
289
|
+
const declared = parseInt(typeof cl === "string" ? cl : cl[0], 10);
|
|
241
290
|
if (Number.isFinite(declared) && declared > max) throw new HttpError(`Request body too large: ${declared} bytes > maxBodySize ${max}`, 413, this.context);
|
|
242
291
|
}
|
|
243
292
|
async parseRequest() {
|
|
244
|
-
if (!(this.method
|
|
293
|
+
if (!hasRequestBody(this.method)) return this.parser;
|
|
294
|
+
this.readBodyLimits();
|
|
245
295
|
if (this.contentType !== "multipart/form-data") this.enforceBodyLimit();
|
|
246
296
|
switch (this.contentType) {
|
|
247
297
|
case "application/xml":
|
|
@@ -349,8 +399,7 @@ var HttpRequest = class {
|
|
|
349
399
|
stream.resume();
|
|
350
400
|
return;
|
|
351
401
|
}
|
|
352
|
-
const
|
|
353
|
-
const field = match ? match[1] : fieldname;
|
|
402
|
+
const field = reg.exec(fieldname)?.[1] ?? fieldname;
|
|
354
403
|
const ext = filename ? path.extname(filename) : "";
|
|
355
404
|
const newFilename = `${randomUUID()}${ext}`;
|
|
356
405
|
const filepath = path.join(dir, newFilename);
|
|
@@ -415,7 +464,7 @@ var HttpRequest = class {
|
|
|
415
464
|
*/
|
|
416
465
|
assignField(fields, name, value) {
|
|
417
466
|
const match = reg.exec(name);
|
|
418
|
-
const key = match
|
|
467
|
+
const key = match?.[1] ?? name;
|
|
419
468
|
if (key in fields) {
|
|
420
469
|
const cur = fields[key];
|
|
421
470
|
if (Array.isArray(cur)) cur.push(value);
|
|
@@ -427,7 +476,7 @@ var HttpRequest = class {
|
|
|
427
476
|
let subtype = "*";
|
|
428
477
|
let type = "*";
|
|
429
478
|
if (Type) parts = Type.split("/");
|
|
430
|
-
|
|
479
|
+
switch (parts.length) {
|
|
431
480
|
case 1:
|
|
432
481
|
subtype = parts.shift();
|
|
433
482
|
break;
|
|
@@ -437,10 +486,7 @@ var HttpRequest = class {
|
|
|
437
486
|
break;
|
|
438
487
|
default: throw new Error("request accepts method bad type format");
|
|
439
488
|
}
|
|
440
|
-
for (
|
|
441
|
-
const line = this.accept[i];
|
|
442
|
-
if ((type === "*" || line.type.test(type)) && (subtype === "*" || line.subtype.test(subtype))) return true;
|
|
443
|
-
}
|
|
489
|
+
for (const line of this.accept) if ((type === "*" || line.type.test(type)) && (subtype === "*" || line.subtype.test(subtype))) return true;
|
|
444
490
|
return false;
|
|
445
491
|
}
|
|
446
492
|
async createFileUpload(name, file) {
|
|
@@ -454,12 +500,15 @@ var HttpRequest = class {
|
|
|
454
500
|
getContentType(request) {
|
|
455
501
|
if (request.headers["content-type"]) {
|
|
456
502
|
const tab = request.headers["content-type"].split(";");
|
|
457
|
-
if (tab.length > 1) for (let i = 1; i < tab.length; i++)
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
503
|
+
if (tab.length > 1) for (let i = 1; i < tab.length; i++) {
|
|
504
|
+
const part = tab[i];
|
|
505
|
+
if (typeof part === "string") {
|
|
506
|
+
const [rawKey = "", value] = part.split("=");
|
|
507
|
+
const key = rawKey.replace(/\s+/g, "").toLowerCase();
|
|
508
|
+
this.rawContentType[key] = value;
|
|
509
|
+
} else continue;
|
|
510
|
+
}
|
|
511
|
+
return tab[0] ?? null;
|
|
463
512
|
}
|
|
464
513
|
return null;
|
|
465
514
|
}
|
|
@@ -485,8 +534,8 @@ var HttpRequest = class {
|
|
|
485
534
|
}
|
|
486
535
|
getHostName(host) {
|
|
487
536
|
if (this.#url?.hostname) return this.#url.hostname;
|
|
488
|
-
if (host) return host.split(":")[0];
|
|
489
|
-
if (host = this.getHost()) return host.split(":")[0];
|
|
537
|
+
if (host) return host.split(":")[0] ?? "";
|
|
538
|
+
if (host = this.getHost()) return host.split(":")[0] ?? "";
|
|
490
539
|
return "";
|
|
491
540
|
}
|
|
492
541
|
getHost() {
|
|
@@ -494,7 +543,7 @@ var HttpRequest = class {
|
|
|
494
543
|
}
|
|
495
544
|
getRemoteAddress() {
|
|
496
545
|
if (this.forwarded) return this.forwarded.clientIp;
|
|
497
|
-
return this.request.socket
|
|
546
|
+
return this.request.socket.remoteAddress ?? null;
|
|
498
547
|
}
|
|
499
548
|
/**
|
|
500
549
|
* Request-target brut (origin-form) tel que reçu du transport — SOURCE
|
|
@@ -511,7 +560,7 @@ var HttpRequest = class {
|
|
|
511
560
|
*/
|
|
512
561
|
resolveScheme(request) {
|
|
513
562
|
if (this.forwarded?.proto) return this.forwarded.proto;
|
|
514
|
-
if (
|
|
563
|
+
if (request.socket.encrypted) return "https";
|
|
515
564
|
return "http";
|
|
516
565
|
}
|
|
517
566
|
getFullUrl(request = this.request) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Cookie from "../../cookies/cookie.js";
|
|
2
|
-
import { typeOf } from "nodefony";
|
|
2
|
+
import { thenMaybe, typeOf } from "nodefony";
|
|
3
3
|
import http from "node:http";
|
|
4
4
|
import mime from "mime-types";
|
|
5
5
|
//#region nodefony/src/context/http/Response.ts
|
|
@@ -14,11 +14,6 @@ const REDIRECT_STATUS_CODES = /* @__PURE__ */ new Set([
|
|
|
14
14
|
307,
|
|
15
15
|
308
|
|
16
16
|
]);
|
|
17
|
-
const NO_CONTENT_LENGTH_METHODS = /* @__PURE__ */ new Set([
|
|
18
|
-
"HEAD",
|
|
19
|
-
"OPTIONS",
|
|
20
|
-
"TRACE"
|
|
21
|
-
]);
|
|
22
17
|
const NO_CONTENT_LENGTH_STATUS = /* @__PURE__ */ new Set([204, 304]);
|
|
23
18
|
var HttpResponse = class {
|
|
24
19
|
context;
|
|
@@ -27,7 +22,9 @@ var HttpResponse = class {
|
|
|
27
22
|
statusMessage = "";
|
|
28
23
|
flushing = false;
|
|
29
24
|
encoding = "utf-8";
|
|
30
|
-
|
|
25
|
+
#text = null;
|
|
26
|
+
#textEncoding = "utf-8";
|
|
27
|
+
#buffer = null;
|
|
31
28
|
contentType = "application/octet-stream";
|
|
32
29
|
headers = {};
|
|
33
30
|
timeout;
|
|
@@ -35,7 +32,7 @@ var HttpResponse = class {
|
|
|
35
32
|
constructor(response, context) {
|
|
36
33
|
this.context = context;
|
|
37
34
|
this.response = response;
|
|
38
|
-
this.timeout = this.context
|
|
35
|
+
this.timeout = this.context.httpKernel?.responseTimeout[this.context.type];
|
|
39
36
|
}
|
|
40
37
|
/**
|
|
41
38
|
* Filet posé juste avant l'émission des en-têtes : si aucun `Content-Type`
|
|
@@ -46,6 +43,33 @@ var HttpResponse = class {
|
|
|
46
43
|
ensureContentTypeHeader() {
|
|
47
44
|
if (this.response && !this.response.hasHeader("content-type")) this.response.setHeader("content-type", this.contentType);
|
|
48
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Corps de la réponse en octets. Un corps posé en texte n'est encodé qu'à la
|
|
48
|
+
* première lecture de cette propriété — l'envoi, lui, écrit le texte tel quel.
|
|
49
|
+
*/
|
|
50
|
+
get body() {
|
|
51
|
+
if (this.#text !== null) {
|
|
52
|
+
this.#buffer = Buffer.from(this.#text, this.#textEncoding);
|
|
53
|
+
this.#text = null;
|
|
54
|
+
}
|
|
55
|
+
return this.#buffer;
|
|
56
|
+
}
|
|
57
|
+
set body(value) {
|
|
58
|
+
this.#text = null;
|
|
59
|
+
this.#buffer = value;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Corps tel qu'il part sur le fil : le texte s'il n'a jamais été encodé, sinon
|
|
63
|
+
* les octets — vide légal si rien n'a été posé (`write(null)` jetterait
|
|
64
|
+
* ERR_STREAM_NULL_VALUES).
|
|
65
|
+
*/
|
|
66
|
+
get payload() {
|
|
67
|
+
return this.#text ?? this.#buffer ?? "";
|
|
68
|
+
}
|
|
69
|
+
/** Encodage à passer avec {@link payload} (ignoré par Node pour des octets). */
|
|
70
|
+
get payloadEncoding() {
|
|
71
|
+
return this.#text !== null ? this.#textEncoding : this.encoding;
|
|
72
|
+
}
|
|
49
73
|
clean() {
|
|
50
74
|
this.response = null;
|
|
51
75
|
this.body = null;
|
|
@@ -68,7 +92,7 @@ var HttpResponse = class {
|
|
|
68
92
|
}
|
|
69
93
|
deleteCookie(cookie) {
|
|
70
94
|
if (cookie instanceof Cookie) {
|
|
71
|
-
if (this.cookies
|
|
95
|
+
if (Object.hasOwn(this.cookies, cookie.name)) {
|
|
72
96
|
delete this.cookies[cookie.name];
|
|
73
97
|
return true;
|
|
74
98
|
}
|
|
@@ -77,19 +101,20 @@ var HttpResponse = class {
|
|
|
77
101
|
throw new Error("Response delCookie not valid cookies");
|
|
78
102
|
}
|
|
79
103
|
deleteCookieByName(name) {
|
|
80
|
-
if (this.cookies
|
|
104
|
+
if (Object.hasOwn(this.cookies, name)) {
|
|
81
105
|
delete this.cookies[name];
|
|
82
106
|
return true;
|
|
83
107
|
}
|
|
84
108
|
return false;
|
|
85
109
|
}
|
|
86
110
|
setCookies() {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
if (
|
|
111
|
+
const cookies = Object.values(this.cookies);
|
|
112
|
+
const first = cookies.at(0);
|
|
113
|
+
if (first === void 0) return;
|
|
114
|
+
if (cookies.length === 1) return this.setCookie(first);
|
|
90
115
|
const serialized = [];
|
|
91
|
-
for (const
|
|
92
|
-
const s =
|
|
116
|
+
for (const cookie of cookies) {
|
|
117
|
+
const s = cookie.serialize();
|
|
93
118
|
this.log(`ADD COOKIE ==> ${s}`, "DEBUG");
|
|
94
119
|
serialized.push(s);
|
|
95
120
|
}
|
|
@@ -105,7 +130,8 @@ var HttpResponse = class {
|
|
|
105
130
|
if (this.flushing) {
|
|
106
131
|
const obj = {};
|
|
107
132
|
obj[name] = value;
|
|
108
|
-
|
|
133
|
+
this.addTrailers(obj);
|
|
134
|
+
return;
|
|
109
135
|
}
|
|
110
136
|
if (!this.response.headersSent) {
|
|
111
137
|
const lower = name.toLowerCase();
|
|
@@ -125,7 +151,7 @@ var HttpResponse = class {
|
|
|
125
151
|
*/
|
|
126
152
|
#mergeVary(value) {
|
|
127
153
|
const incoming = (Array.isArray(value) ? value.join(",") : String(value)).split(",").map((t) => t.trim()).filter(Boolean);
|
|
128
|
-
const current = this.response?.getHeader
|
|
154
|
+
const current = this.response?.getHeader("vary");
|
|
129
155
|
const all = [...(Array.isArray(current) ? current.join(",") : String(current ?? "")).split(",").map((t) => t.trim()).filter(Boolean), ...incoming];
|
|
130
156
|
if (all.some((t) => t === "*")) return "*";
|
|
131
157
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -151,9 +177,9 @@ var HttpResponse = class {
|
|
|
151
177
|
}
|
|
152
178
|
setContentType(type, encoding) {
|
|
153
179
|
if (type && encoding) {
|
|
154
|
-
|
|
155
|
-
if (
|
|
156
|
-
mytype =
|
|
180
|
+
const full = mime.contentType(type);
|
|
181
|
+
if (full) {
|
|
182
|
+
const mytype = full.split(";")[0] ?? full;
|
|
157
183
|
this.contentType = mytype;
|
|
158
184
|
this.encoding = encoding;
|
|
159
185
|
if (mytype === "application/json" || mytype.endsWith("+json")) return this.setHeader("Content-Type", mytype);
|
|
@@ -230,30 +256,45 @@ var HttpResponse = class {
|
|
|
230
256
|
if (this.response) return this.statusMessage || this.response.statusMessage || http.STATUS_CODES[this.statusCode];
|
|
231
257
|
return this.statusMessage || http.STATUS_CODES[this.statusCode];
|
|
232
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* Pose le corps de la réponse : texte gardé tel quel, octets copiés sur leur
|
|
261
|
+
* seule fenêtre, tout autre valeur sérialisée en JSON.
|
|
262
|
+
*
|
|
263
|
+
* @param ele - corps à envoyer.
|
|
264
|
+
* @param encoding - encodage d'un corps texte (défaut : `this.encoding`).
|
|
265
|
+
*/
|
|
233
266
|
setBody(ele, encoding) {
|
|
234
|
-
if (typeof ele === "string") this
|
|
267
|
+
if (typeof ele === "string") this.#setText(ele, encoding || this.encoding);
|
|
235
268
|
else if (ele instanceof ArrayBuffer || ele instanceof SharedArrayBuffer) this.body = Buffer.from(ele);
|
|
236
269
|
else if (ArrayBuffer.isView(ele) && ele.buffer instanceof ArrayBuffer) this.body = Buffer.from(ele.buffer, ele.byteOffset, ele.byteLength);
|
|
237
|
-
else
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
270
|
+
else {
|
|
271
|
+
let text;
|
|
272
|
+
try {
|
|
273
|
+
text = JSON.stringify(ele);
|
|
274
|
+
} catch {
|
|
275
|
+
text = String(ele);
|
|
276
|
+
}
|
|
277
|
+
this.#setText(text, "utf-8");
|
|
241
278
|
}
|
|
242
|
-
|
|
279
|
+
}
|
|
280
|
+
#setText(text, encoding) {
|
|
281
|
+
this.#buffer = null;
|
|
282
|
+
this.#text = text;
|
|
283
|
+
this.#textEncoding = encoding;
|
|
243
284
|
}
|
|
244
285
|
setLength(body) {
|
|
245
286
|
if (this.response?.headersSent) throw new Error("Headers already sended");
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (!NO_CONTENT_LENGTH_METHODS.has(this.context?.method) && !NO_CONTENT_LENGTH_STATUS.has(this.statusCode) && !isChunked) {
|
|
249
|
-
if (actualBody) {
|
|
250
|
-
const length = Buffer.byteLength(actualBody);
|
|
251
|
-
this.setHeader("Content-Length", String(length));
|
|
252
|
-
return length;
|
|
253
|
-
}
|
|
287
|
+
if (this.getHeader("Transfer-Encoding") === "chunked") {
|
|
288
|
+
this.response?.removeHeader("Content-Length");
|
|
254
289
|
return 0;
|
|
255
290
|
}
|
|
256
|
-
if (
|
|
291
|
+
if (NO_CONTENT_LENGTH_STATUS.has(this.statusCode)) return 0;
|
|
292
|
+
const actualBody = body || this.#text || this.#buffer;
|
|
293
|
+
if (actualBody) {
|
|
294
|
+
const length = typeof actualBody === "string" && actualBody === this.#text ? Buffer.byteLength(actualBody, this.#textEncoding) : Buffer.byteLength(actualBody);
|
|
295
|
+
this.setHeader("Content-Length", String(length));
|
|
296
|
+
return length;
|
|
297
|
+
}
|
|
257
298
|
return 0;
|
|
258
299
|
}
|
|
259
300
|
writeHead(statusCode, headers) {
|
|
@@ -264,8 +305,8 @@ var HttpResponse = class {
|
|
|
264
305
|
if (this.statusCode > 599) this.statusCode = 500;
|
|
265
306
|
}
|
|
266
307
|
this.statusMessage = this.getStatusMessage();
|
|
267
|
-
if (this.context.requestId
|
|
268
|
-
if (this.context.traceparent
|
|
308
|
+
if (this.context.requestId) this.response.setHeader("x-request-id", this.context.requestId);
|
|
309
|
+
if (this.context.traceparent) this.response.setHeader("traceparent", this.context.traceparent);
|
|
269
310
|
this.setLength();
|
|
270
311
|
this.ensureContentTypeHeader();
|
|
271
312
|
const std = http.STATUS_CODES[this.statusCode];
|
|
@@ -287,18 +328,29 @@ var HttpResponse = class {
|
|
|
287
328
|
this.setHeader("Transfer-Encoding", "chunked");
|
|
288
329
|
return this.send(chunk, encoding, true);
|
|
289
330
|
}
|
|
290
|
-
|
|
331
|
+
/**
|
|
332
|
+
* Écrit le corps sur le flux et, pour une réponse unique, la termine.
|
|
333
|
+
*
|
|
334
|
+
* Synchrone pour une réponse unique ou une redirection : `end(corps)` est un
|
|
335
|
+
* appel synchrone de Node, rien n'y est à attendre (#505). Une promesse
|
|
336
|
+
* seulement en streaming chunké (`flush()`), réglée quand le flux accepte
|
|
337
|
+
* l'écriture suivante (`drain`) — c'est la contre-pression.
|
|
338
|
+
*
|
|
339
|
+
* @param chunk - le corps (posé par `setBody`) ; absent, le corps déjà posé.
|
|
340
|
+
* @param encoding - l'encodage d'un corps texte.
|
|
341
|
+
* @returns la réponse, ou sa promesse en streaming chunké.
|
|
342
|
+
* @throws de façon SYNCHRONE quand la réponse Node n'existe plus.
|
|
343
|
+
*/
|
|
344
|
+
send(chunk, encoding, _flush = false) {
|
|
291
345
|
if (this.context.isRedirect) {
|
|
292
346
|
if (!this.response?.headersSent) this.writeHead();
|
|
293
|
-
|
|
294
|
-
return this;
|
|
347
|
+
return thenMaybe(this.end(), () => this);
|
|
295
348
|
}
|
|
296
|
-
if (chunk) this.setBody(chunk);
|
|
349
|
+
if (chunk) this.setBody(chunk, encoding);
|
|
297
350
|
if (!this.response) throw new Error(`Http Response not found`);
|
|
298
|
-
if (this.body === null) this.body = Buffer.alloc(0);
|
|
299
351
|
const res = this.response;
|
|
300
352
|
if (!_flush && !this.flushing && !res.writableEnded) {
|
|
301
|
-
res.end(this.
|
|
353
|
+
res.end(this.payload, this.payloadEncoding);
|
|
302
354
|
return this;
|
|
303
355
|
}
|
|
304
356
|
return new Promise((resolve) => {
|
|
@@ -310,7 +362,7 @@ var HttpResponse = class {
|
|
|
310
362
|
}
|
|
311
363
|
};
|
|
312
364
|
const onDrain = () => done();
|
|
313
|
-
if (res.write(this.
|
|
365
|
+
if (res.write(this.payload, this.payloadEncoding, (error) => {
|
|
314
366
|
if (error) {
|
|
315
367
|
this.log(error, "ERROR");
|
|
316
368
|
res.removeListener("drain", onDrain);
|
|
@@ -320,17 +372,21 @@ var HttpResponse = class {
|
|
|
320
372
|
else res.once("drain", onDrain);
|
|
321
373
|
});
|
|
322
374
|
}
|
|
323
|
-
|
|
324
|
-
return
|
|
375
|
+
write(chunk, encoding) {
|
|
376
|
+
return this.send(chunk, encoding || this.encoding);
|
|
325
377
|
}
|
|
326
378
|
writeContinue() {
|
|
327
379
|
return this.response?.writeContinue();
|
|
328
380
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
381
|
+
/**
|
|
382
|
+
* Termine la réponse Node — un appel synchrone, rien n'y est à attendre (#505).
|
|
383
|
+
*
|
|
384
|
+
* @returns le flux terminé (valeur de `ServerResponse.end`).
|
|
385
|
+
* @throws de façon SYNCHRONE quand la réponse Node n'existe plus.
|
|
386
|
+
*/
|
|
387
|
+
end(chunk, encoding) {
|
|
388
|
+
if (!this.response) throw new Error(`response not found`);
|
|
389
|
+
return this.response.end(chunk, encoding || this.encoding);
|
|
334
390
|
}
|
|
335
391
|
getHeader(name) {
|
|
336
392
|
return this.response?.getHeader(name);
|
|
@@ -19,7 +19,7 @@ var Parser = class {
|
|
|
19
19
|
}
|
|
20
20
|
write(buffer) {
|
|
21
21
|
if (this.aborted) return buffer;
|
|
22
|
-
const max = this.request.maxBodySize
|
|
22
|
+
const max = this.request.maxBodySize;
|
|
23
23
|
this.received += buffer.length;
|
|
24
24
|
if (max > 0 && this.received > max) {
|
|
25
25
|
this.aborted = true;
|
|
@@ -78,7 +78,7 @@ var ParserQs = class extends Parser {
|
|
|
78
78
|
charset = "utf8";
|
|
79
79
|
constructor(request) {
|
|
80
80
|
super(request);
|
|
81
|
-
this.parserOptions = this.request.queryStringOptions
|
|
81
|
+
this.parserOptions = this.request.queryStringOptions ?? {};
|
|
82
82
|
this.charset = this.request.charset;
|
|
83
83
|
}
|
|
84
84
|
async parse() {
|
|
@@ -151,13 +151,15 @@ const ACCEPT_ANY = [{
|
|
|
151
151
|
* corrige aussi la négociation (match du token ENTIER, pas d'une sous-chaîne).
|
|
152
152
|
*/
|
|
153
153
|
const acceptMatcher = (token) => !token || token === "*" ? /.*/ : new RegExp(`^${escapeRegExp(token)}$`);
|
|
154
|
+
/** Qualité `q` d'une entrée `Accept` : sa valeur si c'est un nombre fini, 1 sinon. */
|
|
155
|
+
const qualityOf = (entry) => typeof entry.q === "number" && Number.isFinite(entry.q) ? entry.q : 1;
|
|
154
156
|
const acceptParser = function(acc) {
|
|
155
157
|
if (!acc) return ACCEPT_ANY;
|
|
156
158
|
const arr = [];
|
|
157
159
|
try {
|
|
158
160
|
const types = acc.split(",");
|
|
159
|
-
for (
|
|
160
|
-
const type =
|
|
161
|
+
for (const segment of types) {
|
|
162
|
+
const type = segment.split(";");
|
|
161
163
|
const mine = type.shift();
|
|
162
164
|
if (!mine) continue;
|
|
163
165
|
const dec = mine.trim().split("/");
|
|
@@ -167,8 +169,8 @@ const acceptParser = function(acc) {
|
|
|
167
169
|
type: acceptMatcher(ele1),
|
|
168
170
|
subtype: acceptMatcher(ele2)
|
|
169
171
|
};
|
|
170
|
-
for (
|
|
171
|
-
const params =
|
|
172
|
+
for (const param of type) {
|
|
173
|
+
const params = param.split("=");
|
|
172
174
|
const name = params.shift();
|
|
173
175
|
const value = params.shift();
|
|
174
176
|
obj[name] = name === "q" ? parseFloat(value) : value;
|
|
@@ -176,10 +178,7 @@ const acceptParser = function(acc) {
|
|
|
176
178
|
arr.push(obj);
|
|
177
179
|
}
|
|
178
180
|
if (arr.length === 0) return ACCEPT_ANY;
|
|
179
|
-
return arr.sort((a, b) =>
|
|
180
|
-
const qA = a.q || 1;
|
|
181
|
-
return (b.q || 1) - qA;
|
|
182
|
-
});
|
|
181
|
+
return arr.sort((a, b) => qualityOf(b) - qualityOf(a));
|
|
183
182
|
} catch {
|
|
184
183
|
return ACCEPT_ANY;
|
|
185
184
|
}
|
|
@@ -81,8 +81,7 @@ function isCanonicalAuthority(host, scheme) {
|
|
|
81
81
|
}
|
|
82
82
|
const labels = name.split(".");
|
|
83
83
|
for (const label of labels) if (label.length === 0) return false;
|
|
84
|
-
|
|
85
|
-
if (isDigits(last)) {
|
|
84
|
+
if (isDigits(labels.at(-1) ?? "")) {
|
|
86
85
|
if (labels.length !== 4) return false;
|
|
87
86
|
for (const label of labels) {
|
|
88
87
|
if (!isDigits(label) || label.length > 3) return false;
|