@nodefony/http 10.0.0-beta.3 → 10.0.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
- package/dist/index.js +13 -10
- package/dist/nodefony/command/assetsPublishCommand.js +5 -3
- package/dist/nodefony/command/certificatesCommand.js +5 -3
- package/dist/nodefony/command/networkCommand.js +5 -3
- package/dist/nodefony/command/proxyGenerateCommand.js +8 -3
- package/dist/nodefony/config/config.js +3 -3
- package/dist/nodefony/config/defineModuleConfig.js +1 -2
- package/dist/nodefony/interfaces/IRequestTrace.js +1 -0
- package/dist/nodefony/service/HttpAdminApi.js +2 -2
- package/dist/nodefony/service/ProfilerAdminApi.js +13 -3
- package/dist/nodefony/service/audit-logger.js +23 -30
- package/dist/nodefony/service/certificates.js +97 -13
- package/dist/nodefony/service/error-renderer.js +29 -12
- package/dist/nodefony/service/http-kernel.js +198 -50
- package/dist/nodefony/service/pretty-request-logger.js +15 -13
- package/dist/nodefony/service/request-logger.js +24 -16
- package/dist/nodefony/service/reverse-proxy.js +3 -3
- package/dist/nodefony/service/servers/clientError.js +22 -3
- package/dist/nodefony/service/servers/server-h2c.js +126 -0
- package/dist/nodefony/service/servers/server-http.js +5 -8
- package/dist/nodefony/service/servers/server-https.js +12 -24
- package/dist/nodefony/service/servers/server-static.js +3 -3
- package/dist/nodefony/service/servers/server-websocket-secure.js +10 -18
- package/dist/nodefony/service/servers/server-websocket.js +10 -18
- package/dist/nodefony/service/servers/serverShutdown.js +78 -18
- package/dist/nodefony/service/servers/wsHeartbeat.js +1 -2
- package/dist/nodefony/service/sessions/sessions-service.js +7 -14
- package/dist/nodefony/service/trace.js +1 -2
- package/dist/nodefony/service/upload/upload-service.js +5 -8
- package/dist/nodefony/service/x509.js +1 -2
- package/dist/nodefony/src/assets/collectAssets.js +1 -2
- package/dist/nodefony/src/assets/prebuiltUi.js +1 -2
- package/dist/nodefony/src/context/Context.js +78 -43
- package/dist/nodefony/src/context/domainMatcher.js +1 -2
- package/dist/nodefony/src/context/forwarded.js +1 -2
- package/dist/nodefony/src/context/http/HttpContext.js +28 -6
- package/dist/nodefony/src/context/http/Request.js +33 -29
- package/dist/nodefony/src/context/http/Response.js +23 -6
- package/dist/nodefony/src/context/http/SseStream.js +1 -2
- package/dist/nodefony/src/context/http/parser.js +36 -10
- package/dist/nodefony/src/context/http/urlFastPath.js +1 -2
- package/dist/nodefony/src/context/http2/Request.js +4 -8
- package/dist/nodefony/src/context/http2/Response.js +41 -9
- package/dist/nodefony/src/context/metaData.js +1 -2
- package/dist/nodefony/src/context/requestId.js +1 -2
- package/dist/nodefony/src/context/requestTrace.js +104 -0
- package/dist/nodefony/src/context/responseEnded.js +1 -2
- package/dist/nodefony/src/context/timingMode.js +21 -0
- package/dist/nodefony/src/context/transport.js +45 -0
- package/dist/nodefony/src/context/trustProxy.js +1 -2
- package/dist/nodefony/src/context/unsentResponse.js +30 -0
- package/dist/nodefony/src/context/websocket/Response.js +33 -6
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +104 -25
- package/dist/nodefony/src/context/websocket/isWebsocketContext.js +17 -0
- package/dist/nodefony/src/context/websocket/websocketUrl.js +1 -2
- package/dist/nodefony/src/context/websocket/wsBackpressure.js +1 -2
- package/dist/nodefony/src/context/websocket/wsCloseReason.js +1 -2
- package/dist/nodefony/src/context/websocket/wsLogContent.js +1 -2
- package/dist/nodefony/src/cookies/cookie.js +3 -7
- package/dist/nodefony/src/errors/httpError.js +1 -2
- package/dist/nodefony/src/errors/thrownMessage.js +1 -2
- package/dist/nodefony/src/profiler/FrameProfile.js +23 -2
- package/dist/nodefony/src/profiler/Profiler.js +104 -32
- package/dist/nodefony/src/profiler/aggregate.js +442 -0
- package/dist/nodefony/src/proxy/forward.js +29 -5
- package/dist/nodefony/src/proxy/generateProxyConfig.js +14 -6
- package/dist/nodefony/src/proxy/rules.js +12 -6
- package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +1 -2
- package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +1 -2
- package/dist/nodefony/src/rateLimit/rateLimitFilters.js +1 -2
- package/dist/nodefony/src/servers/http2Limits.js +50 -0
- package/dist/nodefony/src/servers/kernelServers.js +12 -3
- package/dist/nodefony/src/servers/portBinder.js +41 -10
- package/dist/nodefony/src/servers/upgradeDispatch.js +86 -7
- package/dist/nodefony/src/session/session.js +2 -3
- package/dist/nodefony/src/session/sessionStoreFailure.js +1 -2
- package/dist/nodefony/src/session/storage/MemorySessionStorage.js +1 -2
- package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +1 -2
- package/dist/nodefony/src/session/storage/sessionFilters.js +1 -2
- package/dist/nodefony/src/session/storage/sessionSort.js +1 -2
- package/dist/types/index.d.ts +9 -3
- package/dist/types/nodefony/config/config.d.ts +4 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +72 -4
- package/dist/types/nodefony/interfaces/IRequest.d.ts +9 -4
- package/dist/types/nodefony/interfaces/IRequestTrace.d.ts +56 -0
- package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +8 -0
- package/dist/types/nodefony/interfaces/IRouting.d.ts +26 -0
- package/dist/types/nodefony/interfaces/index.d.ts +3 -2
- package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +3 -0
- package/dist/types/nodefony/service/audit-logger.d.ts +3 -11
- package/dist/types/nodefony/service/certificates.d.ts +55 -4
- package/dist/types/nodefony/service/error-renderer.d.ts +31 -0
- package/dist/types/nodefony/service/http-kernel.d.ts +96 -10
- package/dist/types/nodefony/service/servers/clientError.d.ts +16 -0
- package/dist/types/nodefony/service/servers/server-h2c.d.ts +51 -0
- package/dist/types/nodefony/service/servers/server-http.d.ts +3 -3
- package/dist/types/nodefony/service/servers/server-https.d.ts +4 -4
- package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +1 -3
- package/dist/types/nodefony/service/servers/server-websocket.d.ts +1 -3
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +30 -16
- package/dist/types/nodefony/src/context/Context.d.ts +38 -5
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +16 -2
- package/dist/types/nodefony/src/context/http/Request.d.ts +20 -15
- package/dist/types/nodefony/src/context/http/Response.d.ts +11 -0
- package/dist/types/nodefony/src/context/http/parser.d.ts +18 -2
- package/dist/types/nodefony/src/context/http2/Request.d.ts +1 -1
- package/dist/types/nodefony/src/context/http2/Response.d.ts +27 -5
- package/dist/types/nodefony/src/context/requestTrace.d.ts +64 -0
- package/dist/types/nodefony/src/context/timingMode.d.ts +23 -0
- package/dist/types/nodefony/src/context/transport.d.ts +53 -0
- package/dist/types/nodefony/src/context/unsentResponse.d.ts +16 -0
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +18 -2
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +58 -8
- package/dist/types/nodefony/src/context/websocket/isWebsocketContext.d.ts +15 -0
- package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +28 -12
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +79 -111
- package/dist/types/nodefony/src/profiler/aggregate.d.ts +132 -0
- package/dist/types/nodefony/src/proxy/forward.d.ts +14 -2
- package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +16 -0
- package/dist/types/nodefony/src/proxy/rules.d.ts +3 -0
- package/dist/types/nodefony/src/servers/http2Limits.d.ts +37 -0
- package/dist/types/nodefony/src/servers/kernelServers.d.ts +10 -1
- package/dist/types/nodefony/src/servers/portBinder.d.ts +29 -5
- package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +33 -3
- package/docs/cookies.md +85 -78
- package/docs/diagnostics-channel.md +303 -0
- package/docs/index.md +6 -1
- package/docs/observabilite.md +159 -81
- package/docs/rate-limit.md +83 -83
- package/docs/reverse-proxy.md +72 -70
- package/docs/servers.md +338 -224
- package/docs/session.md +179 -179
- package/docs/sse.md +24 -20
- package/docs/upload.md +120 -117
- package/package.json +8 -9
- package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
|
@@ -2,7 +2,7 @@ import Cookie from "../../cookies/cookie.js";
|
|
|
2
2
|
import { thenMaybe, typeOf } from "nodefony";
|
|
3
3
|
import http 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");
|
|
7
7
|
const stripAnsi = function(val) {
|
|
8
8
|
return typeof val === "string" ? val.replace(ANSI_REGEX, "") : val;
|
|
@@ -55,7 +55,8 @@ var HttpResponse = class {
|
|
|
55
55
|
constructor(response, context) {
|
|
56
56
|
this.context = context;
|
|
57
57
|
this.response = response;
|
|
58
|
-
|
|
58
|
+
const responseTimeout = this.context.httpKernel?.responseTimeout;
|
|
59
|
+
this.timeout = this.context.serverType === "https" ? responseTimeout?.https : responseTimeout?.http;
|
|
59
60
|
}
|
|
60
61
|
/**
|
|
61
62
|
* Filet posé juste avant l'émission des en-têtes : si aucun `Content-Type`
|
|
@@ -153,16 +154,33 @@ var HttpResponse = class {
|
|
|
153
154
|
const serialized = [];
|
|
154
155
|
for (const cookie of cookies) {
|
|
155
156
|
const s = cookie.serialize();
|
|
156
|
-
this.
|
|
157
|
+
this.logCookie(cookie, s);
|
|
157
158
|
serialized.push(s);
|
|
158
159
|
}
|
|
159
160
|
return this.setHeader("Set-Cookie", serialized);
|
|
160
161
|
}
|
|
161
162
|
setCookie(cookie) {
|
|
162
163
|
const serialize = cookie.serialize();
|
|
163
|
-
this.
|
|
164
|
+
this.logCookie(cookie, serialize);
|
|
164
165
|
return this.setHeader("Set-Cookie", serialize);
|
|
165
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Journalise en DEBUG un cookie posé, sa VALEUR MASQUÉE : nom et attributs
|
|
169
|
+
* seulement. Le journal de développement se lit par le plan d'administration
|
|
170
|
+
* — un identifiant de session y entrerait chez quiconque le consulte.
|
|
171
|
+
* La ligne n'est construite que si DEBUG passe le gate du syslog pour ce
|
|
172
|
+
* `msgid` (debug ciblé compris) : rien n'est alloué en production.
|
|
173
|
+
*
|
|
174
|
+
* @param cookie - le cookie posé.
|
|
175
|
+
* @param serialized - sa forme `Set-Cookie`, déjà calculée pour l'en-tête.
|
|
176
|
+
*/
|
|
177
|
+
logCookie(cookie, serialized) {
|
|
178
|
+
const msgid = `${this.context.serverType} RESPONSE `;
|
|
179
|
+
const syslog = this.context.syslog;
|
|
180
|
+
if (syslog && !syslog.severityEnabled("DEBUG", msgid)) return;
|
|
181
|
+
const attrs = serialized.indexOf("; ", cookie.name.length + 1);
|
|
182
|
+
this.log(`ADD COOKIE ==> ${cookie.name}=<masqué>${attrs === -1 ? "" : serialized.slice(attrs)}`, "DEBUG", msgid);
|
|
183
|
+
}
|
|
166
184
|
setHeader(name, value) {
|
|
167
185
|
if (this.response) {
|
|
168
186
|
if (this.flushing) {
|
|
@@ -459,9 +477,8 @@ var HttpResponse = class {
|
|
|
459
477
|
return this;
|
|
460
478
|
}
|
|
461
479
|
log(pci, severity, msgid, msg) {
|
|
462
|
-
if (!msgid) msgid = `${this.context.
|
|
480
|
+
if (!msgid) msgid = `${this.context.serverType} RESPONSE `;
|
|
463
481
|
return this.context.log(pci, severity, msgid, msg);
|
|
464
482
|
}
|
|
465
483
|
};
|
|
466
|
-
//#endregion
|
|
467
484
|
export { HttpResponse as default };
|
|
@@ -3,7 +3,7 @@ import Http2Response from "../http2/Response.js";
|
|
|
3
3
|
import { RequestContext } from "nodefony";
|
|
4
4
|
import http from "node:http";
|
|
5
5
|
import { AsyncResource } from "node:async_hooks";
|
|
6
|
-
|
|
6
|
+
// nodefony/src/context/http/SseStream.ts
|
|
7
7
|
/** Battement de cœur par défaut, en ms (§9.2.7 : « every 15 seconds or so »). */
|
|
8
8
|
const SSE_HEARTBEAT_MS = 15e3;
|
|
9
9
|
/** Délai de blocage d'écriture par défaut, en ms. */
|
|
@@ -346,5 +346,4 @@ function startSseStream(context, options, settings) {
|
|
|
346
346
|
if (options.retry !== void 0) sse.retry(options.retry);
|
|
347
347
|
return sse;
|
|
348
348
|
}
|
|
349
|
-
//#endregion
|
|
350
349
|
export { SSE_HEARTBEAT_MS, SSE_MAX_EVENT_BYTES, SSE_STALL_TIMEOUT_MS, SseStream, acceptsEventStream, openSseStream };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import HttpError from "../../errors/httpError.js";
|
|
2
|
-
import { escapeRegExp
|
|
2
|
+
import { escapeRegExp } from "nodefony";
|
|
3
3
|
import QS from "qs";
|
|
4
4
|
import xml2js from "xml2js";
|
|
5
|
-
|
|
5
|
+
// nodefony/src/context/http/parser.ts
|
|
6
6
|
var Parser = class {
|
|
7
7
|
request;
|
|
8
8
|
chunks;
|
|
@@ -73,6 +73,27 @@ var Parser = class {
|
|
|
73
73
|
return this;
|
|
74
74
|
}
|
|
75
75
|
};
|
|
76
|
+
/**
|
|
77
|
+
* Corps d'un type que le framework ne décode pas : livré TEL QU'IL EST REÇU,
|
|
78
|
+
* jamais remplacé par un objet vide qui mentirait.
|
|
79
|
+
*
|
|
80
|
+
* - `text/*` → une chaîne, décodée au `charset` du `Content-Type` (UTF-8 par
|
|
81
|
+
* défaut) — comme une trame texte RFC 6455 ;
|
|
82
|
+
* - tout autre type, et un corps sans `Content-Type` (RFC 9110 §8.3 : supposé
|
|
83
|
+
* `application/octet-stream`) → les OCTETS : le `Buffer` déjà assemblé dans
|
|
84
|
+
* `request.data`, sans copie — donc un `Uint8Array`, le type d'octets que
|
|
85
|
+
* la socket livrera aussi (#618). Un même schéma passe par les deux portes.
|
|
86
|
+
*
|
|
87
|
+
* Un corps VIDE laisse `body` tel quel : une requête sans charge ne change pas.
|
|
88
|
+
*/
|
|
89
|
+
var ParserRaw = class extends Parser {
|
|
90
|
+
async parse() {
|
|
91
|
+
await super.parse();
|
|
92
|
+
const data = this.request.data;
|
|
93
|
+
if (data.length > 0) this.request.body = this.request.contentType?.startsWith("text/") ? data.toString(this.request.charset) : data;
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
76
97
|
var ParserQs = class extends Parser {
|
|
77
98
|
parserOptions;
|
|
78
99
|
charset = "utf8";
|
|
@@ -83,8 +104,7 @@ var ParserQs = class extends Parser {
|
|
|
83
104
|
}
|
|
84
105
|
async parse() {
|
|
85
106
|
await super.parse();
|
|
86
|
-
this.request.
|
|
87
|
-
this.request.query = extend({}, this.request.query, this.request.queryPost);
|
|
107
|
+
this.request.body = QS.parse(this.request.data.toString(this.charset), this.parserOptions);
|
|
88
108
|
this.request.context.requestEnded = true;
|
|
89
109
|
return this;
|
|
90
110
|
}
|
|
@@ -102,7 +122,7 @@ var ParserXml = class extends Parser {
|
|
|
102
122
|
return new Promise((resolve, reject) => {
|
|
103
123
|
this.xmlParser.parseString(this.request.data.toString(this.charset), (err, result) => {
|
|
104
124
|
if (err) return reject(err);
|
|
105
|
-
this.request.
|
|
125
|
+
this.request.body = result;
|
|
106
126
|
this.request.context.requestEnded = true;
|
|
107
127
|
return resolve(this);
|
|
108
128
|
});
|
|
@@ -110,7 +130,14 @@ var ParserXml = class extends Parser {
|
|
|
110
130
|
}
|
|
111
131
|
};
|
|
112
132
|
/**
|
|
113
|
-
*
|
|
133
|
+
* Vrai pour un objet ou un tableau JSON — ce que `body` peut porter.
|
|
134
|
+
* Un tableau passe : `@Body()` le rend tel quel (envoi en lot).
|
|
135
|
+
*/
|
|
136
|
+
function isJsonContainer(value) {
|
|
137
|
+
return typeof value === "object" && value !== null;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Parse un corps `application/json` (ou `*+json`) en objet → `body`
|
|
114
141
|
* (source lue par le décorateur `@Body`). Reprend le rôle de l'ancien plugin
|
|
115
142
|
* `json` de formidable, supprimé avec le passage à busboy (multipart seul).
|
|
116
143
|
* Lenient : un corps JSON malformé est ignoré (pas de throw — `parse()` est
|
|
@@ -127,8 +154,8 @@ var ParserJson = class extends Parser {
|
|
|
127
154
|
await super.parse();
|
|
128
155
|
const text = this.request.data.toString(this.charset).trim();
|
|
129
156
|
if (text) try {
|
|
130
|
-
|
|
131
|
-
|
|
157
|
+
const parsed = JSON.parse(text);
|
|
158
|
+
if (isJsonContainer(parsed)) this.request.body = parsed;
|
|
132
159
|
} catch {}
|
|
133
160
|
this.request.context.requestEnded = true;
|
|
134
161
|
return this;
|
|
@@ -183,5 +210,4 @@ const acceptParser = function(acc) {
|
|
|
183
210
|
return ACCEPT_ANY;
|
|
184
211
|
}
|
|
185
212
|
};
|
|
186
|
-
|
|
187
|
-
export { Parser, ParserJson, ParserQs, ParserXml, acceptParser };
|
|
213
|
+
export { Parser, ParserJson, ParserQs, ParserRaw, ParserXml, acceptParser };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/context/http/urlFastPath.ts
|
|
2
2
|
const PATH_SAFE = /* @__PURE__ */ new Uint8Array(128);
|
|
3
3
|
const SEARCH_SAFE = /* @__PURE__ */ new Uint8Array(128);
|
|
4
4
|
const HOST_SAFE = /* @__PURE__ */ new Uint8Array(128);
|
|
@@ -123,5 +123,4 @@ function isDigitsFrom(s, from) {
|
|
|
123
123
|
}
|
|
124
124
|
return true;
|
|
125
125
|
}
|
|
126
|
-
//#endregion
|
|
127
126
|
export { isCanonicalAuthority, splitTarget };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { requestAuthority } from "../transport.js";
|
|
1
2
|
import HttpRequest from "../http/Request.js";
|
|
2
|
-
|
|
3
|
+
// nodefony/src/context/http2/Request.ts
|
|
3
4
|
var Http2Request = class extends HttpRequest {
|
|
4
5
|
request;
|
|
5
6
|
constructor(request, context) {
|
|
@@ -7,8 +8,9 @@ var Http2Request = class extends HttpRequest {
|
|
|
7
8
|
this.context = context;
|
|
8
9
|
this.request = request;
|
|
9
10
|
}
|
|
11
|
+
/** `:authority`, sinon `Host` — cf `requestAuthority` (RFC 9113 §8.3.1). */
|
|
10
12
|
getHost() {
|
|
11
|
-
return this.headers
|
|
13
|
+
return requestAuthority(this.request.headers);
|
|
12
14
|
}
|
|
13
15
|
getUserAgent() {
|
|
14
16
|
return this.headers["user-agent"];
|
|
@@ -19,11 +21,5 @@ var Http2Request = class extends HttpRequest {
|
|
|
19
21
|
getRawTarget() {
|
|
20
22
|
return this.headers[":path"];
|
|
21
23
|
}
|
|
22
|
-
resolveScheme() {
|
|
23
|
-
if (this.forwarded?.proto) return this.forwarded.proto;
|
|
24
|
-
if (this.headers[":scheme"] === "https") return "https";
|
|
25
|
-
return "http";
|
|
26
|
-
}
|
|
27
24
|
};
|
|
28
|
-
//#endregion
|
|
29
25
|
export { Http2Request as default };
|
|
@@ -2,8 +2,13 @@ import HttpResponse from "../http/Response.js";
|
|
|
2
2
|
import { thenMaybe } from "nodefony";
|
|
3
3
|
import http from "node:http";
|
|
4
4
|
import http2 from "node:http2";
|
|
5
|
-
|
|
5
|
+
// nodefony/src/context/http2/Response.ts
|
|
6
6
|
const HTTP2_HEADER_STATUS = http2.constants.HTTP2_HEADER_STATUS;
|
|
7
|
+
/** Réponse Node déjà libérée : aucun en-tête posé à reprendre. */
|
|
8
|
+
const emptyHeaders = {
|
|
9
|
+
getHeaderNames: () => [],
|
|
10
|
+
getHeader: () => void 0
|
|
11
|
+
};
|
|
7
12
|
/**
|
|
8
13
|
* Compose les en-têtes d'une réponse HTTP/2 : ceux posés sur la réponse, puis
|
|
9
14
|
* ceux passés à `writeHead`, qui gagnent.
|
|
@@ -14,14 +19,19 @@ const HTTP2_HEADER_STATUS = http2.constants.HTTP2_HEADER_STATUS;
|
|
|
14
19
|
* (`ERR_HTTP2_HEADER_SINGLE_VALUE`), soit une 500 sur tout média servi en
|
|
15
20
|
* HTTP/2. HTTP/2 n'a de toute façon que des noms en minuscules (RFC 9113 §8.2.1).
|
|
16
21
|
*
|
|
22
|
+
* Les en-têtes posés se lisent nom par nom (`getHeaderNames`, déjà en
|
|
23
|
+
* minuscules) : `getHeaders()` les recopiait d'abord dans un objet sans
|
|
24
|
+
* prototype, avant cette fusion — 3,8 µs par réponse pour 12 en-têtes,
|
|
25
|
+
* contre 1,1 µs en une passe.
|
|
26
|
+
*
|
|
17
27
|
* @param statusMessage - message de statut, recopié en `x-status-message`
|
|
18
|
-
* @param current - en-têtes déjà posés
|
|
28
|
+
* @param current - la réponse Node qui porte les en-têtes déjà posés
|
|
19
29
|
* @param extra - en-têtes passés à `writeHead` (objet, ou liste plate clé/valeur)
|
|
20
30
|
* @returns les en-têtes à transmettre à `respond()`
|
|
21
31
|
*/
|
|
22
32
|
function mergeResponseHeaders(statusMessage, current, extra) {
|
|
23
33
|
const merged = { "x-status-message": statusMessage };
|
|
24
|
-
for (const
|
|
34
|
+
for (const name of current.getHeaderNames()) merged[name] = current.getHeader(name);
|
|
25
35
|
if (Array.isArray(extra)) for (let i = 0; i + 1 < extra.length; i += 2) merged[String(extra[i]).toLowerCase()] = extra[i + 1];
|
|
26
36
|
else if (extra) for (const key in extra) {
|
|
27
37
|
const value = extra[key];
|
|
@@ -58,7 +68,7 @@ var Http2Response = class extends HttpResponse {
|
|
|
58
68
|
this.statusMessage = this.getStatusMessage();
|
|
59
69
|
this.setLength();
|
|
60
70
|
this.ensureContentTypeHeader();
|
|
61
|
-
this.headers = mergeResponseHeaders(this.statusMessage, this.
|
|
71
|
+
this.headers = mergeResponseHeaders(this.statusMessage, this.response ?? emptyHeaders, headers);
|
|
62
72
|
this.headers[HTTP2_HEADER_STATUS] = this.statusCode;
|
|
63
73
|
const requestId = this.context.requestId;
|
|
64
74
|
if (requestId) this.headers["x-request-id"] = requestId;
|
|
@@ -69,9 +79,9 @@ var Http2Response = class extends HttpResponse {
|
|
|
69
79
|
} else return super.writeHead(statusCode, headers);
|
|
70
80
|
}
|
|
71
81
|
/**
|
|
72
|
-
* Écrit le corps sur le flux HTTP/2
|
|
73
|
-
*
|
|
74
|
-
*
|
|
82
|
+
* Écrit le corps sur le flux HTTP/2 et, pour une réponse unique, le termine
|
|
83
|
+
* d'un seul `end(corps)`. Synchrone, sauf en streaming (`flush()`) : une
|
|
84
|
+
* promesse réglée par le rappel d'écriture, la contre-pression (#505).
|
|
75
85
|
*
|
|
76
86
|
* @returns la réponse, ou sa promesse.
|
|
77
87
|
*/
|
|
@@ -83,10 +93,15 @@ var Http2Response = class extends HttpResponse {
|
|
|
83
93
|
const stream = this.stream;
|
|
84
94
|
if (!stream) return super.send(chunk, encoding);
|
|
85
95
|
if (stream.destroyed || !stream.writable) {
|
|
96
|
+
this.settleHeadResponse();
|
|
86
97
|
this.log("HTTP/2 stream not writable before send — skip", "DEBUG");
|
|
87
98
|
return this;
|
|
88
99
|
}
|
|
89
100
|
if (chunk) this.setBody(chunk);
|
|
101
|
+
if (!_flush && !this.flushing) {
|
|
102
|
+
stream.end(this.payload, this.payloadEncoding);
|
|
103
|
+
return this;
|
|
104
|
+
}
|
|
90
105
|
return new Promise((resolve, reject) => {
|
|
91
106
|
stream.write(this.payload, this.payloadEncoding, (error) => {
|
|
92
107
|
if (error) return reject(error);
|
|
@@ -102,11 +117,29 @@ var Http2Response = class extends HttpResponse {
|
|
|
102
117
|
*/
|
|
103
118
|
end(chunk, encoding) {
|
|
104
119
|
if (this.stream) {
|
|
105
|
-
if (this.stream.
|
|
120
|
+
if (this.stream.writableEnded || this.stream.destroyed || this.stream.closed) {
|
|
121
|
+
this.settleHeadResponse();
|
|
122
|
+
return this.stream;
|
|
123
|
+
}
|
|
106
124
|
return this.stream.end(chunk, encoding || this.encoding);
|
|
107
125
|
}
|
|
108
126
|
return super.end(chunk, encoding);
|
|
109
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Termine la réponse de compatibilité d'un HEAD, dont Node a clos le flux de
|
|
130
|
+
* lui-même au `respond()`.
|
|
131
|
+
*
|
|
132
|
+
* Sans ce geste, `Http2ServerResponse` n'émet ni `finish` ni `close` : le
|
|
133
|
+
* démontage de la requête, branché sur ce `close` (ligne d'accès, profil,
|
|
134
|
+
* hooks d'après-réponse, libération du scope DI), n'a jamais lieu. Les statuts
|
|
135
|
+
* que Node termine aussi d'office (204, 205, 304) émettent leur `close` seuls,
|
|
136
|
+
* et un flux détruit (client parti) aussi : ils ne sont pas touchés. Hors du
|
|
137
|
+
* chemin servi — appelé seulement quand le flux est déjà clos.
|
|
138
|
+
*/
|
|
139
|
+
settleHeadResponse() {
|
|
140
|
+
const raw = this.response;
|
|
141
|
+
if (raw !== null && this.context.method === "HEAD" && this.stream?.destroyed === false && !raw.writableEnded) raw.end();
|
|
142
|
+
}
|
|
110
143
|
getStatusMessage(code) {
|
|
111
144
|
if (code) {
|
|
112
145
|
if (this.response) return http.STATUS_CODES[code] || this.statusMessage;
|
|
@@ -121,5 +154,4 @@ var Http2Response = class extends HttpResponse {
|
|
|
121
154
|
};
|
|
122
155
|
}
|
|
123
156
|
};
|
|
124
|
-
//#endregion
|
|
125
157
|
export { Http2Response as default, mergeResponseHeaders };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/context/metaData.ts
|
|
2
2
|
/**
|
|
3
3
|
* Assemble l'enveloppe `metaData` d'une requête EN PLACE (builder monomorphe) :
|
|
4
4
|
* met à jour les champs de `target.nodefony` dans un ordre fixe → V8 garde une
|
|
@@ -43,5 +43,4 @@ function buildMetaData(target, src, override) {
|
|
|
43
43
|
}
|
|
44
44
|
return target;
|
|
45
45
|
}
|
|
46
|
-
//#endregion
|
|
47
46
|
export { buildMetaData };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// nodefony/src/context/requestId.ts
|
|
2
2
|
/**
|
|
3
3
|
* Validation de l'identifiant de corrélation `X-Request-Id` fourni par le
|
|
4
4
|
* client, avant qu'il ne soit adopté comme `Context.requestId`.
|
|
@@ -37,5 +37,4 @@ function sanitizeRequestId(raw) {
|
|
|
37
37
|
if (!raw || raw.length > 128) return null;
|
|
38
38
|
return SAFE_REQUEST_ID.test(raw) ? raw : null;
|
|
39
39
|
}
|
|
40
|
-
//#endregion
|
|
41
40
|
export { MAX_REQUEST_ID_LENGTH, sanitizeRequestId };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import diagnosticsChannel from "node:diagnostics_channel";
|
|
2
|
+
// nodefony/src/context/requestTrace.ts
|
|
3
|
+
/**
|
|
4
|
+
* Nom du `TracingChannel` qui encadre chaque requête HTTP et chaque handshake
|
|
5
|
+
* WebSocket — forme `module.fonction` recommandée par la documentation de
|
|
6
|
+
* `node:diagnostics_channel` ; les sous-canaux sont
|
|
7
|
+
* `tracing:nodefony.http.request:start`, `:end`, `:asyncStart`, `:asyncEnd`,
|
|
8
|
+
* `:error`.
|
|
9
|
+
*/
|
|
10
|
+
const REQUEST_TRACE_CHANNEL = "nodefony.http.request";
|
|
11
|
+
const tracing = diagnosticsChannel.tracingChannel(REQUEST_TRACE_CHANNEL);
|
|
12
|
+
const startChannel = tracing.start;
|
|
13
|
+
const endChannel = tracing.end;
|
|
14
|
+
const asyncStartChannel = tracing.asyncStart;
|
|
15
|
+
const asyncEndChannel = tracing.asyncEnd;
|
|
16
|
+
const errorChannel = tracing.error;
|
|
17
|
+
/**
|
|
18
|
+
* Dit si quelqu'un écoute le canal — LA garde du chemin chaud : ~1,6 ns
|
|
19
|
+
* mesurées, aucune allocation. Lue une seule fois, au début de la requête :
|
|
20
|
+
* un abonné arrivé ensuite ne voit que les requêtes suivantes (règle des
|
|
21
|
+
* `TracingChannel`, qui interdit une fin sans début).
|
|
22
|
+
*
|
|
23
|
+
* @returns `true` si au moins un sous-canal a un abonné
|
|
24
|
+
*/
|
|
25
|
+
function hasRequestTraceSubscribers() {
|
|
26
|
+
return tracing.hasSubscribers;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Crée le message de la requête et le range sur son contexte. À n'appeler
|
|
30
|
+
* que sous {@link hasRequestTraceSubscribers} : c'est la seule allocation
|
|
31
|
+
* du canal, et elle n'a lieu que si quelqu'un écoute.
|
|
32
|
+
*
|
|
33
|
+
* @param context - le contexte qui vient d'être construit
|
|
34
|
+
* @param protocol - requête HTTP ou handshake WebSocket
|
|
35
|
+
* @returns le message, de forme fixe, dont les champs de résultat valent `null`
|
|
36
|
+
*/
|
|
37
|
+
function openRequestTrace(context, protocol) {
|
|
38
|
+
const trace = {
|
|
39
|
+
requestId: context.requestId,
|
|
40
|
+
protocol,
|
|
41
|
+
method: context.method,
|
|
42
|
+
scheme: context.scheme,
|
|
43
|
+
url: context.url,
|
|
44
|
+
route: null,
|
|
45
|
+
routePath: null,
|
|
46
|
+
module: null,
|
|
47
|
+
statusCode: null,
|
|
48
|
+
durationMs: null,
|
|
49
|
+
error: null
|
|
50
|
+
};
|
|
51
|
+
context.requestTrace = trace;
|
|
52
|
+
return trace;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Exécute la partie synchrone du traitement sous `start.runStores` puis
|
|
56
|
+
* publie `end` — la séquence de `TracingChannel.tracePromise`. Les stores
|
|
57
|
+
* liés au canal (`bindStore`, ce que fait un agent APM) restent actifs pour
|
|
58
|
+
* toute la suite asynchrone de la requête.
|
|
59
|
+
*
|
|
60
|
+
* @param trace - le message de la requête
|
|
61
|
+
* @param fn - la fonction qui sert la requête
|
|
62
|
+
* @param thisArg - le receveur de `fn`
|
|
63
|
+
* @param args - les arguments de `fn`
|
|
64
|
+
* @returns ce que rend `fn` — un contexte ou sa promesse
|
|
65
|
+
*/
|
|
66
|
+
function runTraced(trace, fn, thisArg, ...args) {
|
|
67
|
+
try {
|
|
68
|
+
return startChannel.runStores(trace, fn, thisArg, ...args);
|
|
69
|
+
} finally {
|
|
70
|
+
endChannel.publish(trace);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Termine la trace de la requête : remplit les champs de résultat puis
|
|
75
|
+
* publie `error` (s'il y a une exception), `asyncStart` et `asyncEnd`.
|
|
76
|
+
* Idempotent — le premier appel gagne, les suivants ne font rien : un
|
|
77
|
+
* handshake WebSocket a plusieurs sorties, une seule publie la fin.
|
|
78
|
+
*
|
|
79
|
+
* La durée est celle de l'horloge unique du contexte (`stopClock`), la même
|
|
80
|
+
* que celle de la ligne d'accès : le canal n'en mesure aucune.
|
|
81
|
+
*
|
|
82
|
+
* @param context - le contexte de la requête
|
|
83
|
+
* @param statusCode - le statut rendu au client, `null` s'il n'y en a pas eu
|
|
84
|
+
* @param error - l'exception attrapée par le pipeline, `null` ou `undefined` sinon
|
|
85
|
+
*/
|
|
86
|
+
function closeRequestTrace(context, statusCode, error) {
|
|
87
|
+
const trace = context.requestTrace;
|
|
88
|
+
if (trace === null) return;
|
|
89
|
+
context.requestTrace = null;
|
|
90
|
+
const route = context.resolver?.route ?? null;
|
|
91
|
+
trace.route = route?.name ?? null;
|
|
92
|
+
const path = route?.path;
|
|
93
|
+
trace.routePath = path === void 0 ? null : path === "" ? "/" : path;
|
|
94
|
+
trace.module = context.logModule ?? route?.module?.name ?? null;
|
|
95
|
+
trace.statusCode = statusCode;
|
|
96
|
+
trace.durationMs = context.stopClock();
|
|
97
|
+
if (error !== null && error !== void 0) {
|
|
98
|
+
trace.error = error;
|
|
99
|
+
errorChannel.publish(trace);
|
|
100
|
+
}
|
|
101
|
+
asyncStartChannel.publish(trace);
|
|
102
|
+
asyncEndChannel.publish(trace);
|
|
103
|
+
}
|
|
104
|
+
export { REQUEST_TRACE_CHANNEL, closeRequestTrace, hasRequestTraceSubscribers, openRequestTrace, runTraced };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import http2 from "node:http2";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/context/responseEnded.ts
|
|
3
3
|
/**
|
|
4
4
|
* La réponse a-t-elle été TERMINÉE par le serveur ? Règle UNIQUE, pour HTTP/1.1
|
|
5
5
|
* comme pour HTTP/2 — l'écouteur `close` du noyau (client parti → 499) et le
|
|
@@ -23,5 +23,4 @@ function responseEnded(response) {
|
|
|
23
23
|
const stream = response.stream;
|
|
24
24
|
return stream.writableEnded && !stream.aborted;
|
|
25
25
|
}
|
|
26
|
-
//#endregion
|
|
27
26
|
export { responseEnded };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// nodefony/src/context/timingMode.ts
|
|
2
|
+
/**
|
|
3
|
+
* Résout le mode de chronométrage d'une requête depuis `timing.enabled` et
|
|
4
|
+
* l'environnement — la SEULE règle, appelée par le constructeur du `Context`.
|
|
5
|
+
*
|
|
6
|
+
* Défaut (`null`, pas d'avis) : la durée partout, les phases hors production.
|
|
7
|
+
* La durée coûte deux lectures d'horloge, l'équivalent de quelques pourcents
|
|
8
|
+
* de la ligne d'accès qu'elle complète ; les phases coûtent une lecture par
|
|
9
|
+
* phase, un tableau et un index par requête — réservées au développement, ou
|
|
10
|
+
* à un diagnostic à chaud (`true`). `false` coupe tout, durée comprise.
|
|
11
|
+
*
|
|
12
|
+
* @param enabled - valeur de `timing.enabled` (`true`, `false`, `null`)
|
|
13
|
+
* @param environment - environnement normalisé du noyau (`"production"`…)
|
|
14
|
+
* @returns le mode de la requête — une chaîne littérale, rien d'alloué
|
|
15
|
+
*/
|
|
16
|
+
function resolveTimingMode(enabled, environment) {
|
|
17
|
+
if (enabled === false) return "off";
|
|
18
|
+
if (enabled === true) return "phases";
|
|
19
|
+
return environment === "production" ? "clock" : "phases";
|
|
20
|
+
}
|
|
21
|
+
export { resolveTimingMode };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// nodefony/src/context/transport.ts
|
|
2
|
+
/**
|
|
3
|
+
* Famille du protocole traité par un contexte, déduite du serveur qui l'a
|
|
4
|
+
* reçu. Règle UNIQUE : `Context` la pose à la construction.
|
|
5
|
+
*
|
|
6
|
+
* @param serverType - identité du serveur (`http`, `https`, `ws`, `wss`)
|
|
7
|
+
* @returns `"websocket"` pour `ws`/`wss`, `"http"` sinon
|
|
8
|
+
*/
|
|
9
|
+
function protocolNameOf(serverType) {
|
|
10
|
+
return serverType === "ws" || serverType === "wss" ? "websocket" : "http";
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Protocole du saut qui a porté cette requête jusqu'au processus, au sens du
|
|
14
|
+
* registre IANA des identifiants ALPN. Règle UNIQUE : `HttpContext` la lit pour
|
|
15
|
+
* choisir ses classes de requête et de réponse.
|
|
16
|
+
*
|
|
17
|
+
* La valeur se CONSTATE sur la requête, elle ne se déduit pas du serveur : un
|
|
18
|
+
* même serveur TLS reçoit HTTP/1.1 et h2 (`allowHTTP1`). Pour h2, c'est la
|
|
19
|
+
* session de Node qui tranche — `Http2Session.alpnProtocol` vaut `"h2c"` sur
|
|
20
|
+
* une session en clair, l'ALPN négocié (`"h2"`) sur une session TLS.
|
|
21
|
+
*
|
|
22
|
+
* @param request - la requête Node reçue par le serveur
|
|
23
|
+
* @returns `"http/1.1"`, `"h2"` ou `"h2c"`
|
|
24
|
+
*/
|
|
25
|
+
function alpnProtocolOf(request) {
|
|
26
|
+
if (request.httpVersionMajor !== 2) return "http/1.1";
|
|
27
|
+
return (request.stream?.session)?.alpnProtocol === "h2c" ? "h2c" : "h2";
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Autorité (hôte et port) d'une requête, HTTP/1.1 comme HTTP/2. Règle UNIQUE
|
|
31
|
+
* des lecteurs du serveur.
|
|
32
|
+
*
|
|
33
|
+
* `:authority` d'abord, puis `Host` (RFC 9113 §8.3.1). Le repli n'est pas un
|
|
34
|
+
* confort : un intermédiaire qui convertit une requête HTTP/1.1 de forme origine
|
|
35
|
+
* (`GET /chemin`) en HTTP/2 ne DOIT PAS fabriquer `:authority` — il garde
|
|
36
|
+
* `Host`. C'est ce que fait HAProxy `proto h2` vers un port h2c : sans ce repli,
|
|
37
|
+
* l'hôte est introuvable et la requête part en 421.
|
|
38
|
+
*
|
|
39
|
+
* @param headers - en-têtes de la requête Node (`IncomingMessage` ou `Http2ServerRequest`).
|
|
40
|
+
* @returns l'autorité, ou `undefined` si la requête n'en porte aucune.
|
|
41
|
+
*/
|
|
42
|
+
function requestAuthority(headers) {
|
|
43
|
+
return headers[":authority"] ?? headers.host;
|
|
44
|
+
}
|
|
45
|
+
export { alpnProtocolOf, protocolNameOf, requestAuthority };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BlockList, isIPv4, isIPv6 } from "node:net";
|
|
2
|
-
|
|
2
|
+
// nodefony/src/context/trustProxy.ts
|
|
3
3
|
const PRESETS = {
|
|
4
4
|
loopback: [[
|
|
5
5
|
"127.0.0.0",
|
|
@@ -164,5 +164,4 @@ function extractClientIp(xff, socketAddress, checker) {
|
|
|
164
164
|
}
|
|
165
165
|
return resolveFromRight(s.split(",").map((part) => part.trim()).filter(Boolean), socketAddress, checker);
|
|
166
166
|
}
|
|
167
|
-
//#endregion
|
|
168
167
|
export { buildTrustProxy, extractClientIp, resolveFromRight };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// nodefony/src/context/unsentResponse.ts
|
|
2
|
+
/**
|
|
3
|
+
* Rédige l'avertissement de développement d'une requête restée sans réponse :
|
|
4
|
+
* l'action a rendu une valeur que le framework ne sait pas envoyer, et la
|
|
5
|
+
* requête a attendu son délai d'expiration.
|
|
6
|
+
*
|
|
7
|
+
* Le message nomme l'action fautive, ce qu'elle a rendu, et le geste qui
|
|
8
|
+
* convient à ce cas — `null` est presque toujours une entité absente, qui
|
|
9
|
+
* appelle un 404.
|
|
10
|
+
*
|
|
11
|
+
* @param resolver - le resolver de la requête (route, controller, action,
|
|
12
|
+
* valeur non rendue), `null` si aucune route n'a été résolue.
|
|
13
|
+
* @param url - l'URL de la requête, repli quand aucune route n'est connue.
|
|
14
|
+
* @returns le texte de l'avertissement.
|
|
15
|
+
*/
|
|
16
|
+
function unsentResponseWarning(resolver, url) {
|
|
17
|
+
const controller = resolver?.controller?.name;
|
|
18
|
+
const actionName = resolver?.actionName;
|
|
19
|
+
const action = controller && actionName ? `${controller}.${actionName}` : null;
|
|
20
|
+
const route = resolver?.route?.name ?? null;
|
|
21
|
+
const where = action ? `L'action ${action}${route ? ` (route ${route})` : ""}` : `L'action de ${route ?? url}`;
|
|
22
|
+
const returned = resolver?.unrendered ?? null;
|
|
23
|
+
const consequence = "et n'a rien envoyé : la requête a attendu son délai d'expiration.";
|
|
24
|
+
if (returned === "null") return `${where} a rendu null ${consequence} Pour le framework, null veut dire « l'action répond elle-même ». Entité absente → lever HttpError(…, 404) ; sinon rendre la valeur, ou envoyer la réponse soi-même.`;
|
|
25
|
+
if (returned === "undefined") return `${where} n'a rien rendu ${consequence} Un « return » oublié ? Rendre la valeur (objet, chaîne, nombre, booléen, Buffer : envoyés automatiquement), ou envoyer la réponse soi-même.`;
|
|
26
|
+
if (returned?.startsWith("une instance de")) return `${where} a rendu ${returned}, qui ne s'envoie pas telle quelle, ${consequence} Rendre un objet simple (sa forme JSON), ou envoyer la réponse soi-même.`;
|
|
27
|
+
if (returned !== null) return `${where} a rendu une valeur de type ${returned} ${consequence} Rendre un objet, une chaîne, un nombre, un booléen ou un Buffer, ou envoyer la réponse soi-même.`;
|
|
28
|
+
return `${where} n'a rien envoyé : la requête a attendu son délai d'expiration. Rendre la valeur, ou envoyer la réponse soi-même.`;
|
|
29
|
+
}
|
|
30
|
+
export { unsentResponseWarning };
|
|
@@ -4,7 +4,7 @@ import { toWsCloseReason } from "./wsCloseReason.js";
|
|
|
4
4
|
import { RequestContext } from "nodefony";
|
|
5
5
|
import http from "node:http";
|
|
6
6
|
import Ws from "ws";
|
|
7
|
-
|
|
7
|
+
// nodefony/src/context/websocket/Response.ts
|
|
8
8
|
/**
|
|
9
9
|
* Codes d'erreur d'écriture « le client est PARTI » : reload de page, onglet
|
|
10
10
|
* fermé, HMR — la socket TCP est morte côté client pendant qu'une frame était
|
|
@@ -71,7 +71,11 @@ var WebsocketResponse = class {
|
|
|
71
71
|
const payload = data ?? this.body;
|
|
72
72
|
if (payload == null) throw new Error("no data");
|
|
73
73
|
return new Promise((resolve, reject) => {
|
|
74
|
-
if (!this.connection || this.connection.readyState
|
|
74
|
+
if (!this.connection || this.connection.readyState === Ws.CONNECTING) return reject(/* @__PURE__ */ new Error("WebSocket not open"));
|
|
75
|
+
if (this.connection.readyState !== Ws.OPEN) {
|
|
76
|
+
this.log("WS send: client déjà déconnecté — frame perdue", "DEBUG");
|
|
77
|
+
return resolve(this);
|
|
78
|
+
}
|
|
75
79
|
const { max, policy, closeAfterDrops } = readBackpressureOptions(this.context.server);
|
|
76
80
|
const decision = decideSend(this.connection, max, policy, closeAfterDrops);
|
|
77
81
|
if (decision !== "send") {
|
|
@@ -122,7 +126,7 @@ var WebsocketResponse = class {
|
|
|
122
126
|
setBody(ele, encoding) {
|
|
123
127
|
if (typeof ele === "string") this.body = Buffer.from(ele, encoding ?? this.encoding);
|
|
124
128
|
else if (ele instanceof ArrayBuffer || ele instanceof SharedArrayBuffer) this.body = Buffer.from(ele);
|
|
125
|
-
else if (
|
|
129
|
+
else if (ArrayBuffer.isView(ele)) this.body = Buffer.from(ele.buffer, ele.byteOffset, ele.byteLength);
|
|
126
130
|
return this.body;
|
|
127
131
|
}
|
|
128
132
|
drop(reasonCode, description) {
|
|
@@ -176,14 +180,38 @@ var WebsocketResponse = class {
|
|
|
176
180
|
clean() {
|
|
177
181
|
this.connection = null;
|
|
178
182
|
this.body = null;
|
|
183
|
+
this.handshakeHeaders = null;
|
|
179
184
|
}
|
|
180
185
|
setEncoding(encoding) {
|
|
181
186
|
return this.encoding = encoding;
|
|
182
187
|
}
|
|
183
|
-
|
|
188
|
+
/**
|
|
189
|
+
* En-têtes de la réponse au handshake, retenus tant que la socket n'est pas
|
|
190
|
+
* acceptée : ils ne partent que dans un REFUS (`refuseHandshake`) — le `101`
|
|
191
|
+
* de `ws` ne les porte pas. `null` tant que personne n'en pose (le cas
|
|
192
|
+
* nominal : aucune allocation).
|
|
193
|
+
*/
|
|
194
|
+
handshakeHeaders = null;
|
|
195
|
+
/**
|
|
196
|
+
* Retient un en-tête du handshake (cf {@link handshakeHeaders}) ; sans effet
|
|
197
|
+
* une fois la socket ouverte, où il n'y a plus de réponse HTTP.
|
|
198
|
+
*
|
|
199
|
+
* @param name - nom de l'en-tête
|
|
200
|
+
* @param value - valeur
|
|
201
|
+
* @returns toujours `true`
|
|
202
|
+
*/
|
|
203
|
+
setHeader(name, value) {
|
|
204
|
+
if (this.connection !== null) return true;
|
|
205
|
+
this.handshakeHeaders ??= {};
|
|
206
|
+
this.handshakeHeaders[name] = typeof value === "object" ? value.join(", ") : value;
|
|
184
207
|
return true;
|
|
185
208
|
}
|
|
186
|
-
setHeaders() {
|
|
209
|
+
setHeaders(obj) {
|
|
210
|
+
if (this.connection !== null) return true;
|
|
211
|
+
for (const name of Object.keys(obj)) {
|
|
212
|
+
const value = obj[name];
|
|
213
|
+
if (value !== void 0) this.setHeader(name, value);
|
|
214
|
+
}
|
|
187
215
|
return true;
|
|
188
216
|
}
|
|
189
217
|
addCookie(cookie) {
|
|
@@ -193,5 +221,4 @@ var WebsocketResponse = class {
|
|
|
193
221
|
setCookies() {}
|
|
194
222
|
setCookie(_cookie) {}
|
|
195
223
|
};
|
|
196
|
-
//#endregion
|
|
197
224
|
export { WebsocketResponse as default, isPeerGoneError };
|