@nodefony/http 10.0.0-alpha.5 → 10.0.0-alpha.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/dist/_virtual/{_@oxc-project_runtime@0.149.0 → _@oxc-project_runtime@0.150.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.149.0 → _@oxc-project_runtime@0.150.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.149.0 → _@oxc-project_runtime@0.150.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +2 -2
- package/dist/nodefony/service/http-kernel.js +12 -4
- package/dist/nodefony/service/servers/server-http.js +3 -3
- package/dist/nodefony/service/servers/server-https.js +3 -3
- package/dist/nodefony/service/servers/server-websocket-secure.js +3 -3
- package/dist/nodefony/service/servers/server-websocket.js +3 -3
- package/dist/nodefony/service/sessions/sessions-service.js +3 -3
- package/dist/nodefony/service/upload/upload-service.js +3 -3
- package/dist/nodefony/src/context/http/HttpContext.js +17 -4
- package/dist/nodefony/src/context/http/Response.js +4 -0
- package/dist/nodefony/src/servers/portBinder.js +96 -24
- package/dist/nodefony/src/session/sessionStoreFailure.js +29 -0
- package/dist/types/nodefony/service/http-kernel.d.ts +4 -0
- package/dist/types/nodefony/src/servers/portBinder.d.ts +58 -1
- package/dist/types/nodefony/src/session/sessionStoreFailure.d.ts +36 -0
- package/package.json +7 -7
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorate.js
|
|
2
2
|
function __decorate(decorators, target, key, desc) {
|
|
3
3
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
4
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorateMetadata.js
|
|
2
2
|
function __decorateMetadata(k, v) {
|
|
3
3
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
4
4
|
}
|
package/dist/index.js
CHANGED
|
@@ -21,8 +21,8 @@ import DefaultErrorRenderer, { schemaMismatchOf } from "./nodefony/service/error
|
|
|
21
21
|
import DefaultRequestLogger from "./nodefony/service/request-logger.js";
|
|
22
22
|
import JsonAuditLogger from "./nodefony/service/audit-logger.js";
|
|
23
23
|
import PrettyRequestLogger from "./nodefony/service/pretty-request-logger.js";
|
|
24
|
-
import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.
|
|
25
|
-
import __decorate from "./_virtual/_@oxc-project_runtime@0.
|
|
24
|
+
import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMetadata.js";
|
|
25
|
+
import __decorate from "./_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js";
|
|
26
26
|
import http_kernel_default from "./nodefony/service/http-kernel.js";
|
|
27
27
|
import server_http_default from "./nodefony/service/servers/server-http.js";
|
|
28
28
|
import server_https_default from "./nodefony/service/servers/server-https.js";
|
|
@@ -12,9 +12,9 @@ import DefaultRequestLogger from "./request-logger.js";
|
|
|
12
12
|
import JsonAuditLogger from "./audit-logger.js";
|
|
13
13
|
import PrettyRequestLogger from "./pretty-request-logger.js";
|
|
14
14
|
import { resolveTraceparent } from "./trace.js";
|
|
15
|
-
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.
|
|
16
|
-
import __decorate from "../../_virtual/_@oxc-project_runtime@0.
|
|
17
|
-
import { GcScheduler, Module, RequestContext, Service, injectable, logColor, nodefonyError, writeRuntimeState } from "nodefony";
|
|
15
|
+
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMetadata.js";
|
|
16
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js";
|
|
17
|
+
import { GcScheduler, Module, RequestContext, Service, injectable, logColor, nodefonyError, servedUrl, writeRuntimeState } from "nodefony";
|
|
18
18
|
import cluster from "node:cluster";
|
|
19
19
|
import Ws from "ws";
|
|
20
20
|
//#region nodefony/service/http-kernel.ts
|
|
@@ -597,6 +597,10 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
597
597
|
* Le port DÉSIRÉ est publié à côté du port obtenu : un outil peut ainsi dire
|
|
598
598
|
* « tu voulais 5151, tu écoutes sur 5153 » sans deviner.
|
|
599
599
|
*
|
|
600
|
+
* L'ADRESSE complète l'est aussi, et pour la même raison : un port ne dit pas
|
|
601
|
+
* son protocole. Le serveur est le seul à savoir lequel de ses ports parle TLS ;
|
|
602
|
+
* un lecteur qui le devine se trompe le jour où la topologie change.
|
|
603
|
+
*
|
|
600
604
|
* **Publié dans TOUS les environnements, production comprise.** Le glissement de
|
|
601
605
|
* port (`auto`) n'est pas la seule façon de sortir de la convention : une app qui
|
|
602
606
|
* déclare son port (PaaS `PORT`, ingress, `servers.http.port`) écoute ailleurs
|
|
@@ -615,13 +619,16 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
615
619
|
publishRuntimePorts(serverHttp, serverHttps) {
|
|
616
620
|
const ports = [];
|
|
617
621
|
const desired = [];
|
|
622
|
+
const urls = [];
|
|
618
623
|
if (serverHttp?.active && serverHttp.port > 0) {
|
|
619
624
|
ports.push(serverHttp.port);
|
|
625
|
+
urls.push(servedUrl("http", serverHttp.domain, serverHttp.port));
|
|
620
626
|
const d = this.kernel?.options.servers?.http;
|
|
621
627
|
if (d && d.port) desired.push(d.port);
|
|
622
628
|
}
|
|
623
629
|
if (serverHttps?.active && serverHttps.port > 0) {
|
|
624
630
|
ports.push(serverHttps.port);
|
|
631
|
+
urls.push(servedUrl("https", serverHttps.domain, serverHttps.port));
|
|
625
632
|
const d = this.kernel?.options.servers?.https;
|
|
626
633
|
if (d && d.port) desired.push(d.port);
|
|
627
634
|
}
|
|
@@ -629,7 +636,8 @@ let HttpKernel = class HttpKernel extends Service {
|
|
|
629
636
|
writeRuntimeState(process.cwd(), {
|
|
630
637
|
pid: cluster.isWorker ? process.ppid ?? process.pid : process.pid,
|
|
631
638
|
ports,
|
|
632
|
-
desiredPorts: desired.length > 0 ? desired : void 0
|
|
639
|
+
desiredPorts: desired.length > 0 ? desired : void 0,
|
|
640
|
+
urls
|
|
633
641
|
});
|
|
634
642
|
}
|
|
635
643
|
/**
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js";
|
|
3
3
|
import http_kernel_default from "../http-kernel.js";
|
|
4
4
|
import { handleClientError } from "./clientError.js";
|
|
5
5
|
import { createDrainTerminator } from "./serverShutdown.js";
|
|
6
6
|
import { bindWithFallback, buildBindPlan } from "../../src/servers/portBinder.js";
|
|
7
|
-
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.
|
|
7
|
+
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateParam.js";
|
|
8
8
|
import { Module, Service, extend, inject, nodefonyError } from "nodefony";
|
|
9
9
|
import http from "node:http";
|
|
10
10
|
//#region nodefony/service/servers/server-http.ts
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js";
|
|
3
3
|
import http_kernel_default from "../http-kernel.js";
|
|
4
4
|
import { handleClientError } from "./clientError.js";
|
|
5
5
|
import { createDrainTerminator } from "./serverShutdown.js";
|
|
6
6
|
import { bindWithFallback, buildBindPlan } from "../../src/servers/portBinder.js";
|
|
7
|
-
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.
|
|
7
|
+
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateParam.js";
|
|
8
8
|
import { Module, Service, extend, inject, nodefonyError } from "nodefony";
|
|
9
9
|
import http2 from "node:http2";
|
|
10
10
|
import https from "node:https";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js";
|
|
3
3
|
import http_kernel_default from "../http-kernel.js";
|
|
4
|
-
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.
|
|
4
|
+
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateParam.js";
|
|
5
5
|
import { startHeartbeat, trackPong } from "./wsHeartbeat.js";
|
|
6
6
|
import { Module, Service, inject } from "nodefony";
|
|
7
7
|
import Ws, { WebSocketServer } from "ws";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js";
|
|
3
3
|
import http_kernel_default from "../http-kernel.js";
|
|
4
|
-
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.
|
|
4
|
+
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateParam.js";
|
|
5
5
|
import { startHeartbeat, trackPong } from "./wsHeartbeat.js";
|
|
6
6
|
import { Module, Service, inject } from "nodefony";
|
|
7
7
|
import Ws, { WebSocketServer } from "ws";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { SESSION_FACETS } from "../../src/session/storage/sessionFilters.js";
|
|
2
|
-
import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.
|
|
3
|
-
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.
|
|
2
|
+
import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMetadata.js";
|
|
3
|
+
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js";
|
|
4
4
|
import http_kernel_default from "../http-kernel.js";
|
|
5
|
-
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.
|
|
5
|
+
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateParam.js";
|
|
6
6
|
import Session from "../../src/session/session.js";
|
|
7
7
|
import MemorySessionStorage from "../../src/session/storage/MemorySessionStorage.js";
|
|
8
8
|
import RevocationGuardStorage from "../../src/session/storage/RevocationGuardStorage.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js";
|
|
3
3
|
import http_kernel_default from "../http-kernel.js";
|
|
4
|
-
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.
|
|
4
|
+
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateParam.js";
|
|
5
5
|
import { Cli, FileClass, Module, Service, inject } from "nodefony";
|
|
6
6
|
import fs from "node:fs";
|
|
7
7
|
import fsp from "node:fs/promises";
|
|
@@ -6,6 +6,7 @@ import HttpRequest from "./Request.js";
|
|
|
6
6
|
import HttpResponse from "./Response.js";
|
|
7
7
|
import Http2Request from "../http2/Request.js";
|
|
8
8
|
import Http2Response from "../http2/Response.js";
|
|
9
|
+
import { describeSessionStoreFailure } from "../../session/sessionStoreFailure.js";
|
|
9
10
|
import { extend, typeOf } from "nodefony";
|
|
10
11
|
import url, { URL } from "node:url";
|
|
11
12
|
//#region nodefony/src/context/http/HttpContext.ts
|
|
@@ -156,7 +157,10 @@ var HttpContext = class extends Context {
|
|
|
156
157
|
return this.send(data, encoding);
|
|
157
158
|
}
|
|
158
159
|
async end() {
|
|
159
|
-
return this.saveSession().
|
|
160
|
+
return this.saveSession().catch((e) => {
|
|
161
|
+
this.log(describeSessionStoreFailure(e).message, "CRITIC", "SESSION-STORE");
|
|
162
|
+
return null;
|
|
163
|
+
}).then(async (_session) => {
|
|
160
164
|
return this.close().catch((e) => {
|
|
161
165
|
throw e;
|
|
162
166
|
});
|
|
@@ -180,8 +184,16 @@ var HttpContext = class extends Context {
|
|
|
180
184
|
}
|
|
181
185
|
async #doSend(chunk, encoding) {
|
|
182
186
|
try {
|
|
183
|
-
|
|
184
|
-
if (
|
|
187
|
+
let body = chunk;
|
|
188
|
+
if (this.session != null) try {
|
|
189
|
+
await this.saveSession();
|
|
190
|
+
} catch (e) {
|
|
191
|
+
const failure = describeSessionStoreFailure(e);
|
|
192
|
+
this.log(failure.message, "CRITIC", "SESSION-STORE");
|
|
193
|
+
this.response.statusCode = failure.statusCode;
|
|
194
|
+
body = failure.body;
|
|
195
|
+
}
|
|
196
|
+
if (body) this.response?.setBody(body);
|
|
185
197
|
if (this.listenerCount("onSend") > 0) await this.fireAsync("onSend", this.response, this);
|
|
186
198
|
try {
|
|
187
199
|
this.writeHead();
|
|
@@ -189,7 +201,7 @@ var HttpContext = class extends Context {
|
|
|
189
201
|
this.log(e, "WARNING");
|
|
190
202
|
}
|
|
191
203
|
if (this.isRedirect) return await this.close();
|
|
192
|
-
return await this.write(
|
|
204
|
+
return await this.write(body, encoding);
|
|
193
205
|
} catch (error) {
|
|
194
206
|
this.log(error, "ERROR");
|
|
195
207
|
throw error;
|
|
@@ -222,6 +234,7 @@ var HttpContext = class extends Context {
|
|
|
222
234
|
}
|
|
223
235
|
async close() {
|
|
224
236
|
if (this.listenerCount("onClose") > 0) await this.fireAsync("onClose", this);
|
|
237
|
+
if (this.response?.response?.writableEnded) return this.response;
|
|
225
238
|
return this.response.end().then(() => this.response).catch((e) => {
|
|
226
239
|
throw e;
|
|
227
240
|
});
|
|
@@ -297,6 +297,10 @@ var HttpResponse = class {
|
|
|
297
297
|
if (!this.response) throw new Error(`Http Response not found`);
|
|
298
298
|
if (this.body === null) this.body = Buffer.alloc(0);
|
|
299
299
|
const res = this.response;
|
|
300
|
+
if (!_flush && !this.flushing && !res.writableEnded) {
|
|
301
|
+
res.end(this.body, encoding || this.encoding);
|
|
302
|
+
return this;
|
|
303
|
+
}
|
|
300
304
|
return new Promise((resolve) => {
|
|
301
305
|
let settled = false;
|
|
302
306
|
const done = () => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPortListening } from "nodefony";
|
|
1
2
|
//#region nodefony/src/servers/portBinder.ts
|
|
2
3
|
/** Nombre de ports essayés après le désiré, en `auto`. */
|
|
3
4
|
const DEFAULT_PORT_RETRY_ATTEMPTS = 20;
|
|
@@ -42,6 +43,69 @@ function buildBindPlan(which, servers, environment) {
|
|
|
42
43
|
attempts: policy === "auto" ? servers?.portRetryAttempts ?? 20 : 0
|
|
43
44
|
};
|
|
44
45
|
}
|
|
46
|
+
/** Adresses qui désignent « toutes les interfaces » — rien ne s'y connecte. */
|
|
47
|
+
const WILDCARD_HOSTS = /* @__PURE__ */ new Set([
|
|
48
|
+
"",
|
|
49
|
+
"*",
|
|
50
|
+
"0.0.0.0",
|
|
51
|
+
"::",
|
|
52
|
+
"[::]"
|
|
53
|
+
]);
|
|
54
|
+
/** Adresses de la boucle locale, toutes familles — c'est aussi ce qu'on REND. */
|
|
55
|
+
const LOOPBACK_HOSTS = ["127.0.0.1", "::1"];
|
|
56
|
+
/** Les mêmes, plus le nom qui les désigne — DÉRIVÉ, jamais recopié. */
|
|
57
|
+
const LOOPBACK_ALIASES = /* @__PURE__ */ new Set([...LOOPBACK_HOSTS, "localhost"]);
|
|
58
|
+
/**
|
|
59
|
+
* Adresses à interroger pour savoir si notre future liaison chevauche déjà un
|
|
60
|
+
* serveur vivant.
|
|
61
|
+
*
|
|
62
|
+
* La règle est celle du TRAFIC, pas celle du noyau : on sonde ce que notre
|
|
63
|
+
* liaison va RECEVOIR.
|
|
64
|
+
* - Écouter sur **toutes les interfaces** recouvre la boucle locale : on sonde
|
|
65
|
+
* ses deux adresses (`127.0.0.1` et `::1`). Un wildcard n'est pas une
|
|
66
|
+
* destination, on ne peut pas s'y connecter.
|
|
67
|
+
* - Écouter sur la **boucle locale** (`localhost` résout `::1` ici, `127.0.0.1`
|
|
68
|
+
* ailleurs — et les clients ne choisissent pas) : les deux, là encore.
|
|
69
|
+
* - Écouter sur une **adresse d'interface précise** : elle seule. Une connexion
|
|
70
|
+
* vers elle atteint le tiers qu'il ait lié cette adresse ou le wildcard, donc
|
|
71
|
+
* une cible suffit — et sonder la boucle locale ferait refuser un pod à cause
|
|
72
|
+
* d'un voisin de conteneur qui n'a jamais été en conflit.
|
|
73
|
+
*
|
|
74
|
+
* Fonction PURE : le banc l'éprouve sans ouvrir un socket.
|
|
75
|
+
*
|
|
76
|
+
* @param host - hôte passé à `listen` (`kernel.domain`).
|
|
77
|
+
* @returns les adresses à interroger, dans l'ordre.
|
|
78
|
+
*/
|
|
79
|
+
function conflictProbeTargets(host) {
|
|
80
|
+
const value = (host ?? "").trim().toLowerCase();
|
|
81
|
+
if (WILDCARD_HOSTS.has(value) || LOOPBACK_ALIASES.has(value)) return LOOPBACK_HOSTS;
|
|
82
|
+
return [value];
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Première adresse où un serveur répond DÉJÀ sur ce port, `null` si la voie est
|
|
86
|
+
* libre.
|
|
87
|
+
*
|
|
88
|
+
* @param port - port candidat.
|
|
89
|
+
* @param host - hôte que `listen` recevra (décide des adresses interrogées).
|
|
90
|
+
* @param probe - sonde de présence (injectée par le banc).
|
|
91
|
+
*/
|
|
92
|
+
async function detectPortConflict(port, host, probe) {
|
|
93
|
+
for (const target of conflictProbeTargets(host)) if (await probe(port, target)) return target;
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Erreur d'un conflit que le noyau aurait laissé passer.
|
|
98
|
+
*
|
|
99
|
+
* Porte `EADDRINUSE` : pour l'appelant c'est le même fait — le port n'est pas
|
|
100
|
+
* prenable — et une seule branche le traite. Le message, lui, doit dire ce que
|
|
101
|
+
* le code d'erreur seul ferait chercher au mauvais endroit : ici `listen` aurait
|
|
102
|
+
* RÉUSSI, et c'est la liaison d'une AUTRE adresse qui capte le trafic.
|
|
103
|
+
*/
|
|
104
|
+
function portConflictError(port, host) {
|
|
105
|
+
const error = /* @__PURE__ */ new Error(`Port ${port} déjà servi par un autre processus sur ${host} — le noyau l'aurait pourtant accordé (deux adresses distinctes coexistent sur le même port), et cette application aurait écouté sans rien recevoir. Libérer le port (nodefony status · nodefony stop), en figer un autre (servers.http.port / servers.https.port), ou autoriser le repli (servers.portPolicy = "auto", défaut en développement).`);
|
|
106
|
+
error.code = "EADDRINUSE";
|
|
107
|
+
return error;
|
|
108
|
+
}
|
|
45
109
|
/** Prochain candidat : incrémente, en sautant ce que les autres serveurs veulent. */
|
|
46
110
|
function nextCandidate(from, reserved) {
|
|
47
111
|
let port = from + 1;
|
|
@@ -84,31 +148,39 @@ function isPortUnavailable(code, port) {
|
|
|
84
148
|
* (`ENOTFOUND`, `EACCES` sous 1024 — cf `isPortUnavailable`), soit un port
|
|
85
149
|
* indisponible après épuisement des essais (le fallback n'est PAS infini).
|
|
86
150
|
*/
|
|
87
|
-
function bindWithFallback(server, host, plan) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const onListening = () => {
|
|
100
|
-
server.removeListener("error", onError);
|
|
101
|
-
resolve({
|
|
102
|
-
address: server.address(),
|
|
103
|
-
shiftedFrom: candidate === plan.desired ? null : plan.desired
|
|
104
|
-
});
|
|
105
|
-
};
|
|
106
|
-
server.once("error", onError);
|
|
107
|
-
server.once("listening", onListening);
|
|
108
|
-
server.listen(candidate, host);
|
|
151
|
+
async function bindWithFallback(server, host, plan, probe = isPortListening) {
|
|
152
|
+
let candidate = plan.desired;
|
|
153
|
+
let used = 0;
|
|
154
|
+
/**
|
|
155
|
+
* Une tentative d'écoute. L'erreur est RENDUE, jamais levée depuis le
|
|
156
|
+
* callback : les deux écouteurs se retirent dans les deux cas, et la boucle
|
|
157
|
+
* ci-dessous reste le seul endroit qui décide.
|
|
158
|
+
*/
|
|
159
|
+
const listenOnce = (port) => new Promise((settle) => {
|
|
160
|
+
const onError = (error) => {
|
|
161
|
+
server.removeListener("listening", onListening);
|
|
162
|
+
settle(error);
|
|
109
163
|
};
|
|
110
|
-
|
|
164
|
+
const onListening = () => {
|
|
165
|
+
server.removeListener("error", onError);
|
|
166
|
+
settle(null);
|
|
167
|
+
};
|
|
168
|
+
server.once("error", onError);
|
|
169
|
+
server.once("listening", onListening);
|
|
170
|
+
server.listen(port, host);
|
|
111
171
|
});
|
|
172
|
+
for (;;) {
|
|
173
|
+
const conflict = plan.desired === 0 || candidate !== plan.desired ? null : await detectPortConflict(candidate, host, probe);
|
|
174
|
+
const failure = conflict ? portConflictError(candidate, conflict) : await listenOnce(candidate);
|
|
175
|
+
if (failure === null) return {
|
|
176
|
+
address: server.address(),
|
|
177
|
+
shiftedFrom: candidate === plan.desired ? null : plan.desired
|
|
178
|
+
};
|
|
179
|
+
if (!conflict && !isPortUnavailable(failure.code, candidate)) throw failure;
|
|
180
|
+
if (plan.desired === 0 || used >= plan.attempts) throw failure;
|
|
181
|
+
used += 1;
|
|
182
|
+
candidate = nextCandidate(candidate, plan.reserved);
|
|
183
|
+
}
|
|
112
184
|
}
|
|
113
185
|
//#endregion
|
|
114
|
-
export { DEFAULT_PORT_RETRY_ATTEMPTS, bindWithFallback, buildBindPlan, resolvePortPolicy };
|
|
186
|
+
export { DEFAULT_PORT_RETRY_ATTEMPTS, bindWithFallback, buildBindPlan, conflictProbeTargets, detectPortConflict, resolvePortPolicy };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region nodefony/src/session/sessionStoreFailure.ts
|
|
2
|
+
/**
|
|
3
|
+
* Reconnaît une table de session absente, quel que soit le dialecte.
|
|
4
|
+
*
|
|
5
|
+
* Chaque moteur formule le même défaut à sa façon, et aucun ne porte de code
|
|
6
|
+
* commun : SQLite dit « no such table », PostgreSQL « relation ... does not
|
|
7
|
+
* exist », MySQL « doesn't exist ». On reste volontairement large — le seul
|
|
8
|
+
* enjeu est de savoir s'il faut PARLER DE MIGRATIONS, jamais de trancher une
|
|
9
|
+
* branche de code.
|
|
10
|
+
*/
|
|
11
|
+
function looksLikeMissingTable(text) {
|
|
12
|
+
return /no such table/i.test(text) || /does ?n[o']?t exist/i.test(text) || /undefined table/i.test(text) || /unknown table/i.test(text);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Compose la réponse et le journal d'une panne du stockage de session.
|
|
16
|
+
*
|
|
17
|
+
* @param error - l'erreur remontée par le store.
|
|
18
|
+
* @returns le statut, le corps à servir, et la ligne de journal.
|
|
19
|
+
*/
|
|
20
|
+
function describeSessionStoreFailure(error) {
|
|
21
|
+
const cause = error instanceof Error ? error.message : String(error ?? "cause inconnue");
|
|
22
|
+
return {
|
|
23
|
+
statusCode: 500,
|
|
24
|
+
body: "Internal Server Error — session store unavailable",
|
|
25
|
+
message: `La session n'a pas pu être persistée — la requête est servie en 500 plutôt que laissée sans réponse. Cause : ${cause}.${looksLikeMissingTable(cause) ? " La table de session est probablement absente : appliquer les migrations (`nodefony orm:migrate`) avant de servir, ou choisir un autre stockage (`http.session.store`)." : " Vérifier que le stockage de session déclaré sous `http.session.store` est joignable ; à défaut, appliquer les migrations (`nodefony orm:migrate`)."}`
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
export { describeSessionStoreFailure };
|
|
@@ -314,6 +314,10 @@ declare class HttpKernel extends Service implements IHttpKernelInterface {
|
|
|
314
314
|
* Le port DÉSIRÉ est publié à côté du port obtenu : un outil peut ainsi dire
|
|
315
315
|
* « tu voulais 5151, tu écoutes sur 5153 » sans deviner.
|
|
316
316
|
*
|
|
317
|
+
* L'ADRESSE complète l'est aussi, et pour la même raison : un port ne dit pas
|
|
318
|
+
* son protocole. Le serveur est le seul à savoir lequel de ses ports parle TLS ;
|
|
319
|
+
* un lecteur qui le devine se trompe le jour où la topologie change.
|
|
320
|
+
*
|
|
317
321
|
* **Publié dans TOUS les environnements, production comprise.** Le glissement de
|
|
318
322
|
* port (`auto`) n'est pas la seule façon de sortir de la convention : une app qui
|
|
319
323
|
* déclare son port (PaaS `PORT`, ingress, `servers.http.port`) écoute ailleurs
|
|
@@ -14,6 +14,24 @@
|
|
|
14
14
|
* `listen()` est, lui, **atomique** : soit il réussit, soit le noyau dit
|
|
15
15
|
* `EADDRINUSE`. On retente donc sur l'échec réel, jamais sur une prédiction.
|
|
16
16
|
*
|
|
17
|
+
* ## Pourquoi une SONDE malgré cela
|
|
18
|
+
*
|
|
19
|
+
* Le `listen()` est atomique, mais il ne dit pas tout : un port peut être **déjà
|
|
20
|
+
* servi** et le noyau l'accorder quand même. Lier `0.0.0.0:P` alors qu'un autre
|
|
21
|
+
* processus tient `127.0.0.1:P` **réussit** sur macOS et les BSD — ce sont deux
|
|
22
|
+
* liaisons distinctes, et les connexions locales partent à la PLUS SPÉCIFIQUE.
|
|
23
|
+
* Entre familles d'adresses (`::1` tenu, `0.0.0.0` demandé), même linux
|
|
24
|
+
* l'accorde. L'application annonce alors `READY`, publie ses ports… et ne reçoit
|
|
25
|
+
* rien : une panne muette, et un banc qui interroge le serveur du VOISIN.
|
|
26
|
+
*
|
|
27
|
+
* On constate donc, AVANT de binder, qu'aucun serveur ne répond déjà là où notre
|
|
28
|
+
* liaison va recevoir (`isPortListening` — une connexion en boucle locale, aucun
|
|
29
|
+
* outil système, même verdict sur les trois plateformes). Ce n'est pas la sonde
|
|
30
|
+
* « ce port est-il libre ? » écartée plus haut : elle ne remplace pas le repli
|
|
31
|
+
* atomique, qui reste le seul à trancher la course — elle ajoute le seul conflit
|
|
32
|
+
* que le noyau n'exprime JAMAIS. La course résiduelle (un tiers prend le port
|
|
33
|
+
* entre la sonde et le bind) retombe donc sur `EADDRINUSE`, inchangé.
|
|
34
|
+
*
|
|
17
35
|
* ## Pourquoi sauter les ports réservés
|
|
18
36
|
*
|
|
19
37
|
* HTTP veut 5151, HTTPS veut 5152. Si 5151 est pris, incrémenter naïvement ferait
|
|
@@ -86,6 +104,45 @@ export interface ServersPortConfig {
|
|
|
86
104
|
* @param environment - `kernel.environment` (arbitre le défaut de la politique).
|
|
87
105
|
*/
|
|
88
106
|
export declare function buildBindPlan(which: "http" | "https", servers: ServersPortConfig | undefined, environment: string | undefined): BindPlan;
|
|
107
|
+
/**
|
|
108
|
+
* Sonde « un serveur répond-il déjà ici ? ».
|
|
109
|
+
*
|
|
110
|
+
* Injectable pour que le banc éprouve la DÉCISION sans dépendre d'un décor
|
|
111
|
+
* réseau ; le défaut est `isPortListening`, exposé par le cœur pour ne pas être
|
|
112
|
+
* recopié de paquet en paquet.
|
|
113
|
+
*/
|
|
114
|
+
export type PortListeningProbe = (port: number, host: string) => Promise<boolean>;
|
|
115
|
+
/**
|
|
116
|
+
* Adresses à interroger pour savoir si notre future liaison chevauche déjà un
|
|
117
|
+
* serveur vivant.
|
|
118
|
+
*
|
|
119
|
+
* La règle est celle du TRAFIC, pas celle du noyau : on sonde ce que notre
|
|
120
|
+
* liaison va RECEVOIR.
|
|
121
|
+
* - Écouter sur **toutes les interfaces** recouvre la boucle locale : on sonde
|
|
122
|
+
* ses deux adresses (`127.0.0.1` et `::1`). Un wildcard n'est pas une
|
|
123
|
+
* destination, on ne peut pas s'y connecter.
|
|
124
|
+
* - Écouter sur la **boucle locale** (`localhost` résout `::1` ici, `127.0.0.1`
|
|
125
|
+
* ailleurs — et les clients ne choisissent pas) : les deux, là encore.
|
|
126
|
+
* - Écouter sur une **adresse d'interface précise** : elle seule. Une connexion
|
|
127
|
+
* vers elle atteint le tiers qu'il ait lié cette adresse ou le wildcard, donc
|
|
128
|
+
* une cible suffit — et sonder la boucle locale ferait refuser un pod à cause
|
|
129
|
+
* d'un voisin de conteneur qui n'a jamais été en conflit.
|
|
130
|
+
*
|
|
131
|
+
* Fonction PURE : le banc l'éprouve sans ouvrir un socket.
|
|
132
|
+
*
|
|
133
|
+
* @param host - hôte passé à `listen` (`kernel.domain`).
|
|
134
|
+
* @returns les adresses à interroger, dans l'ordre.
|
|
135
|
+
*/
|
|
136
|
+
export declare function conflictProbeTargets(host: string | undefined): readonly string[];
|
|
137
|
+
/**
|
|
138
|
+
* Première adresse où un serveur répond DÉJÀ sur ce port, `null` si la voie est
|
|
139
|
+
* libre.
|
|
140
|
+
*
|
|
141
|
+
* @param port - port candidat.
|
|
142
|
+
* @param host - hôte que `listen` recevra (décide des adresses interrogées).
|
|
143
|
+
* @param probe - sonde de présence (injectée par le banc).
|
|
144
|
+
*/
|
|
145
|
+
export declare function detectPortConflict(port: number, host: string | undefined, probe: PortListeningProbe): Promise<string | null>;
|
|
89
146
|
/**
|
|
90
147
|
* Écoute sur `plan.desired`, ou sur le prochain port libre si `attempts > 0`.
|
|
91
148
|
*
|
|
@@ -99,4 +156,4 @@ export declare function buildBindPlan(which: "http" | "https", servers: ServersP
|
|
|
99
156
|
* (`ENOTFOUND`, `EACCES` sous 1024 — cf `isPortUnavailable`), soit un port
|
|
100
157
|
* indisponible après épuisement des essais (le fallback n'est PAS infini).
|
|
101
158
|
*/
|
|
102
|
-
export declare function bindWithFallback(server: Listenable, host: string, plan: BindPlan): Promise<BindResult>;
|
|
159
|
+
export declare function bindWithFallback(server: Listenable, host: string, plan: BindPlan, probe?: PortListeningProbe): Promise<BindResult>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ce qu'on répond quand le stockage de session est inutilisable.
|
|
3
|
+
*
|
|
4
|
+
* Une requête doit TOUJOURS recevoir une réponse. La sauvegarde de session a
|
|
5
|
+
* lieu juste avant l'écriture des en-têtes : une exception qui remonte de là
|
|
6
|
+
* laisse la socket ouverte, et le client attend jusqu'à son propre délai. Rien
|
|
7
|
+
* dans ses journaux ne nomme la cause — l'exploitant cherche du côté du réseau
|
|
8
|
+
* ou de la charge, et le banc de tenue dans la durée est resté rouge dix jours
|
|
9
|
+
* pour cette seule raison.
|
|
10
|
+
*
|
|
11
|
+
* Le cas de loin le plus courant est une application mise en ligne **sans ses
|
|
12
|
+
* migrations** : la table de session n'existe pas, et `session.store: "auto"`
|
|
13
|
+
* retombe sur SQLite dès que l'ORM est chargé. C'est un oubli banal que le
|
|
14
|
+
* framework doit rendre BRUYANT.
|
|
15
|
+
*
|
|
16
|
+
* Pourquoi 500 et non une réponse normale : une session qu'on ne sait pas
|
|
17
|
+
* persister est une **dégradation**, pas une indisponibilité passagère. Servir
|
|
18
|
+
* 200 laisserait croire que la connexion a été retenue alors que rien n'a été
|
|
19
|
+
* écrit — c'est exactement la dégradation silencieuse que ce framework refuse.
|
|
20
|
+
*/
|
|
21
|
+
/** La conduite à tenir : ce qu'on répond, et ce qu'on écrit dans le journal. */
|
|
22
|
+
export interface ISessionStoreFailure {
|
|
23
|
+
/** Le statut à servir — toujours 500 : le serveur n'a pas tenu sa part. */
|
|
24
|
+
statusCode: number;
|
|
25
|
+
/** Le corps servi au client : sobre, il ne divulgue aucun détail interne. */
|
|
26
|
+
body: string;
|
|
27
|
+
/** La ligne de journal côté serveur — elle NOMME la cause et le remède. */
|
|
28
|
+
message: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Compose la réponse et le journal d'une panne du stockage de session.
|
|
32
|
+
*
|
|
33
|
+
* @param error - l'erreur remontée par le store.
|
|
34
|
+
* @returns le statut, le corps à servir, et la ligne de journal.
|
|
35
|
+
*/
|
|
36
|
+
export declare function describeSessionStoreFailure(error: unknown): ISessionStoreFailure;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodefony/http",
|
|
3
|
-
"version": "10.0.0-alpha.
|
|
3
|
+
"version": "10.0.0-alpha.7",
|
|
4
4
|
"description": "Serveurs HTTP, HTTPS, HTTP/2 et WebSocket natifs pour Nodefony : sessions, contextes de requête, certificats TLS",
|
|
5
5
|
"author": "Christophe CAMENSULI <ccamensuli@gmail.com>",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -66,26 +66,26 @@
|
|
|
66
66
|
"@types/chai": "5.2.3",
|
|
67
67
|
"@types/mime-types": "3.0.1",
|
|
68
68
|
"@types/ms": "2.1.0",
|
|
69
|
-
"@types/node": "26.
|
|
69
|
+
"@types/node": "26.6.1",
|
|
70
70
|
"@types/node-forge": "1.3.14",
|
|
71
71
|
"@types/qs": "6.15.1",
|
|
72
72
|
"@types/serve-static": "2.2.0",
|
|
73
73
|
"@types/websocket": "1.0.10",
|
|
74
74
|
"@types/ws": "8.18.1",
|
|
75
75
|
"@types/xml2js": "0.4.14",
|
|
76
|
-
"@vitest/coverage-v8": "5.0.
|
|
76
|
+
"@vitest/coverage-v8": "5.0.1",
|
|
77
77
|
"chai": "6.2.2",
|
|
78
|
-
"nodefony": "^10.0.0-alpha.
|
|
78
|
+
"nodefony": "^10.0.0-alpha.7",
|
|
79
79
|
"rimraf": "6.1.3",
|
|
80
80
|
"tsx": "4.23.13",
|
|
81
|
-
"vitest": "5.0.
|
|
81
|
+
"vitest": "5.0.1"
|
|
82
82
|
},
|
|
83
83
|
"license": "Apache-2.0",
|
|
84
84
|
"readmeFilename": "README.md",
|
|
85
85
|
"contributors": [],
|
|
86
86
|
"peerDependencies": {
|
|
87
|
-
"nodefony": "^10.0.0-alpha.
|
|
88
|
-
"zod": "^4.6.
|
|
87
|
+
"nodefony": "^10.0.0-alpha.7",
|
|
88
|
+
"zod": "^4.6.5"
|
|
89
89
|
},
|
|
90
90
|
"files": [
|
|
91
91
|
"dist",
|