@nodefony/http 10.0.0-beta.5 → 10.0.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +30 -26
- package/dist/nodefony/command/proxyGenerateCommand.js +2 -2
- package/dist/nodefony/config/config.js +29 -4
- package/dist/nodefony/grpc/GrpcCall.js +298 -0
- package/dist/nodefony/grpc/GrpcEndpoint.js +528 -0
- package/dist/nodefony/grpc/index.js +4 -0
- package/dist/nodefony/interfaces/IRpcCall.js +1 -0
- package/dist/nodefony/service/ProfilerAdminApi.js +5 -2
- package/dist/nodefony/service/audit-logger.js +52 -6
- package/dist/nodefony/service/error-renderer.js +47 -13
- package/dist/nodefony/service/http-kernel.js +306 -49
- package/dist/nodefony/service/pretty-request-logger.js +5 -3
- package/dist/nodefony/service/request-logger.js +21 -6
- package/dist/nodefony/service/reverse-proxy.js +8 -3
- package/dist/nodefony/service/servers/clientError.js +53 -4
- package/dist/nodefony/service/servers/server-h2c.js +6 -6
- package/dist/nodefony/service/servers/server-http.js +6 -4
- package/dist/nodefony/service/servers/server-https.js +14 -7
- package/dist/nodefony/service/servers/server-static.js +1 -6
- package/dist/nodefony/service/servers/server-websocket-secure.js +7 -11
- package/dist/nodefony/service/servers/server-websocket.js +7 -11
- package/dist/nodefony/service/servers/serverShutdown.js +43 -6
- package/dist/nodefony/service/sessions/sessions-service.js +7 -7
- package/dist/nodefony/service/upload/upload-service.js +2 -2
- package/dist/nodefony/src/assets/prebuiltUi.js +23 -5
- package/dist/nodefony/src/context/Context.js +12 -1
- package/dist/nodefony/src/context/http/ByteStream.js +364 -0
- package/dist/nodefony/src/context/http/HttpContext.js +139 -32
- package/dist/nodefony/src/context/http/Request.js +111 -22
- package/dist/nodefony/src/context/http/Response.js +459 -37
- package/dist/nodefony/src/context/http/SseStream.js +83 -148
- package/dist/nodefony/src/context/http/StreamLifecycle.js +582 -0
- package/dist/nodefony/src/context/http/parser.js +29 -11
- package/dist/nodefony/src/context/http/urlFastPath.js +80 -1
- package/dist/nodefony/src/context/http/webResponse.js +56 -0
- package/dist/nodefony/src/context/http2/Request.js +1 -1
- package/dist/nodefony/src/context/http2/Response.js +55 -12
- package/dist/nodefony/src/context/requestTrace.js +6 -2
- package/dist/nodefony/src/context/rpcOutcome.js +20 -0
- package/dist/nodefony/src/context/transport.js +20 -1
- package/dist/nodefony/src/context/websocket/Response.js +15 -3
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +86 -18
- 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 +57 -14
- package/dist/nodefony/src/errors/httpError.js +58 -25
- package/dist/nodefony/src/grpc/cors.js +59 -0
- package/dist/nodefony/src/grpc/limits.js +16 -0
- package/dist/nodefony/src/grpc/refusal.js +271 -0
- package/dist/nodefony/src/profiler/Profiler.js +14 -1
- package/dist/nodefony/src/profiler/aggregate.js +2 -2
- package/dist/nodefony/src/proxy/forward.js +109 -9
- package/dist/nodefony/src/proxy/generateProxyConfig.js +97 -17
- package/dist/nodefony/src/proxy/rules.js +29 -14
- package/dist/nodefony/src/servers/http2Goaway.js +76 -0
- package/dist/nodefony/src/servers/http2Limits.js +41 -15
- package/dist/nodefony/src/servers/http2StreamGrace.js +100 -0
- package/dist/nodefony/src/servers/receiveDeadline.js +177 -0
- package/dist/nodefony/src/servers/upgradeDispatch.js +62 -4
- package/dist/types/index.d.ts +11 -2
- package/dist/types/nodefony/config/config.d.ts +14 -0
- package/dist/types/nodefony/grpc/GrpcCall.d.ts +142 -0
- package/dist/types/nodefony/grpc/GrpcEndpoint.d.ts +98 -0
- package/dist/types/nodefony/grpc/index.d.ts +15 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +9 -0
- package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +14 -0
- package/dist/types/nodefony/interfaces/IRequest.d.ts +2 -1
- package/dist/types/nodefony/interfaces/IRequestTrace.d.ts +10 -0
- package/dist/types/nodefony/interfaces/IRouting.d.ts +19 -0
- package/dist/types/nodefony/interfaces/IRpcCall.d.ts +83 -0
- package/dist/types/nodefony/interfaces/ISecurity.d.ts +27 -2
- package/dist/types/nodefony/interfaces/IUpload.d.ts +51 -4
- package/dist/types/nodefony/interfaces/index.d.ts +2 -1
- package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +2 -1
- package/dist/types/nodefony/service/audit-logger.d.ts +33 -3
- package/dist/types/nodefony/service/error-renderer.d.ts +27 -5
- package/dist/types/nodefony/service/http-kernel.d.ts +36 -3
- package/dist/types/nodefony/service/request-logger.d.ts +10 -0
- package/dist/types/nodefony/service/servers/clientError.d.ts +3 -2
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +7 -5
- package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +9 -4
- package/dist/types/nodefony/src/context/Context.d.ts +9 -1
- package/dist/types/nodefony/src/context/http/ByteStream.d.ts +109 -0
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +99 -3
- package/dist/types/nodefony/src/context/http/Request.d.ts +46 -9
- package/dist/types/nodefony/src/context/http/Response.d.ts +188 -8
- package/dist/types/nodefony/src/context/http/SseStream.d.ts +33 -8
- package/dist/types/nodefony/src/context/http/StreamLifecycle.d.ts +106 -0
- package/dist/types/nodefony/src/context/http/parser.d.ts +17 -3
- package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +40 -0
- package/dist/types/nodefony/src/context/http/webResponse.d.ts +31 -0
- package/dist/types/nodefony/src/context/http2/Response.d.ts +40 -5
- package/dist/types/nodefony/src/context/requestTrace.d.ts +3 -1
- package/dist/types/nodefony/src/context/rpcOutcome.d.ts +11 -0
- package/dist/types/nodefony/src/context/transport.d.ts +17 -0
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +6 -1
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +48 -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/errors/httpError.d.ts +29 -4
- package/dist/types/nodefony/src/grpc/cors.d.ts +32 -0
- package/dist/types/nodefony/src/grpc/limits.d.ts +14 -0
- package/dist/types/nodefony/src/grpc/refusal.d.ts +136 -0
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +11 -1
- package/dist/types/nodefony/src/proxy/forward.d.ts +37 -2
- package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +15 -2
- package/dist/types/nodefony/src/proxy/rules.d.ts +20 -0
- package/dist/types/nodefony/src/servers/http2Goaway.d.ts +41 -0
- package/dist/types/nodefony/src/servers/http2Limits.d.ts +27 -14
- package/dist/types/nodefony/src/servers/http2StreamGrace.d.ts +34 -0
- package/dist/types/nodefony/src/servers/receiveDeadline.d.ts +77 -0
- package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +41 -1
- package/docs/byte-stream.md +406 -0
- package/docs/cookies.md +13 -3
- package/docs/diagnostics-channel.md +45 -1
- package/docs/index.md +8 -0
- package/docs/reverse-proxy.md +15 -3
- package/docs/servers.md +67 -43
- package/docs/session.md +3 -3
- package/docs/sse.md +27 -20
- package/docs/upload.md +43 -17
- package/package.json +38 -18
|
@@ -1,8 +1,43 @@
|
|
|
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
|
+
/**
|
|
7
|
+
* Plafond, en octets, d'un corps de requête HTTP/1.1 non lu que le serveur
|
|
8
|
+
* accepte de vider pour garder la connexion réutilisable — 256 Kio, le geste du
|
|
9
|
+
* serveur HTTP de Go que Connect reprend (ADR-0026, D5). Au-delà, la réponse
|
|
10
|
+
* ferme la connexion.
|
|
11
|
+
*/
|
|
12
|
+
const UNREAD_BODY_DRAIN_LIMIT = 262144;
|
|
13
|
+
/**
|
|
14
|
+
* Silence, en millisecondes, au bout duquel une connexion HTTP/1.1 fermée sur
|
|
15
|
+
* un corps non lu est détruite — 5 s, le `lingering_timeout` par défaut de
|
|
16
|
+
* nginx. La durée TOTALE reste bornée par le délai de réception de Node
|
|
17
|
+
* (`requestTimeout`), qui court tant que le corps n'est pas fini.
|
|
18
|
+
*/
|
|
19
|
+
const UNREAD_BODY_LINGER_MS = 5e3;
|
|
20
|
+
/**
|
|
21
|
+
* Fermeture ATTARDÉE (RFC 9112 §9.6) d'une connexion qui part en
|
|
22
|
+
* `connection: close` avec un corps encore en route : la réponse écrite, le
|
|
23
|
+
* serveur ferme son côté écriture et continue de LIRE — Node jette les octets
|
|
24
|
+
* du corps non consommé — jusqu'à ce que le client ferme, ou après
|
|
25
|
+
* {@link UNREAD_BODY_LINGER_MS} de silence.
|
|
26
|
+
*
|
|
27
|
+
* Sans elle, Node détruit la socket aussitôt la réponse écrite
|
|
28
|
+
* (`destroySoon`) : le client qui écrit encore son corps prend une
|
|
29
|
+
* réinitialisation (`EPIPE`, `ECONNRESET`) et ne lit jamais la réponse — un
|
|
30
|
+
* 413 sur un envoi de 5 Mo devenait une erreur réseau. Posée sur CETTE socket
|
|
31
|
+
* seulement, qui ne sert plus d'autre requête.
|
|
32
|
+
*
|
|
33
|
+
* @param socket - la socket de la connexion qui se ferme.
|
|
34
|
+
*/
|
|
35
|
+
function lingerBeforeClose(socket) {
|
|
36
|
+
socket.destroySoon = () => {
|
|
37
|
+
socket.setTimeout(UNREAD_BODY_LINGER_MS, () => socket.destroy());
|
|
38
|
+
socket.end();
|
|
39
|
+
};
|
|
40
|
+
}
|
|
6
41
|
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");
|
|
7
42
|
const stripAnsi = function(val) {
|
|
8
43
|
return typeof val === "string" ? val.replace(ANSI_REGEX, "") : val;
|
|
@@ -15,6 +50,40 @@ const REDIRECT_STATUS_CODES = /* @__PURE__ */ new Set([
|
|
|
15
50
|
308
|
|
16
51
|
]);
|
|
17
52
|
const NO_CONTENT_LENGTH_STATUS = /* @__PURE__ */ new Set([204, 304]);
|
|
53
|
+
const NO_CONTENT_STATUS = /* @__PURE__ */ new Set([
|
|
54
|
+
204,
|
|
55
|
+
205,
|
|
56
|
+
304
|
|
57
|
+
]);
|
|
58
|
+
/**
|
|
59
|
+
* Vrai pour un statut dont la réponse n'a, par définition, aucun contenu —
|
|
60
|
+
* 204, 205 et 304 (RFC 9110 §15.3.5, §15.3.6, §15.4.5).
|
|
61
|
+
*
|
|
62
|
+
* Seule source de cette liste : la réponse s'en sert pour ne jamais écrire de
|
|
63
|
+
* corps, le résolveur pour terminer une action qui ne rend rien.
|
|
64
|
+
*
|
|
65
|
+
* @param status - le code de statut de la réponse.
|
|
66
|
+
* @returns `true` quand aucun corps ne doit partir.
|
|
67
|
+
*/
|
|
68
|
+
function isContentlessStatus(status) {
|
|
69
|
+
return NO_CONTENT_STATUS.has(status);
|
|
70
|
+
}
|
|
71
|
+
/** Espace ou tabulation : l'OWS de RFC 9110 §5.6.3. */
|
|
72
|
+
const isOws = (code) => code === 32 || code === 9;
|
|
73
|
+
/**
|
|
74
|
+
* Valeur d'en-tête sans espace de bord (RFC 9110 §5.5 ; RFC 9113 §8.2.1 en fait
|
|
75
|
+
* un MUST NOT en HTTP/2). Ne copie la chaîne que dans le cas rare où il y a
|
|
76
|
+
* quelque chose à rogner.
|
|
77
|
+
*/
|
|
78
|
+
function trimFieldValue(value) {
|
|
79
|
+
const last = value.length - 1;
|
|
80
|
+
if (last < 0 || !isOws(value.charCodeAt(0)) && !isOws(value.charCodeAt(last))) return value;
|
|
81
|
+
let start = 0;
|
|
82
|
+
let end = last;
|
|
83
|
+
while (start <= end && isOws(value.charCodeAt(start))) start++;
|
|
84
|
+
while (end >= start && isOws(value.charCodeAt(end))) end--;
|
|
85
|
+
return value.slice(start, end + 1);
|
|
86
|
+
}
|
|
18
87
|
const CONTENT_TYPE_CACHE_MAX = 64;
|
|
19
88
|
const contentTypeCache = /* @__PURE__ */ new Map();
|
|
20
89
|
/**
|
|
@@ -38,6 +107,26 @@ function resolveContentType(type) {
|
|
|
38
107
|
if (contentTypeCache.size < CONTENT_TYPE_CACHE_MAX) contentTypeCache.set(type, resolved);
|
|
39
108
|
return resolved;
|
|
40
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Valeur d'en-tête sous la forme que Node accepte (une liste figée est copiée),
|
|
112
|
+
* rognée de ses espaces de bord.
|
|
113
|
+
*/
|
|
114
|
+
function toHeaderValue(value) {
|
|
115
|
+
if (typeof value === "number") return value;
|
|
116
|
+
if (typeof value === "string") return trimFieldValue(value);
|
|
117
|
+
return value.map(trimFieldValue);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Contrôle d'un en-tête comme le fait `setHeader` natif — nom, puis chaque
|
|
121
|
+
* valeur (un nombre est toujours valide).
|
|
122
|
+
*
|
|
123
|
+
* @throws TypeError `ERR_INVALID_HTTP_TOKEN` / `ERR_INVALID_CHAR`.
|
|
124
|
+
*/
|
|
125
|
+
function assertHeader(name, value) {
|
|
126
|
+
validateHeaderName(name);
|
|
127
|
+
if (typeof value === "string") validateHeaderValue(name, value);
|
|
128
|
+
else if (typeof value !== "number") for (const item of value) validateHeaderValue(name, item);
|
|
129
|
+
}
|
|
41
130
|
var HttpResponse = class {
|
|
42
131
|
context;
|
|
43
132
|
response;
|
|
@@ -46,12 +135,38 @@ var HttpResponse = class {
|
|
|
46
135
|
flushing = false;
|
|
47
136
|
encoding = "utf-8";
|
|
48
137
|
#text = null;
|
|
49
|
-
#textEncoding = "
|
|
138
|
+
#textEncoding = "utf8";
|
|
50
139
|
#buffer = null;
|
|
51
140
|
contentType = "application/octet-stream";
|
|
52
141
|
headers = {};
|
|
53
142
|
timeout;
|
|
54
143
|
cookies = {};
|
|
144
|
+
/**
|
|
145
|
+
* En-têtes posés, remis TELS QUELS en un seul `writeHead(statut, liste)` :
|
|
146
|
+
* un `setHeader` natif par en-tête coûtait validation, rangement dans la
|
|
147
|
+
* table de Node puis relecture au `writeHead` — 41 % de plus pour le même
|
|
148
|
+
* bloc (`micro-write-head.mjs`). Et un seul `setHeader` natif avant le
|
|
149
|
+
* `writeHead` suffit à faire retomber Node sur son chemin lent.
|
|
150
|
+
* `null` tant que rien n'est posé ; amorcée avec les en-têtes du transport.
|
|
151
|
+
*/
|
|
152
|
+
#headers = null;
|
|
153
|
+
/**
|
|
154
|
+
* Pare-feu qui posera les en-têtes de sécurité applicatifs juste avant
|
|
155
|
+
* l'émission des en-têtes, une fois le type de la réponse fixé : la politique
|
|
156
|
+
* suit ce type (ADR-0024 §C11). Armé par le noyau HTTP au routage, consommé
|
|
157
|
+
* une fois ; `null` sans module de sécurité.
|
|
158
|
+
*/
|
|
159
|
+
headerPolicy = null;
|
|
160
|
+
/**
|
|
161
|
+
* Vrai une fois la réserve recopiée sur la réponse Node ({@link commitHeaders}) :
|
|
162
|
+
* le transport y est déjà, il ne s'amorce plus.
|
|
163
|
+
*/
|
|
164
|
+
#committed = false;
|
|
165
|
+
/**
|
|
166
|
+
* Méthode `setHeader` native retenue pendant que l'interception du repli
|
|
167
|
+
* statique est armée ; `null` sinon (et sur toute réponse routée).
|
|
168
|
+
*/
|
|
169
|
+
#nativeSetHeader = null;
|
|
55
170
|
constructor(response, context) {
|
|
56
171
|
this.context = context;
|
|
57
172
|
this.response = response;
|
|
@@ -68,16 +183,16 @@ var HttpResponse = class {
|
|
|
68
183
|
* contenu (RFC 9110 §8.3), et il n'y en a pas.
|
|
69
184
|
*/
|
|
70
185
|
ensureContentTypeHeader() {
|
|
71
|
-
if (this.response && !this.
|
|
186
|
+
if (this.response && !this.hasHeader("content-type") && !this.#isBodyless()) this.setHeader("content-type", this.contentType);
|
|
72
187
|
}
|
|
73
188
|
/**
|
|
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.
|
|
189
|
+
* Vrai quand la réponse n'a pas de corps : 204, 205 et 304 n'en ont jamais
|
|
190
|
+
* (RFC 9110 §15.3.5, §15.3.6, §15.4.5) ; ailleurs, un corps posé et VIDE. Un
|
|
191
|
+
* corps jamais posé ne dit rien — un flux peut suivre l'en-tête — et `HEAD`
|
|
192
|
+
* rend les en-têtes du `GET` (§9.3.2) : tous deux gardent le défaut.
|
|
78
193
|
*/
|
|
79
194
|
#isBodyless() {
|
|
80
|
-
if (
|
|
195
|
+
if (NO_CONTENT_STATUS.has(this.statusCode)) return true;
|
|
81
196
|
if (this.context.method === "HEAD") return false;
|
|
82
197
|
if (this.#text !== null) return this.#text === "" && !this.flushing;
|
|
83
198
|
return this.#buffer?.length === 0 && !this.flushing;
|
|
@@ -100,9 +215,12 @@ var HttpResponse = class {
|
|
|
100
215
|
/**
|
|
101
216
|
* Corps tel qu'il part sur le fil : le texte s'il n'a jamais été encodé, sinon
|
|
102
217
|
* les octets — vide légal si rien n'a été posé (`write(null)` jetterait
|
|
103
|
-
* ERR_STREAM_NULL_VALUES).
|
|
218
|
+
* ERR_STREAM_NULL_VALUES). Vide aussi sous un statut sans contenu : un corps
|
|
219
|
+
* posé par l'application sous un 205 ne part pas (RFC 9110 §15.3.6) — Node
|
|
220
|
+
* ne l'écarte de lui-même que pour 204 et 304.
|
|
104
221
|
*/
|
|
105
222
|
get payload() {
|
|
223
|
+
if (NO_CONTENT_STATUS.has(this.statusCode)) return "";
|
|
106
224
|
return this.#text ?? this.#buffer ?? "";
|
|
107
225
|
}
|
|
108
226
|
/** Encodage à passer avec {@link payload} (ignoré par Node pour des octets). */
|
|
@@ -113,6 +231,7 @@ var HttpResponse = class {
|
|
|
113
231
|
this.response = null;
|
|
114
232
|
this.body = null;
|
|
115
233
|
this.cookies = {};
|
|
234
|
+
this.#headers = null;
|
|
116
235
|
}
|
|
117
236
|
isHeaderSent() {
|
|
118
237
|
if (this.response) return this.response.headersSent;
|
|
@@ -126,7 +245,10 @@ var HttpResponse = class {
|
|
|
126
245
|
this.timeout = ms;
|
|
127
246
|
}
|
|
128
247
|
addCookie(cookie) {
|
|
129
|
-
if (cookie instanceof Cookie)
|
|
248
|
+
if (cookie instanceof Cookie) {
|
|
249
|
+
cookie.assertGrammar();
|
|
250
|
+
return this.cookies[cookie.name] = cookie;
|
|
251
|
+
}
|
|
130
252
|
throw new Error("Response addCookies not valid cookies");
|
|
131
253
|
}
|
|
132
254
|
deleteCookie(cookie) {
|
|
@@ -150,19 +272,22 @@ var HttpResponse = class {
|
|
|
150
272
|
const cookies = Object.values(this.cookies);
|
|
151
273
|
const first = cookies.at(0);
|
|
152
274
|
if (first === void 0) return;
|
|
153
|
-
if (cookies.length === 1)
|
|
275
|
+
if (cookies.length === 1) {
|
|
276
|
+
this.setCookie(first);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
154
279
|
const serialized = [];
|
|
155
280
|
for (const cookie of cookies) {
|
|
156
281
|
const s = cookie.serialize();
|
|
157
282
|
this.logCookie(cookie, s);
|
|
158
283
|
serialized.push(s);
|
|
159
284
|
}
|
|
160
|
-
|
|
285
|
+
this.setHeader("Set-Cookie", serialized);
|
|
161
286
|
}
|
|
162
287
|
setCookie(cookie) {
|
|
163
288
|
const serialize = cookie.serialize();
|
|
164
289
|
this.logCookie(cookie, serialize);
|
|
165
|
-
|
|
290
|
+
this.setHeader("Set-Cookie", serialize);
|
|
166
291
|
}
|
|
167
292
|
/**
|
|
168
293
|
* Journalise en DEBUG un cookie posé, sa VALEUR MASQUÉE : nom et attributs
|
|
@@ -181,20 +306,219 @@ var HttpResponse = class {
|
|
|
181
306
|
const attrs = serialized.indexOf("; ", cookie.name.length + 1);
|
|
182
307
|
this.log(`ADD COOKIE ==> ${cookie.name}=<masqué>${attrs === -1 ? "" : serialized.slice(attrs)}`, "DEBUG", msgid);
|
|
183
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* Pose un en-tête de la réponse ; en flux chunké déjà ouvert, le range en
|
|
311
|
+
* trailer. Le nom et la valeur sont VALIDÉS ici, comme le ferait Node : une
|
|
312
|
+
* valeur invalide (saut de ligne…) lève tout de suite et n'entre jamais dans
|
|
313
|
+
* la réserve — sinon elle lèverait au `writeHead`, et la page d'erreur
|
|
314
|
+
* rejouerait le même en-tête.
|
|
315
|
+
*
|
|
316
|
+
* @param name - nom de l'en-tête (casse indifférente).
|
|
317
|
+
* @param value - valeur ; une liste émet une ligne par élément (`Set-Cookie`).
|
|
318
|
+
* @throws TypeError `ERR_INVALID_HTTP_TOKEN` / `ERR_INVALID_CHAR` sur un nom
|
|
319
|
+
* ou une valeur qu'un en-tête HTTP ne peut pas porter.
|
|
320
|
+
*/
|
|
184
321
|
setHeader(name, value) {
|
|
185
322
|
if (this.response) {
|
|
186
323
|
if (this.flushing) {
|
|
187
324
|
const obj = {};
|
|
188
|
-
obj[name] = value;
|
|
325
|
+
obj[name] = toHeaderValue(value);
|
|
189
326
|
this.addTrailers(obj);
|
|
190
327
|
return;
|
|
191
328
|
}
|
|
192
329
|
if (!this.response.headersSent) {
|
|
330
|
+
assertHeader(name, value);
|
|
193
331
|
const lower = name.toLowerCase();
|
|
194
|
-
|
|
195
|
-
|
|
332
|
+
this.#put(lower, lower === "vary" ? this.#mergeVary(value) : toHeaderValue(value));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Pose un en-tête dont la valeur a DÉJÀ été validée par l'appelant — une
|
|
338
|
+
* table figée et contrôlée au démarrage (en-têtes de sécurité). Node la
|
|
339
|
+
* validera une fois, au `writeHead` ; la revalider ici effaçait le gain de
|
|
340
|
+
* l'écriture groupée (`micro-write-head.mjs`, ligne « plat validé »).
|
|
341
|
+
*
|
|
342
|
+
* ⚠️ Jamais pour une valeur venue d'une requête : elle passe par {@link setHeader}.
|
|
343
|
+
*
|
|
344
|
+
* @param name - nom de l'en-tête (casse indifférente).
|
|
345
|
+
* @param value - valeur contrôlée au démarrage.
|
|
346
|
+
*/
|
|
347
|
+
setValidatedHeader(name, value) {
|
|
348
|
+
if (this.response === null || this.response.headersSent) return;
|
|
349
|
+
if (this.flushing) {
|
|
350
|
+
this.setHeader(name, value);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
this.#put(name.toLowerCase(), value);
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Pose un en-tête validé au démarrage SAUF si la réponse en porte déjà un :
|
|
357
|
+
* la valeur du framework est un défaut, celle que l'application a écrite
|
|
358
|
+
* gagne. Même coût que {@link setValidatedHeader} — le rangement cherche
|
|
359
|
+
* l'en-tête de toute façon.
|
|
360
|
+
*
|
|
361
|
+
* @param name - nom de l'en-tête (casse indifférente).
|
|
362
|
+
* @param value - valeur contrôlée au démarrage.
|
|
363
|
+
*/
|
|
364
|
+
setDefaultHeader(name, value) {
|
|
365
|
+
if (this.response === null || this.response.headersSent) return;
|
|
366
|
+
const lower = name.toLowerCase();
|
|
367
|
+
if (this.flushing) {
|
|
368
|
+
if (!this.hasHeader(lower)) this.setHeader(name, value);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (this.#indexOf(lower) === -1) this.#store().push(lower, value);
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Pose les en-têtes de la politique armée ({@link headerPolicy}) pour une
|
|
375
|
+
* réponse de type `contentType` — une fois, juste avant l'émission.
|
|
376
|
+
*
|
|
377
|
+
* Un `304` n'en reçoit aucun : ses en-têtes REMPLACENT ceux de la
|
|
378
|
+
* représentation en cache (RFC 9111 §4.3.4). Une politique courte y
|
|
379
|
+
* réduirait la page gardée, un nonce neuf la désaccorderait de ses balises.
|
|
380
|
+
*
|
|
381
|
+
* @param contentType - type de la réponse ; `null` = sans contenu ; `undefined`
|
|
382
|
+
* = inconnu (politique complète).
|
|
383
|
+
* @param servedAsFile - corps écrit par un tiers (fichier statique, relais),
|
|
384
|
+
* qui ne peut pas porter de nonce.
|
|
385
|
+
*/
|
|
386
|
+
applyHeaderPolicy(contentType, servedAsFile = false) {
|
|
387
|
+
const policy = this.headerPolicy;
|
|
388
|
+
if (policy === null) return;
|
|
389
|
+
this.headerPolicy = null;
|
|
390
|
+
if (this.statusCode === 304) return;
|
|
391
|
+
policy.applySecurityHeaders(this.context, contentType, servedAsFile);
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Type qui choisit la politique : `null` pour une réponse sans contenu (204,
|
|
395
|
+
* corps vide) — même si un `Content-Type` y a été posé, rien n'y sera rendu ;
|
|
396
|
+
* sinon le `Content-Type` posé.
|
|
397
|
+
*/
|
|
398
|
+
contentTypeForPolicy() {
|
|
399
|
+
if (this.#isBodyless()) return null;
|
|
400
|
+
const value = this.getHeader("content-type");
|
|
401
|
+
return typeof value === "string" ? value : null;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* La réserve, amorcée au premier usage avec les en-têtes du transport
|
|
405
|
+
* (`Server`, `X-Content-Type-Options`, `X-Frame-Options`, HSTS) que le
|
|
406
|
+
* noyau a figés au démarrage — posés EN TÊTE, comme à l'entrée de la requête.
|
|
407
|
+
*/
|
|
408
|
+
#store() {
|
|
409
|
+
if (this.#headers === null) {
|
|
410
|
+
const transport = this.#committed ? null : this.context.httpKernel?.transportHeaders(this.context.serverType);
|
|
411
|
+
this.#headers = transport ? [...transport] : [];
|
|
412
|
+
}
|
|
413
|
+
return this.#headers;
|
|
414
|
+
}
|
|
415
|
+
/** Position du nom (minuscules) dans la réserve, ou -1. */
|
|
416
|
+
#indexOf(lower) {
|
|
417
|
+
const list = this.#headers;
|
|
418
|
+
if (list === null) return -1;
|
|
419
|
+
for (let i = 0; i < list.length; i += 2) if (list[i] === lower) return i;
|
|
420
|
+
return -1;
|
|
421
|
+
}
|
|
422
|
+
/** Range un en-tête, en remplaçant sa valeur s'il est déjà posé. */
|
|
423
|
+
#put(lower, value) {
|
|
424
|
+
const list = this.#store();
|
|
425
|
+
const at = this.#indexOf(lower);
|
|
426
|
+
if (at === -1) list.push(lower, value);
|
|
427
|
+
else list[at + 1] = value;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Range les en-têtes de corrélation du contexte — `X-Request-Id` et le
|
|
431
|
+
* `traceparent` W3C — que toute réponse renvoie, quel qu'en soit l'émetteur.
|
|
432
|
+
* Validés par Node à la remise, comme le reste de la réserve.
|
|
433
|
+
*/
|
|
434
|
+
putCorrelationHeaders() {
|
|
435
|
+
if (this.context.requestId) this.#put("x-request-id", this.context.requestId);
|
|
436
|
+
if (this.context.traceparent) this.#put("traceparent", this.context.traceparent);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Recopie la réserve sur la réponse Node, pour un émetteur qui écrit sans
|
|
440
|
+
* passer par {@link writeHead} — fichier statique, relais vers un amont,
|
|
441
|
+
* adaptateur RPC. La réserve se vide ; ce qui se pose ensuite y retourne, et
|
|
442
|
+
* Node fusionne les deux au `writeHead` (l'émetteur l'emporte, nom par nom).
|
|
443
|
+
*
|
|
444
|
+
* @param contentType - type de la réponse que l'émetteur écrira, quand il
|
|
445
|
+
* est connu d'avance (adaptateur RPC). Omis : il se lit à l'extension du
|
|
446
|
+
* chemin, comme le serveur statique le fera.
|
|
447
|
+
*/
|
|
448
|
+
commitHeaders(contentType) {
|
|
449
|
+
const res = this.response;
|
|
450
|
+
if (res === null || res.headersSent) return;
|
|
451
|
+
if (this.headerPolicy !== null) {
|
|
452
|
+
let type = contentType;
|
|
453
|
+
if (type === void 0) {
|
|
454
|
+
const pathname = this.context.request.pathname;
|
|
455
|
+
const found = pathname.endsWith("/") ? false : mime.lookup(pathname);
|
|
456
|
+
if (found !== false) type = found;
|
|
196
457
|
}
|
|
458
|
+
this.applyHeaderPolicy(type, true);
|
|
197
459
|
}
|
|
460
|
+
this.putCorrelationHeaders();
|
|
461
|
+
const list = this.#store();
|
|
462
|
+
for (let i = 0; i + 1 < list.length; i += 2) {
|
|
463
|
+
const name = list[i];
|
|
464
|
+
const value = list[i + 1];
|
|
465
|
+
if (typeof name === "string" && value !== void 0) res.setHeader(name, value);
|
|
466
|
+
}
|
|
467
|
+
this.#headers = null;
|
|
468
|
+
this.#committed = true;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Recopie la réserve sur la réponse Node au PREMIER `setHeader` qu'un tiers
|
|
472
|
+
* y fera — fichier statique servi, redirection de dossier — et seulement
|
|
473
|
+
* alors : un raté du repli statique (la 404 d'une route inconnue) ne recopie
|
|
474
|
+
* rien, sa réponse part ensuite en un seul `writeHead`. À désarmer par
|
|
475
|
+
* {@link disarmNativeWrite} quand le tiers rend la main sans avoir écrit.
|
|
476
|
+
*
|
|
477
|
+
* L'interception est une propriété PROPRE de la réponse, posée une fois avec
|
|
478
|
+
* des attributs constants — toutes les réponses interceptées partagent donc
|
|
479
|
+
* la même forme — et désarmée en lui RÉAFFECTANT la méthode native, jamais
|
|
480
|
+
* en la supprimant : un `delete` fait basculer la réponse Node en mode
|
|
481
|
+
* dictionnaire (constaté par `%HasFastProperties` sur Node 26, 200 réponses
|
|
482
|
+
* sur 200), et chaque accès suivant de `node:http` le paie — mesuré in situ
|
|
483
|
+
* sur un 404 : `writeHead` +1,1 µs, `end` +4,3 µs.
|
|
484
|
+
*/
|
|
485
|
+
commitBeforeNativeWrite() {
|
|
486
|
+
const res = this.response;
|
|
487
|
+
if (res === null || res.headersSent || this.#nativeSetHeader !== null) return;
|
|
488
|
+
const native = res.setHeader;
|
|
489
|
+
this.#nativeSetHeader = native;
|
|
490
|
+
Object.defineProperty(res, "setHeader", {
|
|
491
|
+
configurable: true,
|
|
492
|
+
enumerable: false,
|
|
493
|
+
writable: true,
|
|
494
|
+
value: (name, value) => {
|
|
495
|
+
this.disarmNativeWrite();
|
|
496
|
+
this.commitHeaders();
|
|
497
|
+
return Reflect.apply(native, res, [name, value]);
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Retire l'interception posée par {@link commitBeforeNativeWrite} : la
|
|
503
|
+
* propriété propre reçoit la méthode native — la forme de l'objet ne change
|
|
504
|
+
* pas, la réponse reste en mode rapide pour V8.
|
|
505
|
+
*/
|
|
506
|
+
disarmNativeWrite() {
|
|
507
|
+
const native = this.#nativeSetHeader;
|
|
508
|
+
if (native === null) return;
|
|
509
|
+
this.#nativeSetHeader = null;
|
|
510
|
+
const res = this.response;
|
|
511
|
+
if (res !== null) Object.defineProperty(res, "setHeader", { value: native });
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Retire un en-tête, de la réserve comme de la réponse Node.
|
|
515
|
+
*
|
|
516
|
+
* @param name - nom de l'en-tête (casse indifférente).
|
|
517
|
+
*/
|
|
518
|
+
removeHeader(name) {
|
|
519
|
+
const at = this.#indexOf(name.toLowerCase());
|
|
520
|
+
if (at !== -1) this.#headers?.splice(at, 2);
|
|
521
|
+
if (this.response && !this.response.headersSent) this.response.removeHeader(name);
|
|
198
522
|
}
|
|
199
523
|
/**
|
|
200
524
|
* Union des tokens `Vary` déjà posés et de ceux qu'on ajoute, sans doublon.
|
|
@@ -207,7 +531,7 @@ var HttpResponse = class {
|
|
|
207
531
|
*/
|
|
208
532
|
#mergeVary(value) {
|
|
209
533
|
const incoming = (Array.isArray(value) ? value.join(",") : String(value)).split(",").map((t) => t.trim()).filter(Boolean);
|
|
210
|
-
const current = this.
|
|
534
|
+
const current = this.getHeader("vary");
|
|
211
535
|
const all = [...(Array.isArray(current) ? current.join(",") : String(current ?? "")).split(",").map((t) => t.trim()).filter(Boolean), ...incoming];
|
|
212
536
|
if (all.some((t) => t === "*")) return "*";
|
|
213
537
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -226,10 +550,10 @@ var HttpResponse = class {
|
|
|
226
550
|
const value = obj[head];
|
|
227
551
|
if (value !== void 0) this.setHeader(head, value);
|
|
228
552
|
}
|
|
229
|
-
return this.headers = this.
|
|
553
|
+
return this.headers = this.getHeaders();
|
|
230
554
|
}
|
|
231
555
|
this.log("headers already sended ", "WARNING");
|
|
232
|
-
return this.headers = this.
|
|
556
|
+
return this.headers = this.getHeaders();
|
|
233
557
|
}
|
|
234
558
|
setContentType(type, encoding) {
|
|
235
559
|
if (type && encoding) {
|
|
@@ -238,8 +562,12 @@ var HttpResponse = class {
|
|
|
238
562
|
const mytype = resolved.bare;
|
|
239
563
|
this.contentType = mytype;
|
|
240
564
|
this.encoding = encoding;
|
|
241
|
-
if (mytype === "application/json" || mytype.endsWith("+json"))
|
|
242
|
-
|
|
565
|
+
if (mytype === "application/json" || mytype.endsWith("+json")) {
|
|
566
|
+
this.setHeader("Content-Type", mytype);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
this.setHeader("Content-Type", `${mytype}; charset=${encoding}`);
|
|
570
|
+
return;
|
|
243
571
|
}
|
|
244
572
|
}
|
|
245
573
|
if (type && !encoding) {
|
|
@@ -249,10 +577,11 @@ var HttpResponse = class {
|
|
|
249
577
|
this.contentType = mytype;
|
|
250
578
|
const charset = resolved.charset;
|
|
251
579
|
if (charset) this.encoding = charset;
|
|
252
|
-
|
|
580
|
+
this.setHeader("Content-Type", mytype);
|
|
581
|
+
return;
|
|
253
582
|
}
|
|
254
583
|
}
|
|
255
|
-
|
|
584
|
+
this.setHeader("Content-Type", `${this.contentType}; charset=${this.encoding}`);
|
|
256
585
|
}
|
|
257
586
|
setFileMimeType(type, encoding) {
|
|
258
587
|
let myType = this.getMimeType(type);
|
|
@@ -261,7 +590,7 @@ var HttpResponse = class {
|
|
|
261
590
|
myType = "application/octet-stream";
|
|
262
591
|
}
|
|
263
592
|
this.contentType = myType;
|
|
264
|
-
|
|
593
|
+
this.setContentType(myType, encoding || this.encoding);
|
|
265
594
|
}
|
|
266
595
|
setContentTypeByExtension(extention) {
|
|
267
596
|
const ismime = mime.contentType(extention);
|
|
@@ -269,7 +598,8 @@ var HttpResponse = class {
|
|
|
269
598
|
this.contentType = ismime;
|
|
270
599
|
let charset = mime.charset(this.contentType);
|
|
271
600
|
if (charset) this.encoding = charset;
|
|
272
|
-
|
|
601
|
+
this.setHeader("Content-Type", ismime);
|
|
602
|
+
return;
|
|
273
603
|
}
|
|
274
604
|
this.log(`setContentTypeByExtension: ${extention} not found`, "WARNING");
|
|
275
605
|
}
|
|
@@ -337,19 +667,23 @@ var HttpResponse = class {
|
|
|
337
667
|
#setText(text, encoding) {
|
|
338
668
|
this.#buffer = null;
|
|
339
669
|
this.#text = text;
|
|
340
|
-
this.#textEncoding = encoding;
|
|
670
|
+
this.#textEncoding = encoding === "utf-8" ? "utf8" : encoding;
|
|
341
671
|
}
|
|
342
672
|
setLength(body) {
|
|
343
673
|
if (this.response?.headersSent) throw new Error("Headers already sended");
|
|
344
674
|
if (this.getHeader("Transfer-Encoding") === "chunked") {
|
|
345
|
-
this.
|
|
675
|
+
this.removeHeader("Content-Length");
|
|
346
676
|
return 0;
|
|
347
677
|
}
|
|
348
678
|
if (NO_CONTENT_LENGTH_STATUS.has(this.statusCode)) return 0;
|
|
679
|
+
if (this.statusCode === 205) {
|
|
680
|
+
this.#put("content-length", "0");
|
|
681
|
+
return 0;
|
|
682
|
+
}
|
|
349
683
|
const actualBody = body || this.#text || this.#buffer;
|
|
350
684
|
if (actualBody) {
|
|
351
685
|
const length = typeof actualBody === "string" && actualBody === this.#text ? Buffer.byteLength(actualBody, this.#textEncoding) : Buffer.byteLength(actualBody);
|
|
352
|
-
this
|
|
686
|
+
this.#put("content-length", String(length));
|
|
353
687
|
return length;
|
|
354
688
|
}
|
|
355
689
|
return 0;
|
|
@@ -362,27 +696,68 @@ var HttpResponse = class {
|
|
|
362
696
|
if (this.statusCode > 599) this.statusCode = 500;
|
|
363
697
|
}
|
|
364
698
|
this.statusMessage = this.getStatusMessage();
|
|
365
|
-
if (
|
|
366
|
-
|
|
699
|
+
if (headers) this.#apply(headers);
|
|
700
|
+
this.putCorrelationHeaders();
|
|
367
701
|
this.setLength();
|
|
368
702
|
this.ensureContentTypeHeader();
|
|
703
|
+
this.applyHeaderPolicy(this.contentTypeForPolicy());
|
|
704
|
+
const list = this.#store();
|
|
369
705
|
const std = http.STATUS_CODES[this.statusCode];
|
|
370
|
-
if (this.statusMessage === std) this.response.writeHead(this.statusCode,
|
|
706
|
+
if (this.statusMessage === std) this.response.writeHead(this.statusCode, list);
|
|
371
707
|
else {
|
|
372
708
|
const safeMsg = this.statusMessage.replace(/[^\x20-\x7E]/g, "").trim() || (std ?? "Unknown Error");
|
|
373
|
-
this.response.writeHead(this.statusCode, safeMsg,
|
|
709
|
+
this.response.writeHead(this.statusCode, safeMsg, list);
|
|
374
710
|
}
|
|
375
711
|
} else {
|
|
376
712
|
this.log("Headers already sent !!", "WARNING");
|
|
377
713
|
throw new Error(`Headers already sent !!`);
|
|
378
714
|
}
|
|
379
715
|
}
|
|
716
|
+
/**
|
|
717
|
+
* Range dans la réserve les en-têtes passés à `writeHead` — ils l'emportent
|
|
718
|
+
* sur ceux déjà posés, comme chez Node.
|
|
719
|
+
*/
|
|
720
|
+
#apply(headers) {
|
|
721
|
+
if (Array.isArray(headers)) {
|
|
722
|
+
for (let i = 0; i + 1 < headers.length; i += 2) {
|
|
723
|
+
const value = headers[i + 1];
|
|
724
|
+
if (value !== void 0) this.setHeader(String(headers[i]), value);
|
|
725
|
+
}
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
for (const name in headers) {
|
|
729
|
+
const value = headers[name];
|
|
730
|
+
if (value !== void 0) this.setHeader(name, value);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
/** La réserve amorcée — pour une remise sous une autre forme (HTTP/2). */
|
|
734
|
+
headerList() {
|
|
735
|
+
return this.#store();
|
|
736
|
+
}
|
|
380
737
|
addTrailers(headers) {
|
|
381
738
|
return this.response?.addTrailers(headers);
|
|
382
739
|
}
|
|
740
|
+
/**
|
|
741
|
+
* Écrit un morceau d'une réponse en flux, sans la terminer — c'est `end()`
|
|
742
|
+
* qui la clôt après le dernier.
|
|
743
|
+
*
|
|
744
|
+
* Au premier morceau, les en-têtes partent SANS longueur ni corps posé (le
|
|
745
|
+
* patron de `SseStream`), et le cadrage revient à Node : `chunked` pour un
|
|
746
|
+
* client HTTP/1.1, fermeture de connexion pour un HTTP/1.0 (RFC 9112 §6.1 —
|
|
747
|
+
* pas de `Transfer-Encoding` à qui ne le connaît pas), trames DATA en HTTP/2
|
|
748
|
+
* où l'en-tête est interdit (RFC 9113 §8.2.2). Ne JAMAIS le poser à la main :
|
|
749
|
+
* une fois `flushing` levé, `setHeader` range en trailer, et un
|
|
750
|
+
* `Transfer-Encoding` en trailer est un message malformé (RFC 9110 §6.5.1).
|
|
751
|
+
*
|
|
752
|
+
* @param chunk - le morceau à écrire.
|
|
753
|
+
* @param encoding - l'encodage d'un morceau texte.
|
|
754
|
+
* @returns la promesse réglée quand le flux accepte l'écriture suivante.
|
|
755
|
+
*/
|
|
383
756
|
flush(chunk, encoding) {
|
|
384
|
-
this.flushing
|
|
385
|
-
|
|
757
|
+
if (!this.flushing) {
|
|
758
|
+
if (!this.isHeaderSent()) this.writeHead();
|
|
759
|
+
this.flushing = true;
|
|
760
|
+
}
|
|
386
761
|
return this.send(chunk, encoding, true);
|
|
387
762
|
}
|
|
388
763
|
/**
|
|
@@ -445,15 +820,62 @@ var HttpResponse = class {
|
|
|
445
820
|
if (!this.response) throw new Error(`response not found`);
|
|
446
821
|
return this.response.end(chunk, encoding || this.encoding);
|
|
447
822
|
}
|
|
823
|
+
/**
|
|
824
|
+
* Règle le sort d'un corps de requête resté NON LU au moment de répondre —
|
|
825
|
+
* route inconnue (404, 405), refus avant l'action, 408, 413. À appeler AVANT
|
|
826
|
+
* l'écriture des en-têtes ; sans effet si le corps est entièrement reçu.
|
|
827
|
+
*
|
|
828
|
+
* HTTP/1.1 : Node vide lui-même un corps non consommé une fois la réponse
|
|
829
|
+
* écrite, pour réutiliser la connexion — sans borne. On ne le laisse faire
|
|
830
|
+
* que pour un corps DÉCLARÉ d'au plus {@link UNREAD_BODY_DRAIN_LIMIT}
|
|
831
|
+
* octets ; au-delà, ou sans longueur déclarée (`transfer-encoding`), la
|
|
832
|
+
* réponse part avec `connection: close` (RFC 9112 §9.6) et la connexion
|
|
833
|
+
* n'est pas réutilisée — fermée par étapes ({@link lingerBeforeClose}),
|
|
834
|
+
* pour que le client qui écrit encore lise la réponse. La décision se prend
|
|
835
|
+
* sur la longueur ANNONCÉE, sans attendre un octet : la réponse n'est jamais
|
|
836
|
+
* retardée par le corps.
|
|
837
|
+
*
|
|
838
|
+
* HTTP/2 surcharge cette méthode (`Http2Response`) : fermeture du flux en
|
|
839
|
+
* `NO_ERROR` une fois la réponse écrite. Une seule entrée pour les deux
|
|
840
|
+
* protocoles, partagée par tout refus qui laisse un corps non lu (ADR-0026,
|
|
841
|
+
* D5).
|
|
842
|
+
*/
|
|
843
|
+
closeUnfinishedRequest() {
|
|
844
|
+
const response = this.response;
|
|
845
|
+
if (response === null || response.headersSent) return;
|
|
846
|
+
if (this.context.request.request.complete) return;
|
|
847
|
+
const headers = this.context.request.headers;
|
|
848
|
+
if (headers["transfer-encoding"] === void 0) {
|
|
849
|
+
if (Number(headers["content-length"] ?? 0) <= 262144) return;
|
|
850
|
+
}
|
|
851
|
+
this.setHeader("connection", "close");
|
|
852
|
+
const socket = response.socket;
|
|
853
|
+
if (socket !== null) lingerBeforeClose(socket);
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Valeur d'un en-tête posé — dans la réserve, sinon sur la réponse Node (ce
|
|
857
|
+
* qu'un tiers y a écrit directement : `send`, quota).
|
|
858
|
+
*
|
|
859
|
+
* @param name - nom de l'en-tête (casse indifférente).
|
|
860
|
+
*/
|
|
448
861
|
getHeader(name) {
|
|
862
|
+
const at = this.#indexOf(name.toLowerCase());
|
|
863
|
+
if (at !== -1) return this.#headers?.[at + 1];
|
|
449
864
|
return this.response?.getHeader(name);
|
|
450
865
|
}
|
|
451
866
|
hasHeader(name) {
|
|
452
|
-
if (this.response) return this.response.hasHeader(name);
|
|
867
|
+
if (this.response) return this.#indexOf(name.toLowerCase()) !== -1 || this.response.hasHeader(name);
|
|
453
868
|
throw new Error(`Respose not foud`);
|
|
454
869
|
}
|
|
870
|
+
/** Tous les en-têtes posés, noms en minuscules — une copie, à chaque appel. */
|
|
455
871
|
getHeaders() {
|
|
456
|
-
|
|
872
|
+
const out = { ...this.response?.getHeaders() };
|
|
873
|
+
const list = this.#headers;
|
|
874
|
+
if (list !== null) for (let i = 0; i + 1 < list.length; i += 2) {
|
|
875
|
+
const name = list[i];
|
|
876
|
+
if (typeof name === "string") out[name] = list[i + 1];
|
|
877
|
+
}
|
|
878
|
+
return out;
|
|
457
879
|
}
|
|
458
880
|
redirect(url, status, headers) {
|
|
459
881
|
this.context.isRedirect = true;
|
|
@@ -481,4 +903,4 @@ var HttpResponse = class {
|
|
|
481
903
|
return this.context.log(pci, severity, msgid, msg);
|
|
482
904
|
}
|
|
483
905
|
};
|
|
484
|
-
export { HttpResponse as default };
|
|
906
|
+
export { UNREAD_BODY_DRAIN_LIMIT, UNREAD_BODY_LINGER_MS, HttpResponse as default, isContentlessStatus };
|