@nodefony/http 10.0.0-beta.5 → 10.0.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +29 -26
- package/dist/nodefony/command/proxyGenerateCommand.js +2 -2
- package/dist/nodefony/config/config.js +4 -2
- package/dist/nodefony/service/ProfilerAdminApi.js +5 -2
- package/dist/nodefony/service/error-renderer.js +6 -4
- package/dist/nodefony/service/http-kernel.js +112 -34
- package/dist/nodefony/service/pretty-request-logger.js +1 -1
- package/dist/nodefony/service/request-logger.js +1 -1
- package/dist/nodefony/service/reverse-proxy.js +7 -2
- package/dist/nodefony/service/servers/clientError.js +28 -4
- package/dist/nodefony/service/servers/server-h2c.js +5 -5
- package/dist/nodefony/service/servers/server-http.js +5 -3
- package/dist/nodefony/service/servers/server-https.js +12 -5
- package/dist/nodefony/service/servers/server-static.js +1 -6
- package/dist/nodefony/service/servers/server-websocket-secure.js +6 -10
- package/dist/nodefony/service/servers/server-websocket.js +6 -10
- package/dist/nodefony/service/servers/serverShutdown.js +63 -4
- package/dist/nodefony/service/sessions/sessions-service.js +3 -3
- package/dist/nodefony/service/upload/upload-service.js +2 -2
- package/dist/nodefony/src/assets/prebuiltUi.js +23 -5
- package/dist/nodefony/src/context/http/ByteStream.js +359 -0
- package/dist/nodefony/src/context/http/HttpContext.js +55 -26
- package/dist/nodefony/src/context/http/Request.js +15 -6
- package/dist/nodefony/src/context/http/Response.js +354 -35
- package/dist/nodefony/src/context/http/SseStream.js +81 -146
- package/dist/nodefony/src/context/http/StreamLifecycle.js +273 -0
- package/dist/nodefony/src/context/http/urlFastPath.js +80 -1
- package/dist/nodefony/src/context/http2/Request.js +1 -1
- package/dist/nodefony/src/context/http2/Response.js +8 -4
- package/dist/nodefony/src/context/transport.js +20 -1
- package/dist/nodefony/src/context/websocket/Response.js +4 -2
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +71 -14
- package/dist/nodefony/src/context/websocket/websocketUrl.js +7 -0
- package/dist/nodefony/src/context/websocket/wsBackpressure.js +15 -15
- package/dist/nodefony/src/context/websocket/wsFrameRate.js +71 -0
- package/dist/nodefony/src/context/websocket/wsLogContent.js +55 -17
- package/dist/nodefony/src/cookies/cookie.js +54 -13
- package/dist/nodefony/src/profiler/Profiler.js +14 -1
- package/dist/nodefony/src/proxy/forward.js +109 -9
- package/dist/nodefony/src/proxy/generateProxyConfig.js +6 -2
- package/dist/nodefony/src/proxy/rules.js +29 -14
- package/dist/nodefony/src/servers/upgradeDispatch.js +62 -4
- package/dist/types/index.d.ts +6 -0
- package/dist/types/nodefony/config/config.d.ts +4 -0
- package/dist/types/nodefony/interfaces/ISecurity.d.ts +27 -2
- package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +2 -1
- package/dist/types/nodefony/service/error-renderer.d.ts +5 -4
- package/dist/types/nodefony/service/http-kernel.d.ts +15 -2
- package/dist/types/nodefony/service/servers/clientError.d.ts +3 -2
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +5 -3
- package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +9 -4
- package/dist/types/nodefony/src/context/http/ByteStream.d.ts +109 -0
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +45 -3
- package/dist/types/nodefony/src/context/http/Request.d.ts +8 -1
- package/dist/types/nodefony/src/context/http/Response.d.ts +134 -8
- package/dist/types/nodefony/src/context/http/SseStream.d.ts +32 -7
- package/dist/types/nodefony/src/context/http/StreamLifecycle.d.ts +53 -0
- package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +40 -0
- package/dist/types/nodefony/src/context/http2/Response.d.ts +5 -3
- package/dist/types/nodefony/src/context/transport.d.ts +17 -0
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +40 -0
- package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +4 -0
- package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +12 -2
- package/dist/types/nodefony/src/context/websocket/wsFrameRate.d.ts +69 -0
- package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +16 -8
- package/dist/types/nodefony/src/cookies/cookie.d.ts +20 -2
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +11 -1
- package/dist/types/nodefony/src/proxy/forward.d.ts +30 -2
- package/dist/types/nodefony/src/proxy/rules.d.ts +20 -0
- package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +41 -1
- package/docs/byte-stream.md +371 -0
- package/docs/cookies.md +13 -3
- package/docs/index.md +8 -0
- package/docs/reverse-proxy.md +15 -3
- package/docs/servers.md +26 -15
- package/docs/session.md +3 -3
- package/docs/sse.md +27 -20
- package/package.json +16 -16
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Cookie from "../../cookies/cookie.js";
|
|
2
2
|
import { thenMaybe, typeOf } from "nodefony";
|
|
3
|
-
import http from "node:http";
|
|
3
|
+
import http, { validateHeaderName, validateHeaderValue } from "node:http";
|
|
4
4
|
import mime from "mime-types";
|
|
5
5
|
// nodefony/src/context/http/Response.ts
|
|
6
6
|
const ANSI_REGEX = new RegExp(["[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)", "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))"].join("|"), "g");
|
|
@@ -15,6 +15,40 @@ const REDIRECT_STATUS_CODES = /* @__PURE__ */ new Set([
|
|
|
15
15
|
308
|
|
16
16
|
]);
|
|
17
17
|
const NO_CONTENT_LENGTH_STATUS = /* @__PURE__ */ new Set([204, 304]);
|
|
18
|
+
const NO_CONTENT_STATUS = /* @__PURE__ */ new Set([
|
|
19
|
+
204,
|
|
20
|
+
205,
|
|
21
|
+
304
|
|
22
|
+
]);
|
|
23
|
+
/**
|
|
24
|
+
* Vrai pour un statut dont la réponse n'a, par définition, aucun contenu —
|
|
25
|
+
* 204, 205 et 304 (RFC 9110 §15.3.5, §15.3.6, §15.4.5).
|
|
26
|
+
*
|
|
27
|
+
* Seule source de cette liste : la réponse s'en sert pour ne jamais écrire de
|
|
28
|
+
* corps, le résolveur pour terminer une action qui ne rend rien.
|
|
29
|
+
*
|
|
30
|
+
* @param status - le code de statut de la réponse.
|
|
31
|
+
* @returns `true` quand aucun corps ne doit partir.
|
|
32
|
+
*/
|
|
33
|
+
function isContentlessStatus(status) {
|
|
34
|
+
return NO_CONTENT_STATUS.has(status);
|
|
35
|
+
}
|
|
36
|
+
/** Espace ou tabulation : l'OWS de RFC 9110 §5.6.3. */
|
|
37
|
+
const isOws = (code) => code === 32 || code === 9;
|
|
38
|
+
/**
|
|
39
|
+
* Valeur d'en-tête sans espace de bord (RFC 9110 §5.5 ; RFC 9113 §8.2.1 en fait
|
|
40
|
+
* un MUST NOT en HTTP/2). Ne copie la chaîne que dans le cas rare où il y a
|
|
41
|
+
* quelque chose à rogner.
|
|
42
|
+
*/
|
|
43
|
+
function trimFieldValue(value) {
|
|
44
|
+
const last = value.length - 1;
|
|
45
|
+
if (last < 0 || !isOws(value.charCodeAt(0)) && !isOws(value.charCodeAt(last))) return value;
|
|
46
|
+
let start = 0;
|
|
47
|
+
let end = last;
|
|
48
|
+
while (start <= end && isOws(value.charCodeAt(start))) start++;
|
|
49
|
+
while (end >= start && isOws(value.charCodeAt(end))) end--;
|
|
50
|
+
return value.slice(start, end + 1);
|
|
51
|
+
}
|
|
18
52
|
const CONTENT_TYPE_CACHE_MAX = 64;
|
|
19
53
|
const contentTypeCache = /* @__PURE__ */ new Map();
|
|
20
54
|
/**
|
|
@@ -38,6 +72,26 @@ function resolveContentType(type) {
|
|
|
38
72
|
if (contentTypeCache.size < CONTENT_TYPE_CACHE_MAX) contentTypeCache.set(type, resolved);
|
|
39
73
|
return resolved;
|
|
40
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Valeur d'en-tête sous la forme que Node accepte (une liste figée est copiée),
|
|
77
|
+
* rognée de ses espaces de bord.
|
|
78
|
+
*/
|
|
79
|
+
function toHeaderValue(value) {
|
|
80
|
+
if (typeof value === "number") return value;
|
|
81
|
+
if (typeof value === "string") return trimFieldValue(value);
|
|
82
|
+
return value.map(trimFieldValue);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Contrôle d'un en-tête comme le fait `setHeader` natif — nom, puis chaque
|
|
86
|
+
* valeur (un nombre est toujours valide).
|
|
87
|
+
*
|
|
88
|
+
* @throws TypeError `ERR_INVALID_HTTP_TOKEN` / `ERR_INVALID_CHAR`.
|
|
89
|
+
*/
|
|
90
|
+
function assertHeader(name, value) {
|
|
91
|
+
validateHeaderName(name);
|
|
92
|
+
if (typeof value === "string") validateHeaderValue(name, value);
|
|
93
|
+
else if (typeof value !== "number") for (const item of value) validateHeaderValue(name, item);
|
|
94
|
+
}
|
|
41
95
|
var HttpResponse = class {
|
|
42
96
|
context;
|
|
43
97
|
response;
|
|
@@ -52,6 +106,27 @@ var HttpResponse = class {
|
|
|
52
106
|
headers = {};
|
|
53
107
|
timeout;
|
|
54
108
|
cookies = {};
|
|
109
|
+
/**
|
|
110
|
+
* En-têtes posés, remis TELS QUELS en un seul `writeHead(statut, liste)` :
|
|
111
|
+
* un `setHeader` natif par en-tête coûtait validation, rangement dans la
|
|
112
|
+
* table de Node puis relecture au `writeHead` — 41 % de plus pour le même
|
|
113
|
+
* bloc (`micro-write-head.mjs`). Et un seul `setHeader` natif avant le
|
|
114
|
+
* `writeHead` suffit à faire retomber Node sur son chemin lent.
|
|
115
|
+
* `null` tant que rien n'est posé ; amorcée avec les en-têtes du transport.
|
|
116
|
+
*/
|
|
117
|
+
#headers = null;
|
|
118
|
+
/**
|
|
119
|
+
* Pare-feu qui posera les en-têtes de sécurité applicatifs juste avant
|
|
120
|
+
* l'émission des en-têtes, une fois le type de la réponse fixé : la politique
|
|
121
|
+
* suit ce type (ADR-0024 §C11). Armé par le noyau HTTP au routage, consommé
|
|
122
|
+
* une fois ; `null` sans module de sécurité.
|
|
123
|
+
*/
|
|
124
|
+
headerPolicy = null;
|
|
125
|
+
/**
|
|
126
|
+
* Vrai une fois la réserve recopiée sur la réponse Node ({@link commitHeaders}) :
|
|
127
|
+
* le transport y est déjà, il ne s'amorce plus.
|
|
128
|
+
*/
|
|
129
|
+
#committed = false;
|
|
55
130
|
constructor(response, context) {
|
|
56
131
|
this.context = context;
|
|
57
132
|
this.response = response;
|
|
@@ -68,16 +143,16 @@ var HttpResponse = class {
|
|
|
68
143
|
* contenu (RFC 9110 §8.3), et il n'y en a pas.
|
|
69
144
|
*/
|
|
70
145
|
ensureContentTypeHeader() {
|
|
71
|
-
if (this.response && !this.
|
|
146
|
+
if (this.response && !this.hasHeader("content-type") && !this.#isBodyless()) this.setHeader("content-type", this.contentType);
|
|
72
147
|
}
|
|
73
148
|
/**
|
|
74
|
-
* Vrai quand la réponse n'a pas de corps : 204 et 304 n'en ont jamais
|
|
75
|
-
* (RFC 9110 §15.3.5, §15.4.5) ; ailleurs, un corps posé et VIDE. Un
|
|
76
|
-
* jamais posé ne dit rien — un flux peut suivre l'en-tête — et `HEAD`
|
|
77
|
-
* les en-têtes du `GET` (§9.3.2) : tous deux gardent le défaut.
|
|
149
|
+
* Vrai quand la réponse n'a pas de corps : 204, 205 et 304 n'en ont jamais
|
|
150
|
+
* (RFC 9110 §15.3.5, §15.3.6, §15.4.5) ; ailleurs, un corps posé et VIDE. Un
|
|
151
|
+
* corps jamais posé ne dit rien — un flux peut suivre l'en-tête — et `HEAD`
|
|
152
|
+
* rend les en-têtes du `GET` (§9.3.2) : tous deux gardent le défaut.
|
|
78
153
|
*/
|
|
79
154
|
#isBodyless() {
|
|
80
|
-
if (
|
|
155
|
+
if (NO_CONTENT_STATUS.has(this.statusCode)) return true;
|
|
81
156
|
if (this.context.method === "HEAD") return false;
|
|
82
157
|
if (this.#text !== null) return this.#text === "" && !this.flushing;
|
|
83
158
|
return this.#buffer?.length === 0 && !this.flushing;
|
|
@@ -100,9 +175,12 @@ var HttpResponse = class {
|
|
|
100
175
|
/**
|
|
101
176
|
* Corps tel qu'il part sur le fil : le texte s'il n'a jamais été encodé, sinon
|
|
102
177
|
* les octets — vide légal si rien n'a été posé (`write(null)` jetterait
|
|
103
|
-
* ERR_STREAM_NULL_VALUES).
|
|
178
|
+
* ERR_STREAM_NULL_VALUES). Vide aussi sous un statut sans contenu : un corps
|
|
179
|
+
* posé par l'application sous un 205 ne part pas (RFC 9110 §15.3.6) — Node
|
|
180
|
+
* ne l'écarte de lui-même que pour 204 et 304.
|
|
104
181
|
*/
|
|
105
182
|
get payload() {
|
|
183
|
+
if (NO_CONTENT_STATUS.has(this.statusCode)) return "";
|
|
106
184
|
return this.#text ?? this.#buffer ?? "";
|
|
107
185
|
}
|
|
108
186
|
/** Encodage à passer avec {@link payload} (ignoré par Node pour des octets). */
|
|
@@ -113,6 +191,7 @@ var HttpResponse = class {
|
|
|
113
191
|
this.response = null;
|
|
114
192
|
this.body = null;
|
|
115
193
|
this.cookies = {};
|
|
194
|
+
this.#headers = null;
|
|
116
195
|
}
|
|
117
196
|
isHeaderSent() {
|
|
118
197
|
if (this.response) return this.response.headersSent;
|
|
@@ -126,7 +205,10 @@ var HttpResponse = class {
|
|
|
126
205
|
this.timeout = ms;
|
|
127
206
|
}
|
|
128
207
|
addCookie(cookie) {
|
|
129
|
-
if (cookie instanceof Cookie)
|
|
208
|
+
if (cookie instanceof Cookie) {
|
|
209
|
+
cookie.assertGrammar();
|
|
210
|
+
return this.cookies[cookie.name] = cookie;
|
|
211
|
+
}
|
|
130
212
|
throw new Error("Response addCookies not valid cookies");
|
|
131
213
|
}
|
|
132
214
|
deleteCookie(cookie) {
|
|
@@ -150,19 +232,22 @@ var HttpResponse = class {
|
|
|
150
232
|
const cookies = Object.values(this.cookies);
|
|
151
233
|
const first = cookies.at(0);
|
|
152
234
|
if (first === void 0) return;
|
|
153
|
-
if (cookies.length === 1)
|
|
235
|
+
if (cookies.length === 1) {
|
|
236
|
+
this.setCookie(first);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
154
239
|
const serialized = [];
|
|
155
240
|
for (const cookie of cookies) {
|
|
156
241
|
const s = cookie.serialize();
|
|
157
242
|
this.logCookie(cookie, s);
|
|
158
243
|
serialized.push(s);
|
|
159
244
|
}
|
|
160
|
-
|
|
245
|
+
this.setHeader("Set-Cookie", serialized);
|
|
161
246
|
}
|
|
162
247
|
setCookie(cookie) {
|
|
163
248
|
const serialize = cookie.serialize();
|
|
164
249
|
this.logCookie(cookie, serialize);
|
|
165
|
-
|
|
250
|
+
this.setHeader("Set-Cookie", serialize);
|
|
166
251
|
}
|
|
167
252
|
/**
|
|
168
253
|
* Journalise en DEBUG un cookie posé, sa VALEUR MASQUÉE : nom et attributs
|
|
@@ -181,22 +266,189 @@ var HttpResponse = class {
|
|
|
181
266
|
const attrs = serialized.indexOf("; ", cookie.name.length + 1);
|
|
182
267
|
this.log(`ADD COOKIE ==> ${cookie.name}=<masqué>${attrs === -1 ? "" : serialized.slice(attrs)}`, "DEBUG", msgid);
|
|
183
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* Pose un en-tête de la réponse ; en flux chunké déjà ouvert, le range en
|
|
271
|
+
* trailer. Le nom et la valeur sont VALIDÉS ici, comme le ferait Node : une
|
|
272
|
+
* valeur invalide (saut de ligne…) lève tout de suite et n'entre jamais dans
|
|
273
|
+
* la réserve — sinon elle lèverait au `writeHead`, et la page d'erreur
|
|
274
|
+
* rejouerait le même en-tête.
|
|
275
|
+
*
|
|
276
|
+
* @param name - nom de l'en-tête (casse indifférente).
|
|
277
|
+
* @param value - valeur ; une liste émet une ligne par élément (`Set-Cookie`).
|
|
278
|
+
* @throws TypeError `ERR_INVALID_HTTP_TOKEN` / `ERR_INVALID_CHAR` sur un nom
|
|
279
|
+
* ou une valeur qu'un en-tête HTTP ne peut pas porter.
|
|
280
|
+
*/
|
|
184
281
|
setHeader(name, value) {
|
|
185
282
|
if (this.response) {
|
|
186
283
|
if (this.flushing) {
|
|
187
284
|
const obj = {};
|
|
188
|
-
obj[name] = value;
|
|
285
|
+
obj[name] = toHeaderValue(value);
|
|
189
286
|
this.addTrailers(obj);
|
|
190
287
|
return;
|
|
191
288
|
}
|
|
192
289
|
if (!this.response.headersSent) {
|
|
290
|
+
assertHeader(name, value);
|
|
193
291
|
const lower = name.toLowerCase();
|
|
194
|
-
|
|
195
|
-
return this.response.setHeader(lower, value);
|
|
292
|
+
this.#put(lower, lower === "vary" ? this.#mergeVary(value) : toHeaderValue(value));
|
|
196
293
|
}
|
|
197
294
|
}
|
|
198
295
|
}
|
|
199
296
|
/**
|
|
297
|
+
* Pose un en-tête dont la valeur a DÉJÀ été validée par l'appelant — une
|
|
298
|
+
* table figée et contrôlée au démarrage (en-têtes de sécurité). Node la
|
|
299
|
+
* validera une fois, au `writeHead` ; la revalider ici effaçait le gain de
|
|
300
|
+
* l'écriture groupée (`micro-write-head.mjs`, ligne « plat validé »).
|
|
301
|
+
*
|
|
302
|
+
* ⚠️ Jamais pour une valeur venue d'une requête : elle passe par {@link setHeader}.
|
|
303
|
+
*
|
|
304
|
+
* @param name - nom de l'en-tête (casse indifférente).
|
|
305
|
+
* @param value - valeur contrôlée au démarrage.
|
|
306
|
+
*/
|
|
307
|
+
setValidatedHeader(name, value) {
|
|
308
|
+
if (this.response === null || this.response.headersSent) return;
|
|
309
|
+
if (this.flushing) {
|
|
310
|
+
this.setHeader(name, value);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
this.#put(name.toLowerCase(), value);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Pose un en-tête validé au démarrage SAUF si la réponse en porte déjà un :
|
|
317
|
+
* la valeur du framework est un défaut, celle que l'application a écrite
|
|
318
|
+
* gagne. Même coût que {@link setValidatedHeader} — le rangement cherche
|
|
319
|
+
* l'en-tête de toute façon.
|
|
320
|
+
*
|
|
321
|
+
* @param name - nom de l'en-tête (casse indifférente).
|
|
322
|
+
* @param value - valeur contrôlée au démarrage.
|
|
323
|
+
*/
|
|
324
|
+
setDefaultHeader(name, value) {
|
|
325
|
+
if (this.response === null || this.response.headersSent) return;
|
|
326
|
+
const lower = name.toLowerCase();
|
|
327
|
+
if (this.flushing) {
|
|
328
|
+
if (!this.hasHeader(lower)) this.setHeader(name, value);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (this.#indexOf(lower) === -1) this.#store().push(lower, value);
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Pose les en-têtes de la politique armée ({@link headerPolicy}) pour une
|
|
335
|
+
* réponse de type `contentType` — une fois, juste avant l'émission.
|
|
336
|
+
*
|
|
337
|
+
* Un `304` n'en reçoit aucun : ses en-têtes REMPLACENT ceux de la
|
|
338
|
+
* représentation en cache (RFC 9111 §4.3.4). Une politique courte y
|
|
339
|
+
* réduirait la page gardée, un nonce neuf la désaccorderait de ses balises.
|
|
340
|
+
*
|
|
341
|
+
* @param contentType - type de la réponse ; `null` = sans contenu ; `undefined`
|
|
342
|
+
* = inconnu (politique complète).
|
|
343
|
+
* @param servedAsFile - corps écrit par un tiers (fichier statique, relais),
|
|
344
|
+
* qui ne peut pas porter de nonce.
|
|
345
|
+
*/
|
|
346
|
+
applyHeaderPolicy(contentType, servedAsFile = false) {
|
|
347
|
+
const policy = this.headerPolicy;
|
|
348
|
+
if (policy === null) return;
|
|
349
|
+
this.headerPolicy = null;
|
|
350
|
+
if (this.statusCode === 304) return;
|
|
351
|
+
policy.applySecurityHeaders(this.context, contentType, servedAsFile);
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Type qui choisit la politique : `null` pour une réponse sans contenu (204,
|
|
355
|
+
* corps vide) — même si un `Content-Type` y a été posé, rien n'y sera rendu ;
|
|
356
|
+
* sinon le `Content-Type` posé.
|
|
357
|
+
*/
|
|
358
|
+
contentTypeForPolicy() {
|
|
359
|
+
if (this.#isBodyless()) return null;
|
|
360
|
+
const value = this.getHeader("content-type");
|
|
361
|
+
return typeof value === "string" ? value : null;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* La réserve, amorcée au premier usage avec les en-têtes du transport
|
|
365
|
+
* (`Server`, `X-Content-Type-Options`, `X-Frame-Options`, HSTS) que le
|
|
366
|
+
* noyau a figés au démarrage — posés EN TÊTE, comme à l'entrée de la requête.
|
|
367
|
+
*/
|
|
368
|
+
#store() {
|
|
369
|
+
if (this.#headers === null) {
|
|
370
|
+
const transport = this.#committed ? null : this.context.httpKernel?.transportHeaders(this.context.serverType);
|
|
371
|
+
this.#headers = transport ? [...transport] : [];
|
|
372
|
+
}
|
|
373
|
+
return this.#headers;
|
|
374
|
+
}
|
|
375
|
+
/** Position du nom (minuscules) dans la réserve, ou -1. */
|
|
376
|
+
#indexOf(lower) {
|
|
377
|
+
const list = this.#headers;
|
|
378
|
+
if (list === null) return -1;
|
|
379
|
+
for (let i = 0; i < list.length; i += 2) if (list[i] === lower) return i;
|
|
380
|
+
return -1;
|
|
381
|
+
}
|
|
382
|
+
/** Range un en-tête, en remplaçant sa valeur s'il est déjà posé. */
|
|
383
|
+
#put(lower, value) {
|
|
384
|
+
const list = this.#store();
|
|
385
|
+
const at = this.#indexOf(lower);
|
|
386
|
+
if (at === -1) list.push(lower, value);
|
|
387
|
+
else list[at + 1] = value;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Recopie la réserve sur la réponse Node, pour un émetteur qui écrit sans
|
|
391
|
+
* passer par {@link writeHead} — fichier statique, relais vers un amont. La
|
|
392
|
+
* réserve se vide ; ce qui se pose ensuite y retourne, et Node fusionne les
|
|
393
|
+
* deux au `writeHead`.
|
|
394
|
+
*/
|
|
395
|
+
commitHeaders() {
|
|
396
|
+
const res = this.response;
|
|
397
|
+
if (res === null || res.headersSent) return;
|
|
398
|
+
if (this.headerPolicy !== null) {
|
|
399
|
+
const pathname = this.context.request.pathname;
|
|
400
|
+
const type = pathname.endsWith("/") ? false : mime.lookup(pathname);
|
|
401
|
+
this.applyHeaderPolicy(type === false ? void 0 : type, true);
|
|
402
|
+
}
|
|
403
|
+
const list = this.#store();
|
|
404
|
+
for (let i = 0; i + 1 < list.length; i += 2) {
|
|
405
|
+
const name = list[i];
|
|
406
|
+
const value = list[i + 1];
|
|
407
|
+
if (typeof name === "string" && value !== void 0) res.setHeader(name, value);
|
|
408
|
+
}
|
|
409
|
+
this.#headers = null;
|
|
410
|
+
this.#committed = true;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Recopie la réserve sur la réponse Node au PREMIER `setHeader` qu'un tiers
|
|
414
|
+
* y fera — fichier statique servi, redirection de dossier — et seulement
|
|
415
|
+
* alors : un raté du repli statique (la 404 d'une route inconnue) ne recopie
|
|
416
|
+
* rien, sa réponse part ensuite en un seul `writeHead`. À désarmer par
|
|
417
|
+
* {@link disarmNativeWrite} quand le tiers rend la main sans avoir écrit.
|
|
418
|
+
*
|
|
419
|
+
* L'interception est une propriété PROPRE de la réponse, ajoutée puis
|
|
420
|
+
* retirée : V8 annule alors la dernière transition de forme de l'objet.
|
|
421
|
+
*/
|
|
422
|
+
commitBeforeNativeWrite() {
|
|
423
|
+
const res = this.response;
|
|
424
|
+
if (res === null || res.headersSent || Object.hasOwn(res, "setHeader")) return;
|
|
425
|
+
const native = res.setHeader;
|
|
426
|
+
Object.defineProperty(res, "setHeader", {
|
|
427
|
+
configurable: true,
|
|
428
|
+
writable: true,
|
|
429
|
+
value: (name, value) => {
|
|
430
|
+
this.disarmNativeWrite();
|
|
431
|
+
this.commitHeaders();
|
|
432
|
+
return Reflect.apply(native, res, [name, value]);
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
/** Retire l'interception posée par {@link commitBeforeNativeWrite}. */
|
|
437
|
+
disarmNativeWrite() {
|
|
438
|
+
const res = this.response;
|
|
439
|
+
if (res !== null && Object.hasOwn(res, "setHeader")) Reflect.deleteProperty(res, "setHeader");
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Retire un en-tête, de la réserve comme de la réponse Node.
|
|
443
|
+
*
|
|
444
|
+
* @param name - nom de l'en-tête (casse indifférente).
|
|
445
|
+
*/
|
|
446
|
+
removeHeader(name) {
|
|
447
|
+
const at = this.#indexOf(name.toLowerCase());
|
|
448
|
+
if (at !== -1) this.#headers?.splice(at, 2);
|
|
449
|
+
if (this.response && !this.response.headersSent) this.response.removeHeader(name);
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
200
452
|
* Union des tokens `Vary` déjà posés et de ceux qu'on ajoute, sans doublon.
|
|
201
453
|
*
|
|
202
454
|
* `*` absorbe tout (RFC 9110 : « la réponse varie sur des paramètres non
|
|
@@ -207,7 +459,7 @@ var HttpResponse = class {
|
|
|
207
459
|
*/
|
|
208
460
|
#mergeVary(value) {
|
|
209
461
|
const incoming = (Array.isArray(value) ? value.join(",") : String(value)).split(",").map((t) => t.trim()).filter(Boolean);
|
|
210
|
-
const current = this.
|
|
462
|
+
const current = this.getHeader("vary");
|
|
211
463
|
const all = [...(Array.isArray(current) ? current.join(",") : String(current ?? "")).split(",").map((t) => t.trim()).filter(Boolean), ...incoming];
|
|
212
464
|
if (all.some((t) => t === "*")) return "*";
|
|
213
465
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -226,10 +478,10 @@ var HttpResponse = class {
|
|
|
226
478
|
const value = obj[head];
|
|
227
479
|
if (value !== void 0) this.setHeader(head, value);
|
|
228
480
|
}
|
|
229
|
-
return this.headers = this.
|
|
481
|
+
return this.headers = this.getHeaders();
|
|
230
482
|
}
|
|
231
483
|
this.log("headers already sended ", "WARNING");
|
|
232
|
-
return this.headers = this.
|
|
484
|
+
return this.headers = this.getHeaders();
|
|
233
485
|
}
|
|
234
486
|
setContentType(type, encoding) {
|
|
235
487
|
if (type && encoding) {
|
|
@@ -238,8 +490,12 @@ var HttpResponse = class {
|
|
|
238
490
|
const mytype = resolved.bare;
|
|
239
491
|
this.contentType = mytype;
|
|
240
492
|
this.encoding = encoding;
|
|
241
|
-
if (mytype === "application/json" || mytype.endsWith("+json"))
|
|
242
|
-
|
|
493
|
+
if (mytype === "application/json" || mytype.endsWith("+json")) {
|
|
494
|
+
this.setHeader("Content-Type", mytype);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
this.setHeader("Content-Type", `${mytype}; charset=${encoding}`);
|
|
498
|
+
return;
|
|
243
499
|
}
|
|
244
500
|
}
|
|
245
501
|
if (type && !encoding) {
|
|
@@ -249,10 +505,11 @@ var HttpResponse = class {
|
|
|
249
505
|
this.contentType = mytype;
|
|
250
506
|
const charset = resolved.charset;
|
|
251
507
|
if (charset) this.encoding = charset;
|
|
252
|
-
|
|
508
|
+
this.setHeader("Content-Type", mytype);
|
|
509
|
+
return;
|
|
253
510
|
}
|
|
254
511
|
}
|
|
255
|
-
|
|
512
|
+
this.setHeader("Content-Type", `${this.contentType}; charset=${this.encoding}`);
|
|
256
513
|
}
|
|
257
514
|
setFileMimeType(type, encoding) {
|
|
258
515
|
let myType = this.getMimeType(type);
|
|
@@ -261,7 +518,7 @@ var HttpResponse = class {
|
|
|
261
518
|
myType = "application/octet-stream";
|
|
262
519
|
}
|
|
263
520
|
this.contentType = myType;
|
|
264
|
-
|
|
521
|
+
this.setContentType(myType, encoding || this.encoding);
|
|
265
522
|
}
|
|
266
523
|
setContentTypeByExtension(extention) {
|
|
267
524
|
const ismime = mime.contentType(extention);
|
|
@@ -269,7 +526,8 @@ var HttpResponse = class {
|
|
|
269
526
|
this.contentType = ismime;
|
|
270
527
|
let charset = mime.charset(this.contentType);
|
|
271
528
|
if (charset) this.encoding = charset;
|
|
272
|
-
|
|
529
|
+
this.setHeader("Content-Type", ismime);
|
|
530
|
+
return;
|
|
273
531
|
}
|
|
274
532
|
this.log(`setContentTypeByExtension: ${extention} not found`, "WARNING");
|
|
275
533
|
}
|
|
@@ -342,14 +600,18 @@ var HttpResponse = class {
|
|
|
342
600
|
setLength(body) {
|
|
343
601
|
if (this.response?.headersSent) throw new Error("Headers already sended");
|
|
344
602
|
if (this.getHeader("Transfer-Encoding") === "chunked") {
|
|
345
|
-
this.
|
|
603
|
+
this.removeHeader("Content-Length");
|
|
346
604
|
return 0;
|
|
347
605
|
}
|
|
348
606
|
if (NO_CONTENT_LENGTH_STATUS.has(this.statusCode)) return 0;
|
|
607
|
+
if (this.statusCode === 205) {
|
|
608
|
+
this.#put("content-length", "0");
|
|
609
|
+
return 0;
|
|
610
|
+
}
|
|
349
611
|
const actualBody = body || this.#text || this.#buffer;
|
|
350
612
|
if (actualBody) {
|
|
351
613
|
const length = typeof actualBody === "string" && actualBody === this.#text ? Buffer.byteLength(actualBody, this.#textEncoding) : Buffer.byteLength(actualBody);
|
|
352
|
-
this
|
|
614
|
+
this.#put("content-length", String(length));
|
|
353
615
|
return length;
|
|
354
616
|
}
|
|
355
617
|
return 0;
|
|
@@ -362,27 +624,69 @@ var HttpResponse = class {
|
|
|
362
624
|
if (this.statusCode > 599) this.statusCode = 500;
|
|
363
625
|
}
|
|
364
626
|
this.statusMessage = this.getStatusMessage();
|
|
365
|
-
if (
|
|
366
|
-
if (this.context.
|
|
627
|
+
if (headers) this.#apply(headers);
|
|
628
|
+
if (this.context.requestId) this.#put("x-request-id", this.context.requestId);
|
|
629
|
+
if (this.context.traceparent) this.#put("traceparent", this.context.traceparent);
|
|
367
630
|
this.setLength();
|
|
368
631
|
this.ensureContentTypeHeader();
|
|
632
|
+
this.applyHeaderPolicy(this.contentTypeForPolicy());
|
|
633
|
+
const list = this.#store();
|
|
369
634
|
const std = http.STATUS_CODES[this.statusCode];
|
|
370
|
-
if (this.statusMessage === std) this.response.writeHead(this.statusCode,
|
|
635
|
+
if (this.statusMessage === std) this.response.writeHead(this.statusCode, list);
|
|
371
636
|
else {
|
|
372
637
|
const safeMsg = this.statusMessage.replace(/[^\x20-\x7E]/g, "").trim() || (std ?? "Unknown Error");
|
|
373
|
-
this.response.writeHead(this.statusCode, safeMsg,
|
|
638
|
+
this.response.writeHead(this.statusCode, safeMsg, list);
|
|
374
639
|
}
|
|
375
640
|
} else {
|
|
376
641
|
this.log("Headers already sent !!", "WARNING");
|
|
377
642
|
throw new Error(`Headers already sent !!`);
|
|
378
643
|
}
|
|
379
644
|
}
|
|
645
|
+
/**
|
|
646
|
+
* Range dans la réserve les en-têtes passés à `writeHead` — ils l'emportent
|
|
647
|
+
* sur ceux déjà posés, comme chez Node.
|
|
648
|
+
*/
|
|
649
|
+
#apply(headers) {
|
|
650
|
+
if (Array.isArray(headers)) {
|
|
651
|
+
for (let i = 0; i + 1 < headers.length; i += 2) {
|
|
652
|
+
const value = headers[i + 1];
|
|
653
|
+
if (value !== void 0) this.setHeader(String(headers[i]), value);
|
|
654
|
+
}
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
for (const name in headers) {
|
|
658
|
+
const value = headers[name];
|
|
659
|
+
if (value !== void 0) this.setHeader(name, value);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
/** La réserve amorcée — pour une remise sous une autre forme (HTTP/2). */
|
|
663
|
+
headerList() {
|
|
664
|
+
return this.#store();
|
|
665
|
+
}
|
|
380
666
|
addTrailers(headers) {
|
|
381
667
|
return this.response?.addTrailers(headers);
|
|
382
668
|
}
|
|
669
|
+
/**
|
|
670
|
+
* Écrit un morceau d'une réponse en flux, sans la terminer — c'est `end()`
|
|
671
|
+
* qui la clôt après le dernier.
|
|
672
|
+
*
|
|
673
|
+
* Au premier morceau, les en-têtes partent SANS longueur ni corps posé (le
|
|
674
|
+
* patron de `SseStream`), et le cadrage revient à Node : `chunked` pour un
|
|
675
|
+
* client HTTP/1.1, fermeture de connexion pour un HTTP/1.0 (RFC 9112 §6.1 —
|
|
676
|
+
* pas de `Transfer-Encoding` à qui ne le connaît pas), trames DATA en HTTP/2
|
|
677
|
+
* où l'en-tête est interdit (RFC 9113 §8.2.2). Ne JAMAIS le poser à la main :
|
|
678
|
+
* une fois `flushing` levé, `setHeader` range en trailer, et un
|
|
679
|
+
* `Transfer-Encoding` en trailer est un message malformé (RFC 9110 §6.5.1).
|
|
680
|
+
*
|
|
681
|
+
* @param chunk - le morceau à écrire.
|
|
682
|
+
* @param encoding - l'encodage d'un morceau texte.
|
|
683
|
+
* @returns la promesse réglée quand le flux accepte l'écriture suivante.
|
|
684
|
+
*/
|
|
383
685
|
flush(chunk, encoding) {
|
|
384
|
-
this.flushing
|
|
385
|
-
|
|
686
|
+
if (!this.flushing) {
|
|
687
|
+
if (!this.isHeaderSent()) this.writeHead();
|
|
688
|
+
this.flushing = true;
|
|
689
|
+
}
|
|
386
690
|
return this.send(chunk, encoding, true);
|
|
387
691
|
}
|
|
388
692
|
/**
|
|
@@ -445,15 +749,30 @@ var HttpResponse = class {
|
|
|
445
749
|
if (!this.response) throw new Error(`response not found`);
|
|
446
750
|
return this.response.end(chunk, encoding || this.encoding);
|
|
447
751
|
}
|
|
752
|
+
/**
|
|
753
|
+
* Valeur d'un en-tête posé — dans la réserve, sinon sur la réponse Node (ce
|
|
754
|
+
* qu'un tiers y a écrit directement : `send`, quota).
|
|
755
|
+
*
|
|
756
|
+
* @param name - nom de l'en-tête (casse indifférente).
|
|
757
|
+
*/
|
|
448
758
|
getHeader(name) {
|
|
759
|
+
const at = this.#indexOf(name.toLowerCase());
|
|
760
|
+
if (at !== -1) return this.#headers?.[at + 1];
|
|
449
761
|
return this.response?.getHeader(name);
|
|
450
762
|
}
|
|
451
763
|
hasHeader(name) {
|
|
452
|
-
if (this.response) return this.response.hasHeader(name);
|
|
764
|
+
if (this.response) return this.#indexOf(name.toLowerCase()) !== -1 || this.response.hasHeader(name);
|
|
453
765
|
throw new Error(`Respose not foud`);
|
|
454
766
|
}
|
|
767
|
+
/** Tous les en-têtes posés, noms en minuscules — une copie, à chaque appel. */
|
|
455
768
|
getHeaders() {
|
|
456
|
-
|
|
769
|
+
const out = { ...this.response?.getHeaders() };
|
|
770
|
+
const list = this.#headers;
|
|
771
|
+
if (list !== null) for (let i = 0; i + 1 < list.length; i += 2) {
|
|
772
|
+
const name = list[i];
|
|
773
|
+
if (typeof name === "string") out[name] = list[i + 1];
|
|
774
|
+
}
|
|
775
|
+
return out;
|
|
457
776
|
}
|
|
458
777
|
redirect(url, status, headers) {
|
|
459
778
|
this.context.isRedirect = true;
|
|
@@ -481,4 +800,4 @@ var HttpResponse = class {
|
|
|
481
800
|
return this.context.log(pci, severity, msgid, msg);
|
|
482
801
|
}
|
|
483
802
|
};
|
|
484
|
-
export { HttpResponse as default };
|
|
803
|
+
export { HttpResponse as default, isContentlessStatus };
|