@nodefony/http 10.0.0-beta.4 → 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.
Files changed (42) hide show
  1. package/dist/index.js +3 -2
  2. package/dist/nodefony/config/config.js +2 -1
  3. package/dist/nodefony/interfaces/IRequestTrace.js +1 -0
  4. package/dist/nodefony/service/ProfilerAdminApi.js +12 -1
  5. package/dist/nodefony/service/audit-logger.js +3 -17
  6. package/dist/nodefony/service/http-kernel.js +80 -7
  7. package/dist/nodefony/service/pretty-request-logger.js +11 -10
  8. package/dist/nodefony/src/context/Context.js +56 -13
  9. package/dist/nodefony/src/context/http/HttpContext.js +21 -0
  10. package/dist/nodefony/src/context/http2/Response.js +20 -1
  11. package/dist/nodefony/src/context/requestTrace.js +104 -0
  12. package/dist/nodefony/src/context/timingMode.js +21 -0
  13. package/dist/nodefony/src/context/unsentResponse.js +30 -0
  14. package/dist/nodefony/src/context/websocket/WebsocketContext.js +12 -8
  15. package/dist/nodefony/src/profiler/FrameProfile.js +22 -0
  16. package/dist/nodefony/src/profiler/Profiler.js +11 -21
  17. package/dist/nodefony/src/profiler/aggregate.js +26 -6
  18. package/dist/types/index.d.ts +2 -0
  19. package/dist/types/nodefony/config/config.d.ts +4 -0
  20. package/dist/types/nodefony/interfaces/IContext.d.ts +9 -0
  21. package/dist/types/nodefony/interfaces/IRequestTrace.d.ts +56 -0
  22. package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +8 -0
  23. package/dist/types/nodefony/interfaces/IRouting.d.ts +16 -0
  24. package/dist/types/nodefony/interfaces/index.d.ts +1 -0
  25. package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +3 -0
  26. package/dist/types/nodefony/service/audit-logger.d.ts +3 -11
  27. package/dist/types/nodefony/service/http-kernel.d.ts +50 -0
  28. package/dist/types/nodefony/src/context/Context.d.ts +25 -1
  29. package/dist/types/nodefony/src/context/http/HttpContext.d.ts +15 -1
  30. package/dist/types/nodefony/src/context/http2/Response.d.ts +12 -0
  31. package/dist/types/nodefony/src/context/requestTrace.d.ts +64 -0
  32. package/dist/types/nodefony/src/context/timingMode.d.ts +23 -0
  33. package/dist/types/nodefony/src/context/unsentResponse.d.ts +16 -0
  34. package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +1 -1
  35. package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +15 -0
  36. package/dist/types/nodefony/src/profiler/Profiler.d.ts +16 -101
  37. package/dist/types/nodefony/src/profiler/aggregate.d.ts +23 -106
  38. package/docs/diagnostics-channel.md +303 -0
  39. package/docs/index.md +6 -1
  40. package/docs/observabilite.md +94 -16
  41. package/docs/reverse-proxy.md +18 -16
  42. package/package.json +3 -3
@@ -0,0 +1,303 @@
1
+ ---
2
+ title: "Canal de diagnostic — le cycle de chaque requête publié sur diagnostics_channel"
3
+ navTitle: Canal de diagnostic
4
+ lang: fr
5
+ module: "@nodefony/http"
6
+ topic: diagnostics-channel
7
+ section: "Cœur runtime"
8
+ audience: [developer, devops]
9
+ tags:
10
+ [
11
+ observabilite,
12
+ diagnostics_channel,
13
+ tracingchannel,
14
+ opentelemetry,
15
+ apm,
16
+ trace,
17
+ requestId,
18
+ websocket,
19
+ ]
20
+ version: "doc"
21
+ status: stable
22
+ updated: 2026-10-24
23
+ source: "src/packages/@nodefony/http/docs/diagnostics-channel.md"
24
+ coverageModule: http
25
+ coverageFiles: requestTrace.ts
26
+ ---
27
+
28
+ # Canal de diagnostic — le cycle de chaque requête publié sur `diagnostics_channel`
29
+
30
+ > Nodefony **annonce** le début et la fin de chaque requête HTTP et de chaque handshake WebSocket
31
+ > sur un canal natif de Node, `nodefony.http.request`. Un outil d'observation installé dans le même
32
+ > process — OpenTelemetry, Datadog, Sentry, ou votre propre code — s'y abonne et reçoit, sans
33
+ > monkey-patch, ce que Nodefony est seul à connaître : la **route** qui a répondu, son **gabarit**,
34
+ > le **module** signataire, le `requestId`, le statut réel et la durée. Personne n'écoute ? Le canal
35
+ > coûte une lecture de booléen par requête, et rien n'est alloué.
36
+
37
+ 📍 [Documentation](../../../../../docs/index.md) › [@nodefony/http](index.md) › **Canal de diagnostic**
38
+
39
+ ## Schéma général
40
+
41
+ ```mermaid
42
+ sequenceDiagram
43
+ participant C as Client
44
+ participant N as Nodefony (pipeline)
45
+ participant A as Abonné (agent APM)
46
+ C->>N: GET /users/42
47
+ N->>A: start — requestId, méthode, URL
48
+ N->>A: end — la partie synchrone a rendu la main
49
+ N->>C: 200 (dernier octet remis)
50
+ N->>A: asyncStart puis asyncEnd — route, gabarit, module, statut, durée
51
+ ```
52
+
53
+ Le **même objet** voyage de `start` à `asyncEnd` : l'abonné y lit à la fin ce qui n'était pas
54
+ encore connu au début.
55
+
56
+ ## Lexique
57
+
58
+ | Terme | Sens |
59
+ | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
60
+ | `diagnostics_channel` | Module natif de Node (`node:diagnostics_channel`) : un bus de publication NOMMÉ, interne au process, fait pour que des outils observent une bibliothèque. |
61
+ | Canal | Un nom (`tracing:nodefony.http.request:start`) ; publieur et abonné le retrouvent par ce nom, sans s'importer l'un l'autre. |
62
+ | `TracingChannel` | La forme « trace » du bus : cinq sous-canaux (`start`, `end`, `asyncStart`, `asyncEnd`, `error`) qui encadrent UNE opération, avec un objet partagé. |
63
+ | `runStores` / `bindStore` | Un abonné lie un `AsyncLocalStorage` au canal (`bindStore`) ; le publieur entre dans ce store pour la durée de l'opération (`runStores`). |
64
+ | APM | _Application Performance Monitoring_ : agent (Datadog, New Relic, Elastic…) qui mesure et trace une application en production. |
65
+ | OTel | _OpenTelemetry_ : standard ouvert de traces, métriques et journaux, et ses conventions de nommage d'attributs (« semconv »). |
66
+ | Span | Une opération chronométrée d'une trace distribuée — ici, une requête serveur. |
67
+ | Gabarit de route | Le chemin déclaré (`/users/{id}`), pas l'URL reçue (`/users/42`) : faible cardinalité, donc agrégeable. C'est l'attribut `http.route` d'OTel. |
68
+ | `requestId` | Identifiant de corrélation de la requête (`X-Request-Id`), voir [Observabilité HTTP](observabilite.md). |
69
+
70
+ ## Qu'est-ce que c'est ?
71
+
72
+ Un **haut-parleur interne au process**. Nodefony annonce « une requête commence », puis « elle est
73
+ finie, voici son bilan ». Quiconque s'est mis à l'écoute de ce canal, dans le même process, reçoit
74
+ l'annonce — immédiatement, sur le même fil, avec le même objet : pas de copie, pas de réseau, pas
75
+ de fichier. Rien ne sort du process par lui-même : c'est l'abonné qui décide d'exporter, avec son
76
+ propre transport (OTLP vers un collecteur, agent Datadog, Sentry).
77
+
78
+ Le problème qu'il résout : sans lui, un outil de traçage ne voit d'une application Nodefony que ce
79
+ que `node:http` publie — une requête brute, sans route, sans module, sans `requestId`. Pour en savoir
80
+ plus, il devrait patcher les classes du framework à l'exécution, ce qui casse à chaque version.
81
+ Node publie bien ses propres canaux serveur (`http.server.request.start`,
82
+ `http.server.response.finish`), mais ils ne portent que la requête et la réponse brutes : ce que
83
+ Nodefony a décidé — quelle route, quel module, quel statut réel derrière un proxy — n'y figure pas.
84
+
85
+ ## La vision Nodefony
86
+
87
+ - **Un contrat, pas un crochet** : le canal et la forme de son message (`IRequestTrace`) sont une
88
+ API publique de `@nodefony/http`. Un plugin d'APM s'écrit contre eux, et non contre l'intérieur du
89
+ pipeline. C'est un contrat neuf : les agents existants ne le lisent pas d'office, un plugin doit
90
+ le consommer (comme Fastify publie `tracing:fastify.request.handler` pour les siens).
91
+ - **Zéro coût sans abonné** : la garde (`requestTrace.ts#hasRequestTraceSubscribers`) est lue UNE
92
+ fois, au début de la requête. Faux → aucun message n'est construit, la fin ne lit qu'un champ
93
+ `null` (`Context.ts#Context.requestTrace`). Pas d'option de configuration : « éteint » veut dire
94
+ « personne n'écoute ».
95
+ - **Une seule horloge** : la durée publiée est celle de la ligne d'accès (`Context.ts#Context.stopClock`),
96
+ jamais une mesure propre au canal.
97
+ - **La séquence de Node, à la main** : `start` sous `runStores` à l'entrée, `end` quand la partie
98
+ synchrone rend la main (`requestTrace.ts#runTraced`), puis `error`, `asyncStart` et `asyncEnd` au
99
+ démontage, quand le dernier octet est remis (`requestTrace.ts#closeRequestTrace`). C'est l'ordre
100
+ de `TracingChannel.tracePromise`, que les abonnés génériques attendent ; la publication est
101
+ manuelle parce que la fin d'une requête arrive sur l'événement `close` de la réponse, hors de
102
+ toute fonction qu'on pourrait envelopper.
103
+ - **Les stores des abonnés sont honorés** : un agent qui lie son `AsyncLocalStorage` au canal
104
+ (`bindStore`) voit son contexte actif pendant tout le traitement — les requêtes SQL et les appels
105
+ sortants d'une route deviennent des enfants de son span.
106
+
107
+ ## Démarrage rapide
108
+
109
+ Un abonné de dix lignes, dans un fichier chargé AVANT l'application — c'est ainsi que se chargent
110
+ les agents d'APM :
111
+
112
+ ```ts
113
+ // tracing.ts — chargé par `--import`, avant l'application
114
+ import diagnosticsChannel from "node:diagnostics_channel";
115
+ import { REQUEST_TRACE_CHANNEL } from "@nodefony/http";
116
+ import type { IRequestTrace } from "@nodefony/http";
117
+
118
+ const channel = diagnosticsChannel.tracingChannel<IRequestTrace>(
119
+ REQUEST_TRACE_CHANNEL,
120
+ );
121
+
122
+ channel.subscribe({
123
+ start: () => {},
124
+ end: () => {},
125
+ asyncStart: () => {},
126
+ asyncEnd: (trace) => {
127
+ console.log(
128
+ `${trace.requestId} ${trace.method} ${trace.routePath ?? "-"} → ${trace.statusCode} en ${trace.durationMs?.toFixed(3)} ms (${trace.module})`,
129
+ );
130
+ },
131
+ error: (trace) => {
132
+ console.error(trace.requestId, trace.error);
133
+ },
134
+ });
135
+ ```
136
+
137
+ On le charge par `NODE_OPTIONS`, que les process lancés par la commande de développement héritent :
138
+
139
+ ```bash
140
+ NODE_OPTIONS="--import ./tracing.ts" npx nodefony development
141
+ curl -k https://127.0.0.1:5152/
142
+ ```
143
+
144
+ Ce qu'on observe, une ligne par requête servie — ici la page d'accueil d'une application générée :
145
+
146
+ ```text
147
+ 6f1c…-9e2a GET / → 200 en 3.528 ms (app)
148
+ ```
149
+
150
+ Importer `REQUEST_TRACE_CHANNEL` plutôt que recopier la chaîne : le nom vit en un seul endroit.
151
+
152
+ ## Le message — `IRequestTrace`
153
+
154
+ Déclaré dans `IRequestTrace.ts#IRequestTrace`, exporté par `@nodefony/http`. Tout est en lecture
155
+ seule pour l'abonné.
156
+
157
+ | Champ | Posé à | Contenu |
158
+ | ------------ | ------- | ------------------------------------------------------------------------------------------------------------------------ |
159
+ | `requestId` | `start` | identifiant de corrélation (celui de `X-Request-Id` entrant, sinon généré) |
160
+ | `protocol` | `start` | `"http"` pour une requête, `"websocket"` pour un handshake |
161
+ | `method` | `start` | méthode de la requête (`"WEBSOCKET"` pour un handshake) |
162
+ | `scheme` | `start` | `http`, `https`, `ws`, `wss` — tel que vu par le client, proxy de confiance compris |
163
+ | `url` | `start` | URL complète, **chaîne de requête comprise** |
164
+ | `route` | fin | nom de la route qui a répondu, `null` sans route (fichier statique, 404) |
165
+ | `routePath` | fin | gabarit de la route, préfixe compris (`/users/{id}`) ; la racine vaut `/` ; `null` sans route |
166
+ | `module` | fin | module qui a signé la réponse — le même que celui de la ligne d'accès |
167
+ | `statusCode` | fin | statut rendu : celui de l'amont pour une requête relayée, `101` pour un handshake accepté, `null` si le client est parti |
168
+ | `durationMs` | fin | construction du contexte → dernier octet remis (fin du handshake en WebSocket) ; `null` si `timing.enabled: false` |
169
+ | `error` | `error` | l'exception attrapée par le pipeline, `null` sinon |
170
+
171
+ Les champs « fin » valent `null` à `start` et `end` : c'est normal, la route et le statut ne sont
172
+ pas encore connus. On les lit sur `asyncEnd`.
173
+
174
+ ### Correspondance avec OpenTelemetry
175
+
176
+ Pour qui écrit un plugin OTel, la table des attributs du span serveur (conventions sémantiques HTTP) :
177
+
178
+ | Attribut OTel | Champ |
179
+ | ------------------------------ | -------------------------------------------------------------- |
180
+ | `http.request.method` | `method` |
181
+ | `url.scheme` | `scheme` |
182
+ | `url.path` / `url.query` | découpés de `url` par l'abonné (`new URL(trace.url)`) |
183
+ | `http.route` | `routePath` — jamais `route`, qui est un nom et pas un gabarit |
184
+ | `http.response.status_code` | `statusCode` |
185
+ | `error.type` | dérivé de `error` (nom de la classe d'erreur) |
186
+ | `http.server.request.duration` | `durationMs / 1000` (la métrique OTel est en secondes) |
187
+
188
+ Un **4xx sans exception** ne publie pas `error` : pour un span serveur, OTel demande de laisser le
189
+ statut d'un 4xx non renseigné. C'est à l'abonné de classer un statut ; `statusCode` est là pour ça.
190
+
191
+ ## La séquence, sortie par sortie
192
+
193
+ | Situation | Événements publiés | `statusCode` |
194
+ | --------------------------------------- | ----------------------------------------------------- | -------------------- |
195
+ | HTTP servi | `start` · `end` · `asyncStart` · `asyncEnd` | statut de la réponse |
196
+ | HTTP dont le pipeline lève | `start` · `end` · `error` · `asyncStart` · `asyncEnd` | statut de l'erreur |
197
+ | Handshake WebSocket accepté | `start` · `end` · `asyncStart` · `asyncEnd` | `101` |
198
+ | Handshake refusé (origine, pare-feu, …) | `start` · `end` · `error` · `asyncStart` · `asyncEnd` | statut du refus |
199
+ | Client parti pendant la décision | `start` · `end` · `asyncStart` · `asyncEnd` | `null` |
200
+
201
+ La trace d'un WebSocket couvre le **handshake**, pas la vie de la connexion : elle se ferme au
202
+ `101`. Les messages qui suivent ne sont pas publiés sur ce canal.
203
+
204
+ ## Architecture interne
205
+
206
+ ```mermaid
207
+ flowchart TD
208
+ E["contexte construit"] --> G{"quelqu'un écoute ?"}
209
+ G -->|non| P["pipeline, rien d'alloué"]
210
+ G -->|oui| O["message créé, pipeline sous runStores"]
211
+ O --> F["démontage : error, asyncStart, asyncEnd"]
212
+ ```
213
+
214
+ - **Entrée HTTP** — `http-kernel.ts#HttpKernel.handleHttp` : juste après la construction du
215
+ contexte, la garde ; avec abonné, le message est créé (`requestTrace.ts#openRequestTrace`, seule
216
+ allocation du canal) et le pipeline tourne sous `start.runStores`
217
+ (`http-kernel.ts#HttpKernel.dispatchHttpContext`).
218
+ - **Fin HTTP** — `http-kernel.ts#HttpKernel.teardownHttp`, sur l'événement `close` de la réponse,
219
+ APRÈS la ligne d'accès : celle-ci a déjà recopié le statut de l'amont d'une requête relayée
220
+ (`HttpContext.ts#HttpContext.logRequest`).
221
+ - **Handshake WebSocket** — `http-kernel.ts#HttpKernel.handleWebsocket` ouvre la trace ; elle se
222
+ ferme à l'acceptation (`WebsocketContext.ts#WebsocketContext.connect`, au même point que la ligne
223
+ d'accès), au refus (`WebsocketContext.ts#WebsocketContext.refuseHandshake`, seul endroit où le
224
+ statut du refus est connu), ou en échec (`http-kernel.ts#HttpKernel.closeFailedWebsocketTrace`).
225
+ - **Idempotence** — `closeRequestTrace` remet `Context.requestTrace` à `null` avant de publier : un
226
+ handshake a plusieurs sorties, la première ferme la trace, les suivantes ne font rien.
227
+
228
+ ## Configuration
229
+
230
+ Aucune, à dessein. Lire un réglage à chaque requête coûterait autant que lire la garde elle-même,
231
+ pour un résultat identique : sans abonné, rien n'est publié. Pour couper le canal, on ne s'y abonne
232
+ pas — ou on se désabonne (`channel.unsubscribe(subscribers)`).
233
+
234
+ ## Normes appliquées
235
+
236
+ - **Documentation de `node:diagnostics_channel`** — nommage des canaux de trace en
237
+ `tracing:module.function:start` (d'où `nodefony.http.request`) ; et la règle « les événements ne
238
+ sont publiés que si l'abonné était présent **avant le début** de la trace » : un abonné qui arrive
239
+ en cours de requête ne reçoit rien de cette requête, pas même sa fin.
240
+ - **Conventions sémantiques HTTP d'OpenTelemetry** — `http.route` exige un gabarit de faible
241
+ cardinalité, d'où `routePath` ; la durée de `http.server.request.duration` doit égaler celle du
242
+ span serveur, d'où une seule horloge.
243
+
244
+ ## Sécurité
245
+
246
+ - **L'URL complète part chez l'abonné, chaîne de requête comprise** — un jeton passé en `?token=`
247
+ y figure. L'abonné est du code que l'exploitant a choisi d'installer, et les canaux natifs de
248
+ `node:http` exposent déjà la requête entière ; mais un agent qui EXPORTE l'URL doit en retirer les
249
+ paramètres sensibles. Préférer `routePath` pour nommer une opération.
250
+ - **Une exception levée par un abonné fait tomber le process** : Node la relance par
251
+ `process.nextTick` en exception non capturée, hors de la pile de Nodefony. Un abonné se protège
252
+ lui-même (`try`/`catch`).
253
+ - **Un abonné lent ralentit chaque requête** : il est appelé de façon synchrone, sur le fil du
254
+ serveur.
255
+
256
+ ## Performance & mémoire
257
+
258
+ Mesuré au micro-banc (Node 26, médiane de 7 passages) :
259
+
260
+ | Chemin | Coût par requête |
261
+ | ----------------------------- | ------------------------------------------------------------------------------------- |
262
+ | Personne n'écoute (le défaut) | une lecture de `hasSubscribers` (~1,6 ns), un champ `null` sur le contexte (8 octets) |
263
+ | Un abonné | un objet de message, l'appel des abonnés, l'entrée dans les stores liés |
264
+
265
+ `diagnostics_channel.hasSubscribers("nom")` coûterait ~41 ns : la garde lit donc le
266
+ `TracingChannel` gardé en mémoire de module, jamais une recherche par nom. Le gate mémoire
267
+ (`npm run test:memory`) est vert **sans** abonné et **avec** un abonné armé : le message ne retient
268
+ ni contexte ni scope.
269
+
270
+ ## Pièges
271
+
272
+ | Symptôme | Cause | Correction |
273
+ | ----------------------------------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------- |
274
+ | L'abonné ne reçoit rien des premières requêtes | il s'est abonné après leur début — règle des `TracingChannel` | s'abonner AVANT l'application (`--import`) |
275
+ | `route`, `statusCode` à `null` dans le gestionnaire `start` | ils ne sont connus qu'à la fin | les lire dans `asyncEnd` |
276
+ | Le tableau de bord explose en cardinalité | l'opération est nommée d'après `url` (`/users/42`, `/users/43`…) | nommer par `routePath` (`/users/{id}`) |
277
+ | Le serveur tombe sur une exception venue de l'agent | un abonné a levé : Node la relance en exception non capturée | `try`/`catch` dans chaque gestionnaire de l'abonné |
278
+ | Un 404 apparaît en erreur dans l'APM | l'abonné classe sur l'événement `error` sans regarder le statut | classer par `statusCode` ; `error` signale une exception, pas un statut |
279
+ | `durationMs` toujours `null` | `timing.enabled: false` dans la configuration de l'application | rétablir le défaut — la durée est mesurée partout, production comprise |
280
+
281
+ ## Tests & couverture
282
+
283
+ | Type | Où |
284
+ | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
285
+ | Intégration — abonné réel | `integration/request-trace-channel.test.ts` — séquences HTTP servi / en erreur, handshake accepté / refusé, abonné tardif sans fin orpheline, désabonnement, même objet de bout en bout |
286
+ | Mémoire | `http/memory.test.ts` — le gate, rejoué sans abonné (le défaut) et avec la sonde armée |
287
+
288
+ L'abonné vit dans le process du serveur : la sonde `TraceChannelController` du module `test`
289
+ (`/nodefony/test/trace-channel/{arm,disarm,events}`) s'abonne à la demande. Ce qui **manque** : pas
290
+ de banc unitaire isolé de `requestTrace.ts` (exercé de bout en bout par le banc d'intégration), et
291
+ pas de cas « client parti pendant la décision » au banc (sortie couverte par lecture du code).
292
+
293
+ Suites : `npm run test:integration` (serveur requis), `npm run test:memory`. Skills associés :
294
+ `nodefony-check-memory-health`, `nodefony-load-test`.
295
+
296
+ ## Pour aller plus loin
297
+
298
+ - ⬆️ **Retour au hub** : [@nodefony/http — vue du module](index.md) · [Toute la documentation](../../../../../docs/index.md)
299
+ - 🧭 **Pages sœurs** : [Observabilité HTTP](observabilite.md) · [Serveurs](servers.md)
300
+ - La durée d'une requête et sa définition → [Observabilité HTTP](observabilite.md)
301
+ - Le trajet complet d'une requête → [pipeline-requete](../../../../../docs/architecture/pipeline-requete.md)
302
+ - Le proxy inverse (statut de l'amont) → [Proxy inverse](reverse-proxy.md)
303
+ - Les lignes du journal, sur le canal `nodefony.syslog.log` → [Journalisation](../../../../nodefony/docs/syslog.md#observer-le-journal-depuis-un-outil-tiers)
package/docs/index.md CHANGED
@@ -43,7 +43,8 @@ Trois parcours selon ce que tu viens faire. L'ordre compte : chaque étape suppo
43
43
  3. [Pipeline de requête](../../../../../docs/architecture/pipeline-requete.md) — où se branchent
44
44
  rate-limit, en-têtes et firewall.
45
45
  4. [Rate-limit](rate-limit.md) — plafonner le débit par client avant que la charge n'atteigne le contrôleur.
46
- 5. [Observabilité](observabilite.md) — corréler les logs par `requestId` pour diagnostiquer à chaud.
46
+ 5. [Observabilité](observabilite.md) — corréler les logs par `requestId` pour diagnostiquer à chaud ;
47
+ [Canal de diagnostic](diagnostics-channel.md) pour brancher un outil de traçage.
47
48
  6. [Sécurité](../../security/docs/index.md) — le pare-feu applicatif se pose par-dessus ce module.
48
49
 
49
50
  **Je fais du temps réel** — WebSocket dans le même contexte que le web.
@@ -66,6 +67,7 @@ Le tableau pour choisir vite ; les cards en dessous pour savoir ce qu'on y trouv
66
67
  | [Upload & corps](upload.md) | parser le corps et recevoir des fichiers | formulaires, imports multipart, API JSON |
67
68
  | [Rate-limit](rate-limit.md) | plafonner le débit par client (429) | protéger une API d'un flood ou d'un abus |
68
69
  | [Observabilité](observabilite.md) | tracer et journaliser chaque requête | débugger en prod, corréler des logs |
70
+ | [Canal de diagnostic](diagnostics-channel.md) | publier le cycle de chaque requête à un outil de traçage, sans monkey-patch | brancher OpenTelemetry, Datadog ou Sentry |
69
71
  | [Proxy inverse](reverse-proxy.md) | relayer un préfixe d'URL vers un autre serveur, sur la même origine | servir Vite en dev, adosser un service interne sans CORS |
70
72
  | [Pipeline de requête](../../../../../docs/architecture/pipeline-requete.md) | l'ordre exact des étapes, HTTP comme WS | tu débugges « pourquoi ça passe / ça bloque ici » |
71
73
 
@@ -89,6 +91,9 @@ Le tableau pour choisir vite ; les cards en dessous pour savoir ce qu'on y trouv
89
91
  { "icon": "📈", "title": "observabilite", "href": "observabilite.md",
90
92
  "desc": "Observer les requêtes : lignes de log (pretty ou JSON), requestId de corrélation, W3C Trace Context, trace des frames WebSocket, redaction et sampling d'audit. Où partent les logs est traité par la page Syslog du cœur.",
91
93
  "meta": "débugger en prod, corréler les logs par requête" },
94
+ { "icon": "📡", "title": "diagnostics-channel", "href": "diagnostics-channel.md",
95
+ "desc": "Le TracingChannel nodefony.http.request : chaque requête HTTP et chaque handshake WebSocket publiés avec route, gabarit, module, requestId, statut réel et durée. Zéro coût sans abonné ; correspondance avec les attributs OpenTelemetry.",
96
+ "meta": "brancher un APM sans patcher le framework" },
92
97
  { "icon": "↪️", "title": "reverse-proxy", "href": "reverse-proxy.md",
93
98
  "desc": "Relayer un préfixe d'URL vers un autre serveur sans quitter l'origine de la page : HTTP et WebSocket, en-têtes de connexion jamais transmis, X-Forwarded-* et Via posés, barrière d'hôte appliquée avant le relais. C'est par lui que Vite est servi en développement.",
94
99
  "meta": "une origine, un certificat — pas de CORS ni de contenu mixte" },
@@ -265,6 +265,58 @@ de bilan, au teardown, via `Context.logRequest()` (`Context.ts#Context.logReques
265
265
  - `shouldSample?(context, error?)` — **portillon** évalué AVANT le rendu : `false` = ligne sautée sans
266
266
  aucune allocation ni `JSON.stringify` (levier perf du logger d'audit).
267
267
 
268
+ ### La durée d'une requête — une horloge, mesurée partout
269
+
270
+ `durationMs` est **une seule valeur**, que lisent la ligne d'accès JSON, la ligne lisible du
271
+ développement, le profil du Studio et la trace verbose. Elle vient d'une horloge propre à chaque
272
+ requête (`Context.ts#Context.stopClock`) :
273
+
274
+ - **elle démarre** à la construction du contexte, dès que la requête atteint le noyau ;
275
+ - **elle s'arrête** au démontage, sur l'événement `close` de la réponse : le dernier octet vient
276
+ d'être remis au système, ou le client est parti (`http-kernel.ts#HttpKernel.teardownHttp`).
277
+
278
+ Aucun lecteur ne relit l'horloge pour son compte : le journal et le Studio affichent la même durée
279
+ pour la même requête.
280
+
281
+ | Compté dans la durée | Hors de la durée |
282
+ | ------------------------------------------------------ | -------------------------------------------------------------------------- |
283
+ | routage, pare-feu, session, action, rendu | l'acceptation TCP et la négociation TLS |
284
+ | l'écriture de la réponse, jusqu'au dernier octet remis | la lecture des en-têtes par Node, avant que la requête n'atteigne le noyau |
285
+ | l'attente d'un client lent qui lit la réponse | ce qui suit le `close` : hooks `onAfterResponse` et `onFinish` |
286
+
287
+ > [!NOTE]
288
+ > **Pourquoi jusqu'au dernier octet, et pas jusqu'à la fin de l'action ?** Parce que c'est la
289
+ > définition des outils qui liront cette ligne. OpenTelemetry ferme le span serveur sur `close`, et
290
+ > la métrique `http.server.request.duration` doit valoir la durée de ce span. nginx mesure
291
+ > `$request_time` jusqu'à l'écriture du journal, une fois les derniers octets envoyés. Un p99 de
292
+ > l'application se compare ainsi directement à celui du proxy placé devant elle. Le travail du
293
+ > serveur seul, hors réseau, se lit dans les **phases**.
294
+
295
+ Trois cas à connaître :
296
+
297
+ - **Un flux** (SSE, téléchargement long) dure ce que dure le flux, comme chez nginx.
298
+ - **Un client parti avant la fin** (statut `499`) : la durée court jusqu'à son départ.
299
+ - **WebSocket** : la ligne d'accès d'un handshake porte la durée du handshake, pas celle de la
300
+ connexion. Chaque invocation du pont RPC a sa propre horloge, dans son profil
301
+ (`FrameProfile.ts#FrameProfile.stopClock`).
302
+
303
+ #### Deux niveaux de mesure : la durée, puis les phases
304
+
305
+ | `timing.enabled` | Durée (`durationMs`) | Phases (waterfall) | Pour quoi |
306
+ | ---------------- | ---------------------------- | ------------------ | ------------------------------------------------------- |
307
+ | `null` (défaut) | partout, production comprise | hors production | le cas courant |
308
+ | `true` | partout | partout | un diagnostic à chaud en production |
309
+ | `false` | nulle part | nulle part | un banc qui veut le coût du pipeline sans aucune mesure |
310
+
311
+ La règle a une seule implémentation (`timingMode.ts#resolveTimingMode`). La durée coûte deux lectures
312
+ d'horloge par requête ; les phases, une lecture par borne, un tableau et un index par requête. C'est
313
+ pourquoi seules les phases restent coupées en production.
314
+
315
+ La trace verbose (`timing.verbose`) nomme les deux grandeurs côte à côte :
316
+ `TRACE phases [total 15.25ms · Σ 12.80ms] parse=0.50ms · action=12.30ms`. Le total est la durée de la
317
+ requête, Σ la somme des phases : l'écart montre le temps passé hors de toute phase, envoi au client
318
+ compris.
319
+
268
320
  ### La trace des frames WebSocket
269
321
 
270
322
  Chaque frame WS (RECEIVE / SEND / BROADCAST) peut être tracée pour le Suivi de requête (Studio). Le
@@ -281,11 +333,16 @@ chaîne quand l'env est `production` (`WebsocketContext.ts#WebsocketContext.broa
281
333
 
282
334
  ## ⚙️ Configuration
283
335
 
284
- Un seul réglage d'app, côté cœur (bloc `log`), pilote le format des lignes.
336
+ Deux réglages d'app, côté cœur : le format des lignes (bloc `log`) et ce que le chronométrage mesure
337
+ (bloc `timing`).
285
338
 
286
- | Option | Type | Défaut | Effet |
287
- | ------------------- | ----------------------------------------- | ------ | ---------------------------------------------------------------------------------------------- |
288
- | `log.requestFormat` | `auto` \| `default` \| `pretty` \| `json` | `auto` | Format de la ligne de bilan. `auto` = pretty en dev / json en prod (`schema.ts#serverSchema`). |
339
+ | Option | Type | Défaut | Effet |
340
+ | ------------------- | ----------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
341
+ | `log.requestFormat` | `auto` \| `default` \| `pretty` \| `json` | `auto` | Format de la ligne de bilan. `auto` = pretty en dev / json en prod (`schema.ts#serverSchema`). |
342
+ | `timing.enabled` | `boolean` \| `null` | `null` | `null` : durée partout, phases hors production. `true` : phases aussi en production. `false` : ni durée ni phases. Surchargeable sans redéployer : `NF__APP__TIMING__ENABLED`. |
343
+
344
+ Une clé inconnue sous `timing` (`timing: { enable: false }`) arrête le démarrage en la nommant : retirée
345
+ en silence, elle laisserait l'horloge allumée chez qui croit l'avoir coupée.
289
346
 
290
347
  `auto` est résolu au boot selon l'environnement (`applyRequestLoggerFromConfig`, `http-kernel.ts#HttpKernel.applyRequestLoggerFromConfig`) :
291
348
 
@@ -315,8 +372,9 @@ Choisir en cinq secondes :
315
372
 
316
373
  Le plus grand gain en dev : `méthode statut url durée remote [id]`, avec couleur du statut (vert 2xx,
317
374
  jaune 4xx, rouge 5xx — `colorizeStatus()`, `pretty-request-logger.ts#colorizeStatus`) et `requestId` tronqué aux 8
318
- premiers caractères (`shortId()`, `pretty-request-logger.ts#shortId`). La durée est dérivée des phases de
319
- timing (`pretty-request-logger.ts#shortId`).
375
+ premiers caractères (`shortId()`, `pretty-request-logger.ts#shortId`). La durée est celle de l'horloge de
376
+ la requête, la même que dans la ligne JSON (`pretty-request-logger.ts#formatDuration`) ; `---` quand
377
+ `timing.enabled` vaut `false`.
320
378
 
321
379
  ### `JsonAuditLogger` — un PDU par requête, pour la machine
322
380
 
@@ -328,6 +386,16 @@ timing (`pretty-request-logger.ts#shortId`).
328
386
  drapeaux `hasAuthorization`/`hasCookie` le sont (`audit-logger.ts#AuditLogEntry.hasCookie`).
329
387
  - **Sampling déterministe** : `shouldSample()` (`audit-logger.ts#JsonAuditLogger.shouldSample`) garde 1 requête nominale sur N mais
330
388
  **jamais** une erreur ni un `status >= 400` — on ne perd aucun échec ; compteur, pas de RNG.
389
+ - **Durée présente en production** : `durationMs` est la durée de la requête, de son arrivée au
390
+ dernier octet de la réponse, en millisecondes. C'est la même valeur que dans le Studio (voir
391
+ [La durée d'une requête](#la-durée-dune-requête--une-horloge-mesurée-partout)). Le détail par phase
392
+ (`phases`) reste réservé au développement, ou à `timing.enabled: true`.
393
+
394
+ Destinée à une machine, cette ligne se lit dans un outil d'observabilité : une application générée
395
+ livre deux **tableaux de bord d'exemple** (Grafana sur Loki, OpenSearch Dashboards) sous
396
+ `docker/examples/dashboards/`, à importer. Côté OpenSearch, le transport range la ligne sous
397
+ `http.*` (statut, famille, chemin, durée) par un pipeline d'ingestion — la page Syslog du cœur le
398
+ détaille.
331
399
 
332
400
  ### `DefaultRequestLogger` — le format legacy
333
401
 
@@ -371,6 +439,11 @@ Les choix visibles dans le code :
371
439
  `toISOString`, 0 `JSON.stringify`, 0 `Pdu` au ring (`audit-logger.ts#JsonAuditLogger.shouldSample`).
372
440
  - **Audit nominal coupable** — l'option `nominal` coupe le log des 2xx/3xx si le sink texte est `null`
373
441
  (l'entrée n'atteindrait aucune destination) — ~5,9 % du profil CPU récupérés (`audit-logger.ts#JsonAuditLoggerOptions.sampleRate`).
442
+ - **Durée : deux lectures d'horloge, rien d'autre** — `performance.now()` à l'entrée et au `close`,
443
+ quelques dizaines de nanosecondes chacune, deux nombres sur le contexte, aucune structure allouée
444
+ (`Context.ts#Context.stopClock`). C'est un ordre de grandeur de moins que la date et la sérialisation
445
+ JSON de la ligne d'accès qu'elle complète, et c'est trop petit pour qu'un banc de débit le distingue
446
+ de son bruit. Les phases, elles, restent coupées en production.
374
447
  - **Trace des frames WS gatée en prod** — `logMessageContent()` court-circuite avant toute construction
375
448
  de chaîne hors dev (`WebsocketContext.ts#WebsocketContext.logMessageContent`) ; les events lifecycle ne créent aucun `Pdu` en production, et ne sont que DEBUG hors production (`Context.ts#lifecycleEventLogging`).
376
449
 
@@ -408,6 +481,7 @@ instancié **qu'en dev** (fuite d'info + coût en prod).
408
481
  | En-têtes trop volumineux / borne | anti-abus (log flooding) | `MAX_REQUEST_ID_LENGTH` (`requestId.ts#MAX_REQUEST_ID_LENGTH`) |
409
482
  | Log structuré (PDU, sévérités) | RFC 5424 | `Pdu` + `requestId`/`pid` (`Pdu.ts#Pdu.requestId`) |
410
483
  | Sévérité dérivée du statut HTTP | RFC 9110 (catégories) | `severityFromStatus()` (`audit-logger.ts#severityFromStatus`) |
484
+ | Durée d'une requête serveur | OpenTelemetry, conventions sémantiques HTTP (`http.server.request.duration`, stable) | fin sur le `close` de la réponse (`Context.ts#Context.stopClock`) |
411
485
  | Non-journalisation des secrets | OWASP (logging) | redaction présence-only (`audit-logger.ts#JsonAuditLogger.renderHttp`) |
412
486
 
413
487
  > `X-Request-Id` n'est **pas** un en-tête normalisé (convention de-facto) ; c'est la **valeur** qu'il
@@ -415,16 +489,19 @@ instancié **qu'en dev** (fuite d'info + coût en prod).
415
489
 
416
490
  ## ⚠️ Pièges (symptôme → cause → correction)
417
491
 
418
- | Symptôme | Cause | Correction |
419
- | --------------------------------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
420
- | Le `X-Request-Id` que j'envoie n'est pas réfléchi | Valeur non conforme (espace, CR/LF, non-ASCII, > 128) → **rejetée** | Utiliser `[A-Za-z0-9._-]{1,128}` (UUID/nanoid/traceparent OK) — sinon UUID serveur |
421
- | Les logs de fin de requête n'ont pas de `requestId` | Ils sont émis hors bulle ALS | Déjà géré : l'override `log()` rouvre une micro-bulle (`Context.ts#Context.log`) |
422
- | Réponse HTTP/2 sans `x-request-id` | Chemin de réponse h2 distinct du 1.1 | Déjà géré (`http2/Response.ts#Http2Response.writeHead`) — le port 5152 réfléchit aussi |
423
- | Pas de `traceparent` renvoyé sur un WebSocket | `ws` n'expose pas l'écriture d'en-tête au handshake | Attendu — la trace WS reste propagée en ALS (`http-kernel.ts#HttpKernel.handleWebsocket`) |
424
- | Frame WS binaire loggée en `{"0":..,"1":..}` | Sérialisation naïve d'un Buffer | Déjà géré : résumé `[binary N B]` (`wsLogContent.ts#formatWsLogContent`) |
425
- | Le format de log ne change pas malgré la config | Un `setRequestLogger(...)` programmatique gagne sur la config | L'override est volontaire (last setter wins) — retirer l'appel, ou le régler |
426
- | Logs d'audit trop volumineux en prod | `stack` sérialisée, ou 100 % des 2xx audités | `includeStack:false` (défaut prod) + `sampleRate` via `setRequestLogger` |
427
- | `Authorization`/`Cookie` attendus dans le log JSON | Redaction : jamais sérialisés | Par conception — lire les drapeaux `hasAuthorization`/`hasCookie` |
492
+ | Symptôme | Cause | Correction |
493
+ | --------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
494
+ | Le `X-Request-Id` que j'envoie n'est pas réfléchi | Valeur non conforme (espace, CR/LF, non-ASCII, > 128) → **rejetée** | Utiliser `[A-Za-z0-9._-]{1,128}` (UUID/nanoid/traceparent OK) — sinon UUID serveur |
495
+ | Les logs de fin de requête n'ont pas de `requestId` | Ils sont émis hors bulle ALS | Déjà géré : l'override `log()` rouvre une micro-bulle (`Context.ts#Context.log`) |
496
+ | Réponse HTTP/2 sans `x-request-id` | Chemin de réponse h2 distinct du 1.1 | Déjà géré (`http2/Response.ts#Http2Response.writeHead`) — le port 5152 réfléchit aussi |
497
+ | `durationMs: null` dans chaque ligne d'accès | L'application a posé `timing.enabled: false` (ou `NF__APP__TIMING__ENABLED=false`) | Retirer ce réglage : la durée est mesurée par défaut, production comprise |
498
+ | La durée dépasse largement la somme des phases | Temps passé hors de toute phase : envoi à un client lent, flux encore ouvert | Attendu — la durée va jusqu'au dernier octet remis ; le travail du serveur se lit dans les phases |
499
+ | `phases` absent des lignes de production | Les phases sont réservées au développement (coût par borne) | `NF__APP__TIMING__ENABLED=true` le temps d'un diagnostic, puis le retirer |
500
+ | Pas de `traceparent` renvoyé sur un WebSocket | `ws` n'expose pas l'écriture d'en-tête au handshake | Attendu — la trace WS reste propagée en ALS (`http-kernel.ts#HttpKernel.handleWebsocket`) |
501
+ | Frame WS binaire loggée en `{"0":..,"1":..}` | Sérialisation naïve d'un Buffer | Déjà géré : résumé `[binary N B]` (`wsLogContent.ts#formatWsLogContent`) |
502
+ | Le format de log ne change pas malgré la config | Un `setRequestLogger(...)` programmatique gagne sur la config | L'override est volontaire (last setter wins) — retirer l'appel, ou le régler |
503
+ | Logs d'audit trop volumineux en prod | `stack` sérialisée, ou 100 % des 2xx audités | `includeStack:false` (défaut prod) + `sampleRate` via `setRequestLogger` |
504
+ | `Authorization`/`Cookie` attendus dans le log JSON | Redaction : jamais sérialisés | Par conception — lire les drapeaux `hasAuthorization`/`hasCookie` |
428
505
 
429
506
  ## 🧪 Tests & couverture
430
507
 
@@ -453,6 +530,7 @@ Skills associés : `nodefony-check-memory-health`, `nodefony-load-test`, `nodefo
453
530
 
454
531
  - ⬆️ **Retour au hub** : [@nodefony/http — vue du module](index.md) · [Toute la documentation](../../../../../docs/index.md)
455
532
  - 🧭 **Pages sœurs** : [Serveurs](servers.md) · [Sessions](session.md)
533
+ - Brancher un outil de traçage (OpenTelemetry, Datadog) sur le cycle de chaque requête → [Canal de diagnostic](diagnostics-channel.md)
456
534
  - Où partent les logs (stdout, fichier, Loki, OpenSearch) + format RFC 5424 → [Journalisation (Syslog)](../../../../nodefony/docs/syslog.md)
457
535
  - Le trajet complet d'une requête (où s'insèrent corrélation et trace) → [pipeline-requete](../../../../../docs/architecture/pipeline-requete.md)
458
536
  - Sondes de santé et arrêt gracieux → [Serveurs](servers.md)
@@ -228,16 +228,17 @@ démarrage en la nommant (`proxySchema`, `config.ts#proxySchema`).
228
228
 
229
229
  Chaque montage (`proxyMountSchema`, `config.ts#proxyMountSchema`) :
230
230
 
231
- | Option | Type | Défaut | Effet |
232
- | -------------- | ---------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------- |
233
- | `target` | `string` | — (requis) | Origine de l'amont, `http(s)://hôte[:port]`, **sans chemin**. |
234
- | `methods` | `string[]` | toutes | Méthodes relayées ; une autre méthode suit le routage de l'application. |
235
- | `websocket` | `boolean` | `false` | Relaie aussi l'upgrade WebSocket du préfixe. |
236
- | `stripPrefix` | `boolean` | `false` | Retire le préfixe du chemin transmis ; `Location` est alors ramené sous le préfixe. |
237
- | `preserveHost` | `boolean` | `false` | Transmet le `Host` du client au lieu de celui de la cible. |
238
- | `stripHeaders` | `string[]` | `[]` | En-têtes de requête à ne pas transmettre (`cookie`, `authorization`…). |
239
- | `timeoutMs` | entier | `proxy.timeoutMs` | Délai d'inactivité propre au montage, handshake WebSocket compris (jusqu'au `101`). Ne s'applique pas à un WebSocket établi. |
240
- | `secure` | `boolean` | `true` | Vérifie le certificat d'une cible `https`. `false` n'est accepté que vers la boucle locale. |
231
+ | Option | Type | Défaut | Effet |
232
+ | -------------- | --------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------- |
233
+ | `target` | `string` | — (requis) | Origine de l'amont, `http(s)://hôte[:port]`, **sans chemin**. |
234
+ | `methods` | `string[]` | toutes | Méthodes relayées ; une autre méthode suit le routage de l'application. |
235
+ | `websocket` | `boolean` | `false` | Relaie aussi l'upgrade WebSocket du préfixe. |
236
+ | `stripPrefix` | `boolean` | `false` | Retire le préfixe du chemin transmis ; `Location` est alors ramené sous le préfixe. |
237
+ | `preserveHost` | `boolean` | `false` | Transmet le `Host` du client au lieu de celui de la cible. |
238
+ | `stripHeaders` | `string[]` | `[]` | En-têtes de requête à ne pas transmettre (`cookie`, `authorization`…). |
239
+ | `timeoutMs` | entier | `proxy.timeoutMs` | Délai d'inactivité propre au montage, handshake WebSocket compris (jusqu'au `101`). Ne s'applique pas à un WebSocket établi. |
240
+ | `secure` | `boolean` | `true` | Vérifie le certificat d'une cible `https`. `false` n'est accepté que vers la boucle locale. |
241
+ | `accessLog` | `"all"` \| `"errors"` | `"all"` | Ligne d'accès des requêtes relayées : toutes, ou seulement celles en échec (statut ≥ 400), avec le statut rendu par l'amont. |
241
242
 
242
243
  **Refusés au démarrage**, avec le motif (`proxyMountProblems()`, `rules.ts#proxyMountProblems`) :
243
244
 
@@ -295,12 +296,13 @@ modules, ses images et le WebSocket du rechargement à chaud partagent alors l'o
295
296
  l'application — une IP de réseau local, un téléphone ou un conteneur fonctionnent en HTTPS sans
296
297
  réglage.
297
298
 
298
- | Ce qui est fixé par `@nodefony/frontend` | Pourquoi |
299
- | ----------------------------------------------- | ---------------------------------------------------------- |
300
- | `methods: ["GET", "HEAD"]` | un serveur de sources ne sert que des lectures |
301
- | `websocket: true` | le rechargement à chaud passe par un WebSocket |
302
- | `stripHeaders: ["cookie", "authorization"]` | Vite n'a pas à voir la session de l'application |
303
- | cible = le Vite de CETTE application, s'il sert | un port repris par une autre application n'est jamais visé |
299
+ | Ce qui est fixé par `@nodefony/frontend` | Pourquoi |
300
+ | ----------------------------------------------- | ----------------------------------------------------------------------------------------- |
301
+ | `methods: ["GET", "HEAD"]` | un serveur de sources ne sert que des lectures |
302
+ | `websocket: true` | le rechargement à chaud passe par un WebSocket |
303
+ | `stripHeaders: ["cookie", "authorization"]` | Vite n'a pas à voir la session de l'application |
304
+ | `accessLog: "errors"` | une page charge des centaines de modules ; seuls les échecs méritent une ligne au journal |
305
+ | cible = le Vite de CETTE application, s'il sert | un port repris par une autre application n'est jamais visé |
304
306
 
305
307
  Ce qui se règle reste côté frontend, dans `use("@nodefony/frontend", { … })` : `devHost` (défaut
306
308
  `127.0.0.1`, à garder — le navigateur ne joint jamais Vite directement) et `devPort` (défaut `5173`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodefony/http",
3
- "version": "10.0.0-beta.4",
3
+ "version": "10.0.0-beta.5",
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",
@@ -72,7 +72,7 @@
72
72
  "@types/ws": "8.18.2",
73
73
  "@types/xml2js": "0.4.14",
74
74
  "@vitest/coverage-v8": "5.0.5",
75
- "nodefony": "^10.0.0-beta.4",
75
+ "nodefony": "^10.0.0-beta.5",
76
76
  "tsx": "4.23.15",
77
77
  "vitest": "5.0.5"
78
78
  },
@@ -80,7 +80,7 @@
80
80
  "readmeFilename": "README.md",
81
81
  "contributors": [],
82
82
  "peerDependencies": {
83
- "nodefony": "^10.0.0-beta.4",
83
+ "nodefony": "^10.0.0-beta.5",
84
84
  "zod": "^4.6.7"
85
85
  },
86
86
  "files": [