@nodefony/http 10.0.0-beta.4 → 10.0.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +29 -25
- package/dist/nodefony/command/proxyGenerateCommand.js +2 -2
- package/dist/nodefony/config/config.js +6 -3
- package/dist/nodefony/interfaces/IRequestTrace.js +1 -0
- package/dist/nodefony/service/ProfilerAdminApi.js +17 -3
- package/dist/nodefony/service/audit-logger.js +3 -17
- package/dist/nodefony/service/error-renderer.js +6 -4
- package/dist/nodefony/service/http-kernel.js +191 -40
- package/dist/nodefony/service/pretty-request-logger.js +11 -10
- package/dist/nodefony/service/request-logger.js +1 -1
- package/dist/nodefony/service/reverse-proxy.js +7 -2
- package/dist/nodefony/service/servers/clientError.js +28 -4
- package/dist/nodefony/service/servers/server-h2c.js +5 -5
- package/dist/nodefony/service/servers/server-http.js +5 -3
- package/dist/nodefony/service/servers/server-https.js +12 -5
- package/dist/nodefony/service/servers/server-static.js +1 -6
- package/dist/nodefony/service/servers/server-websocket-secure.js +6 -10
- package/dist/nodefony/service/servers/server-websocket.js +6 -10
- package/dist/nodefony/service/servers/serverShutdown.js +63 -4
- package/dist/nodefony/service/sessions/sessions-service.js +3 -3
- package/dist/nodefony/service/upload/upload-service.js +2 -2
- package/dist/nodefony/src/assets/prebuiltUi.js +23 -5
- package/dist/nodefony/src/context/Context.js +56 -13
- package/dist/nodefony/src/context/http/ByteStream.js +359 -0
- package/dist/nodefony/src/context/http/HttpContext.js +75 -25
- package/dist/nodefony/src/context/http/Request.js +15 -6
- package/dist/nodefony/src/context/http/Response.js +354 -35
- package/dist/nodefony/src/context/http/SseStream.js +81 -146
- package/dist/nodefony/src/context/http/StreamLifecycle.js +273 -0
- package/dist/nodefony/src/context/http/urlFastPath.js +80 -1
- package/dist/nodefony/src/context/http2/Request.js +1 -1
- package/dist/nodefony/src/context/http2/Response.js +28 -5
- package/dist/nodefony/src/context/requestTrace.js +104 -0
- package/dist/nodefony/src/context/timingMode.js +21 -0
- package/dist/nodefony/src/context/transport.js +20 -1
- package/dist/nodefony/src/context/unsentResponse.js +30 -0
- package/dist/nodefony/src/context/websocket/Response.js +4 -2
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +81 -20
- package/dist/nodefony/src/context/websocket/websocketUrl.js +7 -0
- package/dist/nodefony/src/context/websocket/wsBackpressure.js +15 -15
- package/dist/nodefony/src/context/websocket/wsFrameRate.js +71 -0
- package/dist/nodefony/src/context/websocket/wsLogContent.js +55 -17
- package/dist/nodefony/src/cookies/cookie.js +54 -13
- package/dist/nodefony/src/profiler/FrameProfile.js +22 -0
- package/dist/nodefony/src/profiler/Profiler.js +25 -22
- package/dist/nodefony/src/profiler/aggregate.js +26 -6
- package/dist/nodefony/src/proxy/forward.js +109 -9
- package/dist/nodefony/src/proxy/generateProxyConfig.js +6 -2
- package/dist/nodefony/src/proxy/rules.js +29 -14
- package/dist/nodefony/src/servers/upgradeDispatch.js +62 -4
- package/dist/types/index.d.ts +8 -0
- package/dist/types/nodefony/config/config.d.ts +8 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +9 -0
- 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 +16 -0
- package/dist/types/nodefony/interfaces/ISecurity.d.ts +27 -2
- package/dist/types/nodefony/interfaces/index.d.ts +1 -0
- package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +5 -1
- package/dist/types/nodefony/service/audit-logger.d.ts +3 -11
- package/dist/types/nodefony/service/error-renderer.d.ts +5 -4
- package/dist/types/nodefony/service/http-kernel.d.ts +65 -2
- package/dist/types/nodefony/service/servers/clientError.d.ts +3 -2
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +5 -3
- package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +9 -4
- package/dist/types/nodefony/src/context/Context.d.ts +25 -1
- package/dist/types/nodefony/src/context/http/ByteStream.d.ts +109 -0
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +60 -4
- package/dist/types/nodefony/src/context/http/Request.d.ts +8 -1
- package/dist/types/nodefony/src/context/http/Response.d.ts +134 -8
- package/dist/types/nodefony/src/context/http/SseStream.d.ts +32 -7
- package/dist/types/nodefony/src/context/http/StreamLifecycle.d.ts +53 -0
- package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +40 -0
- package/dist/types/nodefony/src/context/http2/Response.d.ts +17 -3
- 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 +17 -0
- package/dist/types/nodefony/src/context/unsentResponse.d.ts +16 -0
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +41 -1
- package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +4 -0
- package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +12 -2
- package/dist/types/nodefony/src/context/websocket/wsFrameRate.d.ts +69 -0
- package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +16 -8
- package/dist/types/nodefony/src/cookies/cookie.d.ts +20 -2
- package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +15 -0
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +27 -102
- package/dist/types/nodefony/src/profiler/aggregate.d.ts +23 -106
- package/dist/types/nodefony/src/proxy/forward.d.ts +30 -2
- package/dist/types/nodefony/src/proxy/rules.d.ts +20 -0
- package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +41 -1
- package/docs/byte-stream.md +371 -0
- package/docs/cookies.md +13 -3
- package/docs/diagnostics-channel.md +303 -0
- package/docs/index.md +14 -1
- package/docs/observabilite.md +94 -16
- package/docs/reverse-proxy.md +33 -19
- package/docs/servers.md +26 -15
- package/docs/session.md +3 -3
- package/docs/sse.md +27 -20
- package/package.json +16 -16
|
@@ -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,9 @@ 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 |
|
|
71
|
+
| [Flux SSE](sse.md) | pousser des événements texte au navigateur sur une réponse qui ne finit pas | progression d'une tâche, réponse d'un modèle mot à mot, journal en direct |
|
|
72
|
+
| [Flux d'octets](byte-stream.md) | servir la sortie d'un processus, un relais `fetch` ou une archive à la volée | ffmpeg vers un `<video>`, caméra relayée, export `tar` |
|
|
69
73
|
| [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
74
|
| [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
75
|
|
|
@@ -89,6 +93,15 @@ Le tableau pour choisir vite ; les cards en dessous pour savoir ce qu'on y trouv
|
|
|
89
93
|
{ "icon": "📈", "title": "observabilite", "href": "observabilite.md",
|
|
90
94
|
"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
95
|
"meta": "débugger en prod, corréler les logs par requête" },
|
|
96
|
+
{ "icon": "📡", "title": "diagnostics-channel", "href": "diagnostics-channel.md",
|
|
97
|
+
"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.",
|
|
98
|
+
"meta": "brancher un APM sans patcher le framework" },
|
|
99
|
+
{ "icon": "📻", "title": "sse", "href": "sse.md",
|
|
100
|
+
"desc": "Le flux text/event-stream : this.renderSse(), send/comment/retry/close, négociation JSON ou flux sur la même route, client NodefonySse, et les gardes d'une connexion longue (plafond par IP, révocation, lecteur bloqué).",
|
|
101
|
+
"meta": "le serveur parle, le navigateur écoute" },
|
|
102
|
+
{ "icon": "🚰", "title": "byte-stream", "href": "byte-stream.md",
|
|
103
|
+
"desc": "Répondre par une source d'octets qui n'est pas un fichier : this.renderStream() sur un Readable ou un fetch().body, en-têtes au premier octet (une erreur précoce reste une vraie page 5xx), source détruite à la fin, chien de garde d'une source muette, mêmes gardes que le SSE.",
|
|
104
|
+
"meta": "un robinet, pas une bouteille" },
|
|
92
105
|
{ "icon": "↪️", "title": "reverse-proxy", "href": "reverse-proxy.md",
|
|
93
106
|
"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
107
|
"meta": "une origine, un certificat — pas de CORS ni de contenu mixte" },
|
package/docs/observabilite.md
CHANGED
|
@@ -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
|
-
|
|
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
|
|
319
|
-
|
|
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
|
|
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**
|
|
421
|
-
| Les logs de fin de requête n'ont pas de `requestId` | Ils sont émis hors bulle ALS
|
|
422
|
-
| Réponse HTTP/2 sans `x-request-id` | Chemin de réponse h2 distinct du 1.1
|
|
423
|
-
|
|
|
424
|
-
|
|
|
425
|
-
|
|
|
426
|
-
|
|
|
427
|
-
|
|
|
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)
|
package/docs/reverse-proxy.md
CHANGED
|
@@ -140,8 +140,11 @@ Ce qui s'applique malgré tout, parce que posé à l'entrée, avant l'aiguillage
|
|
|
140
140
|
- les **quotas** : le rate-limit par IP compte les requêtes relayées, et un upgrade WebSocket relayé
|
|
141
141
|
obéit aux mêmes bornes qu'un upgrade servi (voir [WebSocket relayé](#-websocket-relayé)).
|
|
142
142
|
|
|
143
|
-
La CSP à nonce de l'application
|
|
144
|
-
serveur, dont les scripts ne portent pas ce nonce.
|
|
143
|
+
La CSP à nonce de l'application n'est jamais imposée à un préfixe relayé : elle casserait les
|
|
144
|
+
pages d'un autre serveur, dont les scripts ne portent pas ce nonce. Dans l'autre sens,
|
|
145
|
+
`proxy:generate` ne pose **aucune** CSP dans la configuration nginx ou haproxy qu'il rend. Écrire
|
|
146
|
+
la politique au frontal plutôt que dans l'application →
|
|
147
|
+
[Poser la CSP au proxy](../../security/docs/headers.md#poser-la-csp-au-proxy-nginx-ingress).
|
|
145
148
|
|
|
146
149
|
## 🚀 Démarrage rapide
|
|
147
150
|
|
|
@@ -228,16 +231,17 @@ démarrage en la nommant (`proxySchema`, `config.ts#proxySchema`).
|
|
|
228
231
|
|
|
229
232
|
Chaque montage (`proxyMountSchema`, `config.ts#proxyMountSchema`) :
|
|
230
233
|
|
|
231
|
-
| Option | Type
|
|
232
|
-
| -------------- |
|
|
233
|
-
| `target` | `string`
|
|
234
|
-
| `methods` | `string[]`
|
|
235
|
-
| `websocket` | `boolean`
|
|
236
|
-
| `stripPrefix` | `boolean`
|
|
237
|
-
| `preserveHost` | `boolean`
|
|
238
|
-
| `stripHeaders` | `string[]`
|
|
239
|
-
| `timeoutMs` | entier
|
|
240
|
-
| `secure` | `boolean`
|
|
234
|
+
| Option | Type | Défaut | Effet |
|
|
235
|
+
| -------------- | --------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
236
|
+
| `target` | `string` | — (requis) | Origine de l'amont, `http(s)://hôte[:port]`, **sans chemin**. |
|
|
237
|
+
| `methods` | `string[]` | toutes | Méthodes relayées ; une autre méthode suit le routage de l'application. |
|
|
238
|
+
| `websocket` | `boolean` | `false` | Relaie aussi l'upgrade WebSocket du préfixe. |
|
|
239
|
+
| `stripPrefix` | `boolean` | `false` | Retire le préfixe du chemin transmis ; `Location` est alors ramené sous le préfixe. |
|
|
240
|
+
| `preserveHost` | `boolean` | `false` | Transmet le `Host` du client au lieu de celui de la cible. |
|
|
241
|
+
| `stripHeaders` | `string[]` | `[]` | En-têtes de requête à ne pas transmettre (`cookie`, `authorization`…). |
|
|
242
|
+
| `timeoutMs` | entier | `proxy.timeoutMs` | Délai d'inactivité propre au montage, handshake WebSocket compris (jusqu'au `101`). Ne s'applique pas à un WebSocket établi. |
|
|
243
|
+
| `secure` | `boolean` | `true` | Vérifie le certificat d'une cible `https`. `false` n'est accepté que vers la boucle locale. |
|
|
244
|
+
| `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
245
|
|
|
242
246
|
**Refusés au démarrage**, avec le motif (`proxyMountProblems()`, `rules.ts#proxyMountProblems`) :
|
|
243
247
|
|
|
@@ -295,12 +299,13 @@ modules, ses images et le WebSocket du rechargement à chaud partagent alors l'o
|
|
|
295
299
|
l'application — une IP de réseau local, un téléphone ou un conteneur fonctionnent en HTTPS sans
|
|
296
300
|
réglage.
|
|
297
301
|
|
|
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
|
-
|
|
|
302
|
+
| Ce qui est fixé par `@nodefony/frontend` | Pourquoi |
|
|
303
|
+
| ----------------------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
304
|
+
| `methods: ["GET", "HEAD"]` | un serveur de sources ne sert que des lectures |
|
|
305
|
+
| `websocket: true` | le rechargement à chaud passe par un WebSocket |
|
|
306
|
+
| `stripHeaders: ["cookie", "authorization"]` | Vite n'a pas à voir la session de l'application |
|
|
307
|
+
| `accessLog: "errors"` | une page charge des centaines de modules ; seuls les échecs méritent une ligne au journal |
|
|
308
|
+
| cible = le Vite de CETTE application, s'il sert | un port repris par une autre application n'est jamais visé |
|
|
304
309
|
|
|
305
310
|
Ce qui se règle reste côté frontend, dans `use("@nodefony/frontend", { … })` : `devHost` (défaut
|
|
306
311
|
`127.0.0.1`, à garder — le navigateur ne joint jamais Vite directement) et `devPort` (défaut `5173`,
|
|
@@ -395,6 +400,7 @@ Ce que le relais fait aux en-têtes, dans les deux sens :
|
|
|
395
400
|
| `X-Forwarded-Prefix` : à la suite de celui d'un pair `trustProxy` | |
|
|
396
401
|
| `Forwarded` d'un pair `trustProxy` : prolongé de ce saut, jamais créé | |
|
|
397
402
|
| prolonge `Via` ; recalcule le cadrage du corps | |
|
|
403
|
+
| décrémente `Max-Forwards` sur `OPTIONS` et `TRACE` | |
|
|
398
404
|
|
|
399
405
|
Les échecs, dans l'ordre où ils peuvent survenir (`forwardRequest()`, `forward.ts#forwardRequest`) :
|
|
400
406
|
|
|
@@ -410,6 +416,13 @@ Les échecs, dans l'ordre où ils peuvent survenir (`forwardRequest()`, `forward
|
|
|
410
416
|
- **coupure** — si l'amont tombe APRÈS avoir commencé à répondre, la réponse au client est
|
|
411
417
|
interrompue : un statut ne se rattrape pas une fois envoyé.
|
|
412
418
|
|
|
419
|
+
Une requête `OPTIONS` ou `TRACE` dont `Max-Forwards` vaut `0` n'atteint pas l'amont : le
|
|
420
|
+
proxy y répond lui-même, en destinataire final (RFC 9110 §7.6.2). `OPTIONS` reçoit `200` avec
|
|
421
|
+
`Allow` si le montage restreint ses `methods` ; `TRACE` reçoit la requête en `message/http`, sans
|
|
422
|
+
les en-têtes qui portent un secret (`Cookie`, `Authorization`, jetons, clés). Au-dessus de `0`, la
|
|
423
|
+
valeur part décrémentée, plafonnée à `MAX_VIA_HOPS` ; une valeur qui n'est pas un entier part
|
|
424
|
+
retirée. Les autres méthodes la relaient telle quelle.
|
|
425
|
+
|
|
413
426
|
Chaque montage a son **pool keep-alive dédié**, jamais l'agent global de Node : sous
|
|
414
427
|
`NODE_USE_ENV_PROXY` (poste d'entreprise), l'agent global enverrait un amont local au proxy de
|
|
415
428
|
l'entreprise (`ReverseProxy.agentFor()`, `reverse-proxy.ts#ReverseProxy.agentFor`). Les pools sont fermés à l'arrêt du
|
|
@@ -442,8 +455,9 @@ une connexion à usage unique, sans recréer de pool que personne ne fermerait.
|
|
|
442
455
|
| RFC 9110 §7.6.1 | Un intermédiaire retire les en-têtes de connexion | Liste fixe + jetons de `Connection`, dans les deux sens (`requestHeadersFor()`, `forward.ts#requestHeadersFor`) |
|
|
443
456
|
| RFC 9110 §7.6.3 | Un intermédiaire ajoute son entrée à `Via` | Pseudonyme par processus, prolongé à l'aller et au retour (`forward.ts#requestHeadersFor`) |
|
|
444
457
|
| RFC 9112 §6.3 | Un seul cadrage de corps ; `Transfer-Encoding` l'emporte | `Content-Length` retiré quand le corps est découpé (`bodyFramingOf()`, `forward.ts#bodyFramingOf`) |
|
|
445
|
-
| RFC 9112 §6.1 | Codage de transfert non compris → 501 | Tout autre que `chunked` seul est refusé, jamais réétiqueté (`forward.ts#
|
|
458
|
+
| RFC 9112 §6.1 | Codage de transfert non compris → 501 | Tout autre que `chunked` seul est refusé, jamais réétiqueté (`forward.ts#bodyFramingOf`) |
|
|
446
459
|
| RFC 9113 §8.2.2 | Pas d'en-tête de connexion en HTTP/2 | Réponse refusée par la sortie h2 → 502, jamais une exception non rattrapée |
|
|
460
|
+
| RFC 9110 §7.6.2 | `Max-Forwards` décrémenté ; à 0, répondre soi-même | Sur `OPTIONS`/`TRACE` seulement (`receivedMaxForwards()`, `forward.ts#receivedMaxForwards`) |
|
|
447
461
|
| RFC 7239 §8.1 | Ne croire `Forwarded` que d'un relais de confiance | `trustProxy` décide (`ReverseProxy.forward()`, `reverse-proxy.ts#ReverseProxy.forward`) |
|
|
448
462
|
| RFC 7239 §4 · §6 | Chaque relais ajoute son élément ; IPv6 entre crochets | Élément `for=…;proto=…;host=…` ajouté, valeurs non-jeton entre guillemets |
|
|
449
463
|
| RFC 6585 §4 | `429 Too Many Requests` | Quota par IP dépassé sur un upgrade relayé, avant le `101` |
|