@nodefony/http 10.0.0-beta.1 → 10.0.0-beta.3
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 +5 -3
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +11 -5
- package/dist/nodefony/command/certificatesCommand.js +0 -1
- package/dist/nodefony/config/config.js +48 -7
- package/dist/nodefony/config/defineModuleConfig.js +1 -1
- package/dist/nodefony/interfaces/IReverseProxy.js +1 -0
- package/dist/nodefony/service/HttpAdminApi.js +0 -1
- package/dist/nodefony/service/audit-logger.js +2 -3
- package/dist/nodefony/service/certificates.js +214 -158
- package/dist/nodefony/service/error-renderer.js +1 -1
- package/dist/nodefony/service/http-kernel.js +221 -47
- package/dist/nodefony/service/reverse-proxy.js +336 -0
- package/dist/nodefony/service/servers/server-http.js +3 -3
- package/dist/nodefony/service/servers/server-https.js +3 -3
- package/dist/nodefony/service/servers/server-static.js +4 -7
- package/dist/nodefony/service/servers/server-websocket-secure.js +12 -4
- package/dist/nodefony/service/servers/server-websocket.js +12 -4
- package/dist/nodefony/service/servers/wsHeartbeat.js +31 -15
- package/dist/nodefony/service/sessions/sessions-service.js +30 -9
- package/dist/nodefony/service/trace.js +2 -1
- package/dist/nodefony/service/upload/upload-service.js +3 -3
- package/dist/nodefony/service/x509.js +324 -0
- package/dist/nodefony/src/context/Context.js +28 -1
- package/dist/nodefony/src/context/domainMatcher.js +20 -2
- package/dist/nodefony/src/context/forwarded.js +2 -2
- package/dist/nodefony/src/context/http/HttpContext.js +91 -40
- package/dist/nodefony/src/context/http/Request.js +9 -4
- package/dist/nodefony/src/context/http/Response.js +46 -7
- package/dist/nodefony/src/context/http/SseStream.js +350 -0
- package/dist/nodefony/src/context/http/urlFastPath.js +35 -10
- package/dist/nodefony/src/context/responseEnded.js +27 -0
- package/dist/nodefony/src/context/websocket/Response.js +9 -0
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +2 -3
- package/dist/nodefony/src/context/websocket/websocketUrl.js +21 -0
- package/dist/nodefony/src/profiler/Profiler.js +6 -10
- package/dist/nodefony/src/proxy/forward.js +476 -0
- package/dist/nodefony/src/proxy/rules.js +182 -0
- package/dist/nodefony/src/servers/portBinder.js +57 -2
- package/dist/nodefony/src/servers/upgradeDispatch.js +49 -0
- package/dist/nodefony/src/session/session.js +28 -12
- package/dist/types/index.d.ts +5 -0
- package/dist/types/nodefony/config/config.d.ts +17 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +2 -0
- package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IReverseProxy.d.ts +67 -0
- package/dist/types/nodefony/interfaces/IRouting.d.ts +16 -0
- package/dist/types/nodefony/interfaces/ISecurity.d.ts +6 -0
- package/dist/types/nodefony/interfaces/index.d.ts +1 -0
- package/dist/types/nodefony/service/audit-logger.d.ts +2 -1
- package/dist/types/nodefony/service/certificates.d.ts +126 -50
- package/dist/types/nodefony/service/error-renderer.d.ts +1 -1
- package/dist/types/nodefony/service/http-kernel.d.ts +111 -5
- package/dist/types/nodefony/service/reverse-proxy.d.ts +156 -0
- package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +1 -0
- package/dist/types/nodefony/service/servers/server-websocket.d.ts +1 -0
- package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +26 -0
- package/dist/types/nodefony/service/sessions/sessions-service.d.ts +19 -1
- package/dist/types/nodefony/service/x509.d.ts +86 -0
- package/dist/types/nodefony/src/context/Context.d.ts +22 -2
- package/dist/types/nodefony/src/context/domainMatcher.d.ts +11 -1
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +47 -0
- package/dist/types/nodefony/src/context/http/Response.d.ts +3 -0
- package/dist/types/nodefony/src/context/http/SseStream.d.ts +155 -0
- package/dist/types/nodefony/src/context/responseEnded.d.ts +20 -0
- package/dist/types/nodefony/src/context/websocket/websocketUrl.d.ts +16 -0
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +1 -0
- package/dist/types/nodefony/src/proxy/forward.d.ts +156 -0
- package/dist/types/nodefony/src/proxy/rules.d.ts +85 -0
- package/dist/types/nodefony/src/servers/portBinder.d.ts +10 -46
- package/dist/types/nodefony/src/servers/upgradeDispatch.d.ts +39 -0
- package/dist/types/nodefony/src/session/session.d.ts +2 -0
- package/docs/cookies.md +13 -13
- package/docs/index.md +9 -3
- package/docs/observabilite.md +18 -18
- package/docs/rate-limit.md +30 -30
- package/docs/reverse-proxy.md +522 -0
- package/docs/servers.md +115 -78
- package/docs/session.md +61 -61
- package/docs/sse.md +283 -0
- package/docs/upload.md +37 -37
- package/package.json +10 -13
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Proxy inverse — relayer un préfixe d'URL vers un autre serveur, sur la même origine"
|
|
3
|
+
navTitle: Proxy inverse
|
|
4
|
+
lang: fr
|
|
5
|
+
module: "@nodefony/http"
|
|
6
|
+
topic: reverse-proxy
|
|
7
|
+
section: "Cœur runtime"
|
|
8
|
+
audience: [developer, devops]
|
|
9
|
+
tags:
|
|
10
|
+
[
|
|
11
|
+
reverse-proxy,
|
|
12
|
+
proxy,
|
|
13
|
+
upstream,
|
|
14
|
+
x-forwarded,
|
|
15
|
+
via,
|
|
16
|
+
hop-by-hop,
|
|
17
|
+
websocket,
|
|
18
|
+
vite,
|
|
19
|
+
429,
|
|
20
|
+
501,
|
|
21
|
+
502,
|
|
22
|
+
503,
|
|
23
|
+
504,
|
|
24
|
+
508,
|
|
25
|
+
]
|
|
26
|
+
version: "doc"
|
|
27
|
+
status: stable
|
|
28
|
+
updated: 2026-10-03
|
|
29
|
+
source: "src/packages/@nodefony/http/docs/reverse-proxy.md"
|
|
30
|
+
coverageModule: http
|
|
31
|
+
coverageFiles: reverse-proxy.ts,proxy/forward.ts,proxy/rules.ts,servers/upgradeDispatch.ts
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
# Proxy inverse — relayer un préfixe d'URL vers un autre serveur, sur la même origine
|
|
35
|
+
|
|
36
|
+
> Un **standard téléphonique** : l'appelant compose un seul numéro, et le standard transfère vers le
|
|
37
|
+
> bon poste selon ce qu'il demande. Nodefony fait de même avec un **préfixe d'URL** : tout ce qui
|
|
38
|
+
> commence par `/billing/` part vers le service de facturation, le reste suit le routage de
|
|
39
|
+
> l'application. Le navigateur ne voit qu'un serveur, une origine, un certificat ; l'autre serveur
|
|
40
|
+
> peut rester sur la boucle locale. Requêtes HTTP et, sur demande, connexions WebSocket. Chaque fait
|
|
41
|
+
> ci-dessous est ancré sur le code.
|
|
42
|
+
|
|
43
|
+
📍 [Documentation](../../../../../docs/index.md) › [@nodefony/http](index.md) › **Proxy inverse**
|
|
44
|
+
|
|
45
|
+
## 🧠 Le modèle mental — un aiguillage AVANT le routage
|
|
46
|
+
|
|
47
|
+
```mermaid
|
|
48
|
+
flowchart LR
|
|
49
|
+
C["Navigateur<br/>https://app:5152"] --> N["Nodefony<br/>barrière Host (421)"]
|
|
50
|
+
N --> Q{"préfixe monté ?"}
|
|
51
|
+
Q -- "oui" --> P["proxy inverse<br/>en-têtes filtrés · X-Forwarded · Via"]
|
|
52
|
+
P --> U["amont<br/>http://127.0.0.1:8080"]
|
|
53
|
+
Q -- "non" --> R["routage → pare-feu → contrôleur"]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Trois idées suffisent pour raisonner :
|
|
57
|
+
|
|
58
|
+
1. **Un montage = un préfixe → une origine.** `"/billing/"` → `http://127.0.0.1:8080`. Le préfixe le
|
|
59
|
+
plus long gagne (`ReverseProxy.match()`, `reverse-proxy.ts:229`).
|
|
60
|
+
2. **L'aiguillage passe AVANT le routage.** Une route attrape-tout de l'application ne peut pas
|
|
61
|
+
avaler un préfixe monté (`HttpKernel.routeHttpRequest()`, `http-kernel.ts:1699`).
|
|
62
|
+
3. **Sans montage, il ne coûte rien.** Le pipeline lit un champ (`mounts === null`) et passe son
|
|
63
|
+
chemin (`ReverseProxy.mounts`, `reverse-proxy.ts:174`).
|
|
64
|
+
|
|
65
|
+
## 📖 Lexique
|
|
66
|
+
|
|
67
|
+
| Terme | Sens |
|
|
68
|
+
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
69
|
+
| **Proxy inverse** | Serveur placé DEVANT d'autres serveurs, qui reçoit les requêtes à leur place et les leur transmet (_reverse proxy_). |
|
|
70
|
+
| **Amont** | Le serveur qui reçoit la requête relayée (_upstream_) — ici une origine `http(s)://hôte:port`. |
|
|
71
|
+
| **Montage** | Association d'un préfixe d'URL à un amont, avec ses réglages. |
|
|
72
|
+
| **Origine** | Triplet schéma + hôte + port (`https://app:5152`). Le navigateur cloisonne cookies, CSP et contextes sécurisés par origine. |
|
|
73
|
+
| **En-têtes de connexion** | En-têtes qui ne valent que pour UN saut (`Connection`, `Keep-Alive`, `Upgrade`…) — _hop-by-hop_, RFC 9110 §7.6.1. Jamais transmis. |
|
|
74
|
+
| **`Via`** | En-tête qui liste les relais traversés (RFC 9110 §7.6.3). Sert aussi à détecter une boucle. |
|
|
75
|
+
| **`X-Forwarded-*`** | En-têtes qui disent à l'amont qui est le vrai client : son IP (`-For`), son schéma (`-Proto`), l'hôte demandé (`-Host`). |
|
|
76
|
+
| **`trustProxy`** | Liste des pairs dont Nodefony croit les en-têtes `X-Forwarded-*` — voir [Serveurs](servers.md). |
|
|
77
|
+
| **`trustedHosts`** | Liste des noms d'hôte que l'application accepte ; un autre `Host` reçoit 421. |
|
|
78
|
+
| **Upgrade** | Bascule d'une connexion HTTP/1.1 vers un autre protocole — ici WebSocket (RFC 6455 §4). |
|
|
79
|
+
| **CSWSH** | _Cross-Site WebSocket Hijacking_ : une page tierce ouvre un WebSocket avec les cookies de la victime. Paré par le contrôle d'Origin. |
|
|
80
|
+
| **HMR** | _Hot Module Replacement_ : rechargement à chaud de Vite, qui passe par un WebSocket. |
|
|
81
|
+
| **Pool keep-alive** | Connexions vers l'amont gardées ouvertes et réutilisées, pour ne pas payer une poignée de main TCP par requête. |
|
|
82
|
+
| **400 / 421 / 403** | Requête ambiguë ou malformée · hôte non servi ici (_Misdirected Request_) · Origin refusée. |
|
|
83
|
+
| **502 / 504 / 508** | Amont injoignable (_Bad Gateway_) · amont muet (_Gateway Timeout_) · boucle de relais détectée (_Loop Detected_, RFC 5842 §7.2). |
|
|
84
|
+
|
|
85
|
+
## Qu'est-ce qu'un proxy inverse, et quel problème résout-il ?
|
|
86
|
+
|
|
87
|
+
Une application réelle a rarement un seul serveur. Un service de facturation existant, un serveur de
|
|
88
|
+
sources (Vite en développement), un outil interne : chacun écoute sur son port. Les exposer tels
|
|
89
|
+
quels coûte cher :
|
|
90
|
+
|
|
91
|
+
- **Plusieurs origines** : le navigateur les cloisonne. Il faut du CORS, les cookies ne suivent pas,
|
|
92
|
+
et chaque port réclame son propre certificat.
|
|
93
|
+
- **Plusieurs ports ouverts** : autant de surfaces à durcir, et un serveur de développement n'est
|
|
94
|
+
jamais fait pour être exposé.
|
|
95
|
+
- **Le contexte sécurisé se perd** : caméra, WebAuthn, presse-papiers exigent HTTPS. Un serveur
|
|
96
|
+
secondaire en HTTP sur une IP de réseau local casse ces API.
|
|
97
|
+
|
|
98
|
+
Le proxy inverse ramène tout sur **une** origine : le client parle à Nodefony, Nodefony parle aux
|
|
99
|
+
autres. Mal écrit, un proxy ouvre ses propres failles, et c'est contre elles que Nodefony est durci :
|
|
100
|
+
|
|
101
|
+
- **Contournement de préfixe** — `/billing/..%2fadmin` : le proxy voit `/billing/`, l'amont décode
|
|
102
|
+
`/admin`. Un chemin ambigu est refusé en 400, jamais normalisé en silence (`isAmbiguousPath()`,
|
|
103
|
+
`rules.ts:229`).
|
|
104
|
+
- **Désynchronisation de requête** (_request smuggling_) — proxy et amont ne s'accordent pas sur la
|
|
105
|
+
fin du corps. Le cadrage est recalculé à chaque relais (`bodyFramingOf()`, `forward.ts:166`).
|
|
106
|
+
- **Usurpation d'origine** — un client pose lui-même `X-Forwarded-For: 10.0.0.1`. Ces en-têtes ne
|
|
107
|
+
sont crus que d'un pair `trustProxy` (`requestHeadersFor()`, `forward.ts:201`).
|
|
108
|
+
|
|
109
|
+
## La vision Nodefony
|
|
110
|
+
|
|
111
|
+
Le proxy est un **service générique** de `@nodefony/http`, `reverse-proxy` (`ReverseProxy`,
|
|
112
|
+
`reverse-proxy.ts:82`). Il accepte deux sources de montages, soumises aux **mêmes règles**
|
|
113
|
+
(`proxyMountProblems()`, `rules.ts:98`) :
|
|
114
|
+
|
|
115
|
+
- **la configuration** de l'application — `proxy.mounts`, validée au démarrage ;
|
|
116
|
+
- **un module**, par `mount()` — c'est ainsi que `@nodefony/frontend` sert Vite sous `/_vite/`.
|
|
117
|
+
|
|
118
|
+
Ce qu'il fait différemment d'un nginx placé devant :
|
|
119
|
+
|
|
120
|
+
- **Même processus, même pipeline** : la barrière d'hôte (`trustedHosts`, 421) et la confiance de
|
|
121
|
+
proxy (`trustProxy`) sont celles de l'application, pas une seconde configuration qui dérive.
|
|
122
|
+
- **HTTP/1.1, HTTP/2 et WebSocket** sur le même montage : le client peut arriver en h2, l'amont est
|
|
123
|
+
joint en HTTP/1.1.
|
|
124
|
+
- **Refus nommés au démarrage** : un préfixe `/`, réservé, ou une cible qui n'est pas une origine nue
|
|
125
|
+
interrompt le boot avec le motif.
|
|
126
|
+
|
|
127
|
+
Le compromis assumé :
|
|
128
|
+
|
|
129
|
+
> [!WARNING]
|
|
130
|
+
> **Un préfixe monté est servi AVANT le pare-feu, la protection CSRF et les en-têtes de sécurité
|
|
131
|
+
> APPLICATIFS de l'application (CSP, `Referrer-Policy`, COOP).** C'est une surface publique pour qui
|
|
132
|
+
> atteint Nodefony : l'amont porte sa propre politique d'accès (`ReverseProxy.forward()`,
|
|
133
|
+
> `reverse-proxy.ts:319`). Ne montez jamais un service qui suppose être protégé par le pare-feu
|
|
134
|
+
> Nodefony.
|
|
135
|
+
|
|
136
|
+
Ce qui s'applique malgré tout, parce que posé à l'entrée, avant l'aiguillage :
|
|
137
|
+
|
|
138
|
+
- les **en-têtes de transport** — `X-Content-Type-Options`, `X-Frame-Options`, HSTS — sur toute
|
|
139
|
+
réponse relayée, sauf si l'amont fixe lui-même le même en-tête ;
|
|
140
|
+
- les **quotas** : le rate-limit par IP compte les requêtes relayées, et un upgrade WebSocket relayé
|
|
141
|
+
obéit aux mêmes bornes qu'un upgrade servi (voir [WebSocket relayé](#-websocket-relayé)).
|
|
142
|
+
|
|
143
|
+
La CSP à nonce de l'application, elle, n'est jamais imposée : elle casserait les pages d'un autre
|
|
144
|
+
serveur, dont les scripts ne portent pas ce nonce.
|
|
145
|
+
|
|
146
|
+
## 🚀 Démarrage rapide
|
|
147
|
+
|
|
148
|
+
Dans une application générée par `nodefony create app`, un service existant écoute sur
|
|
149
|
+
`127.0.0.1:8080`. On le publie sous `/billing/`, sur l'origine de l'application.
|
|
150
|
+
|
|
151
|
+
### 1. Un amont de démonstration
|
|
152
|
+
|
|
153
|
+
Pour voir ce que reçoit l'amont, un serveur minimal qui renvoie les en-têtes reçus suffit :
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
// billing.ts — amont de démonstration : renvoie ce qu'il reçoit
|
|
157
|
+
import http from "node:http";
|
|
158
|
+
|
|
159
|
+
http
|
|
160
|
+
.createServer((req, res) => {
|
|
161
|
+
res.setHeader("content-type", "application/json");
|
|
162
|
+
res.end(JSON.stringify({ path: req.url, headers: req.headers }, null, 2));
|
|
163
|
+
})
|
|
164
|
+
.listen(8080, "127.0.0.1");
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### 2. Déclarer le montage
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
// nodefony.config.ts — /billing/ relayé vers le service de facturation
|
|
171
|
+
export default defineConfig(() => ({
|
|
172
|
+
modules: [
|
|
173
|
+
use("@nodefony/http", {
|
|
174
|
+
proxy: {
|
|
175
|
+
mounts: {
|
|
176
|
+
"/billing/": {
|
|
177
|
+
target: "http://127.0.0.1:8080", // origine nue, sans chemin
|
|
178
|
+
stripPrefix: true, // l'amont reçoit /factures, pas /billing/factures
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
}),
|
|
183
|
+
"@nodefony/framework",
|
|
184
|
+
],
|
|
185
|
+
}));
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### 3. Observer
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
curl -sk -D - https://127.0.0.1:5152/billing/factures?mois=09
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
```text
|
|
195
|
+
HTTP/1.1 200 OK
|
|
196
|
+
content-type: application/json
|
|
197
|
+
via: 1.1 nodefony-3f9a1c2e
|
|
198
|
+
|
|
199
|
+
{
|
|
200
|
+
"path": "/factures?mois=09",
|
|
201
|
+
"headers": {
|
|
202
|
+
"host": "127.0.0.1:8080",
|
|
203
|
+
"x-forwarded-for": "127.0.0.1",
|
|
204
|
+
"x-forwarded-proto": "https",
|
|
205
|
+
"x-forwarded-host": "127.0.0.1:5152",
|
|
206
|
+
"x-forwarded-prefix": "/billing",
|
|
207
|
+
"via": "1.1 nodefony-3f9a1c2e"
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
- Le préfixe a été retiré du chemin, et l'amont l'apprend par `X-Forwarded-Prefix`.
|
|
213
|
+
- `Host` est celui de l'amont ; le nom demandé par le client part dans `X-Forwarded-Host`.
|
|
214
|
+
- `Via` porte le pseudonyme de CE processus, tiré au hasard au démarrage — il ne dit rien de la
|
|
215
|
+
machine (`ReverseProxy.pseudonym`, `reverse-proxy.ts:290`).
|
|
216
|
+
|
|
217
|
+
## ⚙️ Configuration
|
|
218
|
+
|
|
219
|
+
Section `proxy` de `use("@nodefony/http", { … })`, schéma Zod strict : une clé inconnue interrompt le
|
|
220
|
+
démarrage en la nommant (`proxySchema`, `config.ts:1085`).
|
|
221
|
+
|
|
222
|
+
| Option | Type | Défaut | Effet |
|
|
223
|
+
| ------------------ | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
224
|
+
| `timeoutMs` | entier | `30000` | Délai d'inactivité par défaut avec l'amont ; dépassé avant la réponse → 504. |
|
|
225
|
+
| `connectTimeoutMs` | entier | `5000` | Délai d'**établissement** de la connexion ; un amont qui n'accepte pas → 504 sans attendre 30 s. |
|
|
226
|
+
| `maxSockets` | entier | `256` | Connexions simultanées vers UN amont (pool dédié par montage) ; au-delà, les requêtes attendent. Borne aussi les tunnels WebSocket, hors pool : au-delà, 503. |
|
|
227
|
+
| `mounts` | `record` | `{}` | Préfixe → montage. Préfixe le plus long d'abord. |
|
|
228
|
+
|
|
229
|
+
Chaque montage (`proxyMountSchema`, `config.ts:1013`) :
|
|
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. |
|
|
241
|
+
|
|
242
|
+
**Refusés au démarrage**, avec le motif (`proxyMountProblems()`, `rules.ts:98`) :
|
|
243
|
+
|
|
244
|
+
- le préfixe `/` — il relaierait toute l'application, routes et pare-feu compris ;
|
|
245
|
+
- un préfixe sous `/nodefony/` (console d'administration) ou, depuis la configuration, sous
|
|
246
|
+
`/_vite/` (monté par `@nodefony/frontend`) — `RESERVED_PROXY_PREFIXES` (`rules.ts:18`) ;
|
|
247
|
+
- un préfixe qui contient `?`, `#`, `%`, `\`, un espace ou un segment `.`/`..` ;
|
|
248
|
+
- une cible qui n'est pas une origine `http(s)` nue (`parseProxyOrigin()`, `rules.ts:56`) ;
|
|
249
|
+
- deux clés qui désignent le même préfixe (`/svc` et `/svc/`).
|
|
250
|
+
|
|
251
|
+
### Choisir les réglages — en situation
|
|
252
|
+
|
|
253
|
+
**L'amont connaît son préfixe** (Vite et son `base`, une appli servie sous `/docs/`) : ne rien
|
|
254
|
+
retirer.
|
|
255
|
+
|
|
256
|
+
```ts ignore
|
|
257
|
+
"/docs/": { target: "http://127.0.0.1:4000" } // l'amont reçoit /docs/guide
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
**L'amont croit être à la racine** (un service existant qu'on ne modifie pas) : retirer le préfixe.
|
|
261
|
+
Ses redirections `Location: /login` sont ramenées en `/billing/login`
|
|
262
|
+
(`rewriteLocation()`, `forward.ts:288`).
|
|
263
|
+
|
|
264
|
+
```ts ignore
|
|
265
|
+
"/billing/": { target: "http://127.0.0.1:8080", stripPrefix: true }
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
> [!IMPORTANT]
|
|
269
|
+
> `stripPrefix` ne réécrit ni le HTML ni le `Path` des cookies. Un amont qui écrit des liens absolus
|
|
270
|
+
> (`href="/factures"`) les fera sortir du montage : servez-le sous son préfixe, ou faites-lui lire
|
|
271
|
+
> `X-Forwarded-Prefix`.
|
|
272
|
+
|
|
273
|
+
**L'amont n'a pas à voir la session de l'application** : retirer les authentifiants.
|
|
274
|
+
|
|
275
|
+
```ts ignore
|
|
276
|
+
"/sources/": {
|
|
277
|
+
target: "http://127.0.0.1:5173",
|
|
278
|
+
methods: ["GET", "HEAD"], // un serveur de sources ne sert que des lectures
|
|
279
|
+
stripHeaders: ["cookie", "authorization"],
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
| Le client envoie… | Résultat |
|
|
284
|
+
| ------------------------------------------ | ---------------------------------------------------------- |
|
|
285
|
+
| `GET /sources/app.js` avec son cookie | relayé, **sans** `Cookie` ni `Authorization` |
|
|
286
|
+
| `POST /sources/app.js` | **non relayé** : suit le routage (404 si rien ne le prend) |
|
|
287
|
+
| `GET /sources/..%2fsecret` | **400**, l'amont n'est pas contacté |
|
|
288
|
+
| `GET /sources/x` avec `Host: evil.example` | **421** (barrière d'hôte, si `domainCheck`) |
|
|
289
|
+
|
|
290
|
+
### Le cas Vite — rien à déclarer
|
|
291
|
+
|
|
292
|
+
En développement, le front servi par Vite passe par ce proxy **sans une ligne de configuration** :
|
|
293
|
+
`@nodefony/frontend` monte lui-même `/_vite/<famille>/` vers le Vite de chaque famille. La page, ses
|
|
294
|
+
modules, ses images et le WebSocket du rechargement à chaud partagent alors l'origine de
|
|
295
|
+
l'application — une IP de réseau local, un téléphone ou un conteneur fonctionnent en HTTPS sans
|
|
296
|
+
réglage.
|
|
297
|
+
|
|
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é |
|
|
304
|
+
|
|
305
|
+
Ce qui se règle reste côté frontend, dans `use("@nodefony/frontend", { … })` : `devHost` (défaut
|
|
306
|
+
`127.0.0.1`, à garder — le navigateur ne joint jamais Vite directement) et `devPort` (défaut `5173`,
|
|
307
|
+
chaque famille prend le port suivant). Le préfixe `/_vite/` est refusé dans `proxy.mounts` : il
|
|
308
|
+
appartient au module (`MODULE_PROXY_PREFIXES`, `rules.ts:25`). En production, Vite ne tourne pas et
|
|
309
|
+
aucun montage n'est posé.
|
|
310
|
+
|
|
311
|
+
## 🔌 WebSocket relayé
|
|
312
|
+
|
|
313
|
+
Avec `websocket: true`, l'upgrade du préfixe est relayé au lieu d'aller au serveur WebSocket de
|
|
314
|
+
Nodefony. Un seul écouteur `upgrade` par serveur fait l'aiguillage : le proxy d'abord, puis `ws`,
|
|
315
|
+
créé en `noServer` pour garder ses propres contrôles (`path`, `verifyClient`, `maxPayload`)
|
|
316
|
+
(`attachUpgradeDispatch()`, `upgradeDispatch.ts:41`).
|
|
317
|
+
|
|
318
|
+
Avant de relayer, chaque contrôle rend la réponse qu'aurait rendue le serveur WebSocket de Nodefony
|
|
319
|
+
(`ReverseProxy.handleUpgrade()`, `reverse-proxy.ts:384`) :
|
|
320
|
+
|
|
321
|
+
| Le handshake… | Réponse |
|
|
322
|
+
| ------------------------------------------------------------------------- | ------- |
|
|
323
|
+
| porte un `Host` hors `trustedHosts` (si `domainCheck`) | **421** |
|
|
324
|
+
| a un chemin ambigu, ou n'est pas un handshake RFC 6455 conforme | **400** |
|
|
325
|
+
| porte une `Origin` refusée par la règle du serveur WebSocket | **403** |
|
|
326
|
+
| trouve déjà `maxSockets` tunnels ouverts vers cet amont | **503** |
|
|
327
|
+
| dépasse un quota par IP : débit de handshakes, ou `wsMaxConnectionsPerIp` | **429** |
|
|
328
|
+
| passe tous les contrôles | relayé |
|
|
329
|
+
|
|
330
|
+
Les quotas sont ceux d'un upgrade servi par Nodefony, appliqués par la **même** règle du noyau
|
|
331
|
+
(`HttpKernel.websocketQuotaRefusal()`, `http-kernel.ts:2230`) ; un tunnel ouvert garde sa place
|
|
332
|
+
jusqu'à sa fermeture. Seule la forme du refus diffère : le proxy répond avant le `101`, il peut donc
|
|
333
|
+
rendre un statut HTTP, là où le serveur WebSocket ne peut plus que fermer avec le code 1013.
|
|
334
|
+
|
|
335
|
+
Une fois l'upgrade transmis, un amont qui accepte la connexion puis se tait est coupé au bout de
|
|
336
|
+
`timeoutMs` (**504**) ; ce délai cesse au `101`, et un tunnel établi peut rester muet aussi longtemps
|
|
337
|
+
qu'il veut.
|
|
338
|
+
|
|
339
|
+
Seul `Upgrade: websocket` est relayé : un tunnel `h2c` ou tout autre protocole raccordé octet pour
|
|
340
|
+
octet échapperait à toute inspection (`websocketHandshakeProblem()`, `rules.ts:252`). Une fois la
|
|
341
|
+
connexion établie, les deux sockets sont raccordées et détruites ensemble ; le TCP keep-alive détecte
|
|
342
|
+
un pair mort (`forwardUpgrade()`, `forward.ts:590`).
|
|
343
|
+
|
|
344
|
+
## 🧩 Étendre — monter depuis un module
|
|
345
|
+
|
|
346
|
+
Un module se résout le service par **nom**, sans importer `@nodefony/http`. La cible peut être une
|
|
347
|
+
**fonction**, rappelée à chaque requête : utile quand le port de l'amont n'est connu qu'à
|
|
348
|
+
l'exécution. Elle rend `undefined` quand rien n'est relayable, et la requête suit alors le routage.
|
|
349
|
+
|
|
350
|
+
```ts ignore
|
|
351
|
+
// Dans un service de module — un serveur enfant qui choisit son port au démarrage
|
|
352
|
+
const proxy = this.container?.get("reverse-proxy") as ReverseProxy | undefined;
|
|
353
|
+
proxy?.mount("/outil/", {
|
|
354
|
+
target: () => (child.ready ? `http://127.0.0.1:${child.port}` : undefined),
|
|
355
|
+
websocket: true,
|
|
356
|
+
});
|
|
357
|
+
// À l'arrêt de l'enfant :
|
|
358
|
+
proxy?.unmount("/outil/");
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
- `mount()` est **idempotent** pour un préfixe : il remplace le montage précédent. S'il remplace un
|
|
362
|
+
montage de la configuration, il le dit en `WARNING` (`ReverseProxy.mount()`, `reverse-proxy.ts:129`).
|
|
363
|
+
- `unmount()` ferme le pool de l'amont ; le dernier retiré remet `mounts` à `null`, et le pipeline
|
|
364
|
+
cesse de le consulter (`ReverseProxy.unmount()`, `reverse-proxy.ts:199`).
|
|
365
|
+
|
|
366
|
+
**Exemple réel** : `@nodefony/frontend` monte ainsi chaque famille Vite (voir « Le cas Vite »), avec
|
|
367
|
+
une cible fonction qui ne vise un port que si le Vite de CETTE application y répond
|
|
368
|
+
(`FrontendService.mountDevProxy()`, `FrontendService.ts:486`).
|
|
369
|
+
|
|
370
|
+
## 🏗️ Architecture interne — le parcours d'une requête relayée
|
|
371
|
+
|
|
372
|
+
```mermaid
|
|
373
|
+
sequenceDiagram
|
|
374
|
+
participant C as Client
|
|
375
|
+
participant K as HttpKernel
|
|
376
|
+
participant P as ReverseProxy
|
|
377
|
+
participant U as Amont
|
|
378
|
+
C->>K: GET /billing/factures
|
|
379
|
+
K->>K: barrière Host (421 sinon)
|
|
380
|
+
K->>P: forward(req, res)
|
|
381
|
+
P->>P: match · méthode · cible · chemin ambigu ?
|
|
382
|
+
P->>U: requête, en-têtes filtrés + X-Forwarded + Via
|
|
383
|
+
U-->>P: réponse
|
|
384
|
+
P-->>C: en-têtes de connexion retirés, Via prolongé, Location réécrit
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Ce que le relais fait aux en-têtes, dans les deux sens :
|
|
388
|
+
|
|
389
|
+
| Vers l'amont (`requestHeadersFor()`, `forward.ts:201`) | Vers le client (`responseHeadersFor()`, `forward.ts:311`) |
|
|
390
|
+
| --------------------------------------------------------------------- | ---------------------------------------------------------------- |
|
|
391
|
+
| retire les en-têtes de connexion, et ceux que nomme `Connection` | retire les en-têtes de connexion, et ceux que nomme `Connection` |
|
|
392
|
+
| retire `Expect` (Nodefony a déjà répondu `100`) et les `stripHeaders` | garde les `Set-Cookie` multiples |
|
|
393
|
+
| réécrit `Host` sur la cible (sauf `preserveHost`) | ramène `Location` sous le préfixe (`stripPrefix`) |
|
|
394
|
+
| `X-Forwarded-*` : prolongés d'un pair `trustProxy`, recommencés sinon | prolonge `Via` |
|
|
395
|
+
| `X-Forwarded-Prefix` : à la suite de celui d'un pair `trustProxy` | |
|
|
396
|
+
| `Forwarded` d'un pair `trustProxy` : prolongé de ce saut, jamais créé | |
|
|
397
|
+
| prolonge `Via` ; recalcule le cadrage du corps | |
|
|
398
|
+
|
|
399
|
+
Les échecs, dans l'ordre où ils peuvent survenir (`forwardRequest()`, `forward.ts:408`) :
|
|
400
|
+
|
|
401
|
+
- **508** — la requête porte déjà le pseudonyme de ce processus dans `Via`, ou au moins
|
|
402
|
+
`MAX_VIA_HOPS` (10) sauts : une boucle, l'amont n'est pas contacté (`viaLoops()`,
|
|
403
|
+
`forward.ts:129`). Le plafond compte parce que le pseudonyme ne repère une boucle qu'au retour
|
|
404
|
+
dans ce processus — derrière un répartiteur qui tourne sur N instances, après N+1 sauts ;
|
|
405
|
+
- **501** — un `Transfer-Encoding` autre que `chunked` seul (`gzip, chunked`) : le transmettre
|
|
406
|
+
réétiqueté livrerait à l'amont un corps compressé qu'il croirait en clair (`bodyFramingOf()`,
|
|
407
|
+
`forward.ts:166`) ;
|
|
408
|
+
- **504** — connexion non établie sous `connectTimeoutMs`, ou amont muet au-delà de `timeoutMs` ;
|
|
409
|
+
- **502** — amont injoignable, ou réponse que la sortie refuse (en-tête interdit en HTTP/2) ;
|
|
410
|
+
- **coupure** — si l'amont tombe APRÈS avoir commencé à répondre, la réponse au client est
|
|
411
|
+
interrompue : un statut ne se rattrape pas une fois envoyé.
|
|
412
|
+
|
|
413
|
+
Chaque montage a son **pool keep-alive dédié**, jamais l'agent global de Node : sous
|
|
414
|
+
`NODE_USE_ENV_PROXY` (poste d'entreprise), l'agent global enverrait un amont local au proxy de
|
|
415
|
+
l'entreprise (`ReverseProxy.agentFor()`, `reverse-proxy.ts:270`). Les pools sont fermés à l'arrêt du
|
|
416
|
+
noyau (`ReverseProxy.closeAll()`, `reverse-proxy.ts:215`) ; un relais encore en vol part ensuite sur
|
|
417
|
+
une connexion à usage unique, sans recréer de pool que personne ne fermerait.
|
|
418
|
+
|
|
419
|
+
## 🔐 Sécurité
|
|
420
|
+
|
|
421
|
+
- **Surface publique** : le montage passe avant le pare-feu (voir l'avertissement plus haut). Le
|
|
422
|
+
préfixe `/nodefony/` est refusé pour que la console d'administration ne puisse jamais être
|
|
423
|
+
court-circuitée.
|
|
424
|
+
- **Quotas maintenus** : rate-limit par IP sur les requêtes relayées ; mêmes bornes par IP pour un
|
|
425
|
+
upgrade relayé que pour un upgrade servi, plus `maxSockets` tunnels par amont.
|
|
426
|
+
- **Barrière d'hôte maintenue** : un `Host` hors `trustedHosts` n'est jamais relayé ; il suit le
|
|
427
|
+
pipeline jusqu'au 421, comme toute requête (`HttpKernel.routeHttpRequest()`, `http-kernel.ts:1699`).
|
|
428
|
+
- **Chemins ambigus refusés**, pas normalisés : `.`, `..`, `\`, octet nul et leurs formes encodées
|
|
429
|
+
`%2e`, `%2f`, `%5c`, `%00`, `%25` (`isAmbiguousPath()`, `rules.ts:229`).
|
|
430
|
+
- **Chaîne de confiance** : `Forwarded`, `X-Forwarded-*` et `X-Real-IP` d'un client direct sont
|
|
431
|
+
écartés ; seul un pair `trustProxy` les prolonge.
|
|
432
|
+
- **Certificat vérifié** vers une cible `https` ; `secure: false` n'est accepté que vers
|
|
433
|
+
`127.0.0.0/8`, `localhost` ou `::1`. Ailleurs, déclarer l'autorité de l'amont
|
|
434
|
+
(`certificates.extraCaFiles`, voir [servers](servers.md#autorités-sortantes--extracafiles)).
|
|
435
|
+
- **Corps d'erreur muets** : `Bad Gateway`, `Gateway Timeout`, `Loop Detected` — aucun détail
|
|
436
|
+
interne (adresse, pile) ne part vers le client.
|
|
437
|
+
|
|
438
|
+
## 📜 Normes appliquées
|
|
439
|
+
|
|
440
|
+
| Norme | Exigence | Comment le code s'y conforme |
|
|
441
|
+
| ----------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
442
|
+
| 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:201`) |
|
|
443
|
+
| RFC 9110 §7.6.3 | Un intermédiaire ajoute son entrée à `Via` | Pseudonyme par processus, prolongé à l'aller et au retour (`forward.ts:244`) |
|
|
444
|
+
| 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:166`) |
|
|
445
|
+
| RFC 9112 §6.1 | Codage de transfert non compris → 501 | Tout autre que `chunked` seul est refusé, jamais réétiqueté (`forward.ts:147`) |
|
|
446
|
+
| 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 |
|
|
447
|
+
| RFC 7239 §8.1 | Ne croire `Forwarded` que d'un relais de confiance | `trustProxy` décide (`ReverseProxy.forward()`, `reverse-proxy.ts:319`) |
|
|
448
|
+
| 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
|
+
| RFC 6585 §4 | `429 Too Many Requests` | Quota par IP dépassé sur un upgrade relayé, avant le `101` |
|
|
450
|
+
| RFC 6455 §4.2.1 · §10.2 | Handshake conforme ; contrôle d'Origin contre le CSWSH | `websocketHandshakeProblem()` (`rules.ts:252`) + règle d'Origin du serveur WS |
|
|
451
|
+
| RFC 5842 §7.2 | `508 Loop Detected` | Emprunté à WebDAV pour une boucle de relais (`viaLoops()`, `forward.ts:129`) |
|
|
452
|
+
|
|
453
|
+
## ⚡ Performance & mémoire
|
|
454
|
+
|
|
455
|
+
- **Sans montage** : une lecture de champ par requête et par upgrade. Mesuré en débit mono-process
|
|
456
|
+
`production` (route de banc, paires alternées dans les deux ordres) : aucun écart distinguable du
|
|
457
|
+
bruit de la machine, inférieur à ~1 %.
|
|
458
|
+
- **Rétention** : le gate mémoire relaie des requêtes GET et des upgrades WebSocket vers un vrai
|
|
459
|
+
Vite, et mesure la pente du tas — sous le seuil commun d'1 Ko par itération, sans scope ni
|
|
460
|
+
contexte résiduel. Le seuil vit dans `tests/helpers/retention.ts`, jamais recopié ici.
|
|
461
|
+
- **Pool borné** : `maxSockets` connexions par amont, au plus 32 gardées au repos ; le socket le plus
|
|
462
|
+
récent est réutilisé d'abord (`lifo`), le moins susceptible d'avoir été fermé par l'amont.
|
|
463
|
+
|
|
464
|
+
## 📡 Observabilité
|
|
465
|
+
|
|
466
|
+
Les montages se déclarent dans la configuration résolue (console d'administration, carte du module
|
|
467
|
+
`@nodefony/http`) ; un montage posé ou retiré par un module est journalisé en `DEBUG`, et le
|
|
468
|
+
remplacement d'un montage de configuration en `WARNING`. Une réponse relayée se reconnaît à son
|
|
469
|
+
en-tête `Via`.
|
|
470
|
+
|
|
471
|
+
## ⚠️ Pièges (symptôme → cause → correction)
|
|
472
|
+
|
|
473
|
+
| Symptôme | Cause | Correction |
|
|
474
|
+
| ---------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
475
|
+
| Le boot s'arrête sur `proxy.mounts` | Préfixe `/`, réservé, ou cible avec un chemin | Lire le motif : il nomme la clé et le geste (`rules.ts:98`) |
|
|
476
|
+
| Une route de l'application sous le préfixe n'est jamais atteinte | Le proxy passe AVANT le routage | Attendu : choisir un préfixe qui n'appartient qu'à l'amont |
|
|
477
|
+
| L'amont redirige hors du montage | `stripPrefix` + liens absolus dans le HTML de l'amont | Servir l'amont sous son préfixe, ou lui faire lire `X-Forwarded-Prefix` |
|
|
478
|
+
| L'amont voit `127.0.0.1` comme client | Lecture de l'IP de la socket au lieu de `X-Forwarded-For` | Faire confiance à Nodefony côté amont (son équivalent de `trustProxy`) |
|
|
479
|
+
| `X-Forwarded-For` posé par le client disparaît | Client direct, hors `trustProxy` | Attendu : un client ne choisit pas son IP. Régler `trustProxy` pour un relais légitime |
|
|
480
|
+
| 400 sur une URL qui contient `%2F` | Chemin ambigu refusé | Attendu : encoder différemment côté client, ou servir cette ressource hors du montage |
|
|
481
|
+
| 504 après 5 s | L'amont n'accepte pas la connexion | Vérifier qu'il écoute ; ajuster `connectTimeoutMs` seulement si l'amont est lent à accepter |
|
|
482
|
+
| 508 Loop Detected | La cible renvoie vers Nodefony lui-même | Corriger `target` : elle doit désigner l'amont, pas l'application |
|
|
483
|
+
| 508 derrière une longue chaîne de relais | `Via` porte déjà 10 sauts ou plus | Raccourcir la chaîne : au-delà, le proxy présume une boucle |
|
|
484
|
+
| 501 sur un envoi de corps | `Transfer-Encoding: gzip, chunked` (ou autre codage) | Envoyer le corps en `chunked` seul, ou compressé avec `Content-Encoding` |
|
|
485
|
+
| Upgrade relayé refusé en 429 | Quota par IP : débit de handshakes ou `wsMaxConnectionsPerIp` | Attendu sous flood ; sinon ajuster `rateLimit` ou `wsMaxConnectionsPerIp` ([Rate-limit](rate-limit.md)) |
|
|
486
|
+
| Upgrade relayé refusé en 503 | `maxSockets` tunnels déjà ouverts vers cet amont | Relever `proxy.maxSockets`, ou vérifier que les tunnels se ferment côté client |
|
|
487
|
+
| `POST` sous le préfixe rend 404 | `methods` restreint | Ajouter la méthode, ou laisser `methods` absent |
|
|
488
|
+
| Le WebSocket du préfixe arrive au serveur WS de Nodefony | `websocket` absent (défaut `false`) | `websocket: true` sur le montage |
|
|
489
|
+
|
|
490
|
+
## 🧪 Tests & couverture
|
|
491
|
+
|
|
492
|
+
Les chiffres exacts vivent dans la carte de tests de cette page, régénérée depuis vitest, jamais
|
|
493
|
+
figés dans le Markdown.
|
|
494
|
+
|
|
495
|
+
| Type | Où |
|
|
496
|
+
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
497
|
+
| Unitaires — relais | `unit/reverseProxy.test.ts` — réseau réel : HTTP/1.1, front HTTP/2 vers amont HTTP/1.1, upgrade WebSocket (quotas d'un VRAI noyau, tunnels bornés, délai du handshake), refus au montage, durcissement, contrôles avant relais |
|
|
498
|
+
| Unitaires — config | `unit/proxyConfig.test.ts` — schéma Zod strict, refus nommés, préfixes en double |
|
|
499
|
+
| Intégration | `http/vite-relay.test.ts` — serveur de développement réel : image et client Vite relayés, en-têtes de transport présents et CSP absente, socket HMR sur l'origine de la page, écriture non relayée, 400, 421 |
|
|
500
|
+
| Mémoire | `http/memory.test.ts` — GET relayé et upgrade WebSocket relayé : pente du tas, scopes et contextes résiduels |
|
|
501
|
+
|
|
502
|
+
Ces suites ont été vues échouer en débranchant le câblage (montage retiré, filtre d'en-têtes, garde
|
|
503
|
+
du délai de connexion, quotas, plafond de tunnels, délai du handshake, plafond de `Via`, refus 501) :
|
|
504
|
+
elles prouvent le branchement, pas seulement les fonctions.
|
|
505
|
+
|
|
506
|
+
Ce qui **manque** :
|
|
507
|
+
|
|
508
|
+
- Aucun banc de débit **à travers** un montage actif (coût par requête relayée).
|
|
509
|
+
- Aucun test de débit WebSocket relayé : la rétention est prouvée, pas le débit.
|
|
510
|
+
- Le relais vers une cible `https` n'est éprouvé qu'unitairement.
|
|
511
|
+
|
|
512
|
+
Suites : `npm test` (unitaires), `npm run test:integration` et `npm run test:memory` (serveur
|
|
513
|
+
requis). Skills associés : `nodefony-load-test`, `nodefony-check-memory-health`,
|
|
514
|
+
`nodefony-security-review`.
|
|
515
|
+
|
|
516
|
+
## 🔗 Pour aller plus loin
|
|
517
|
+
|
|
518
|
+
- ⬆️ **Retour au hub** : [@nodefony/http — vue du module](index.md) · [Toute la documentation](../../../../../docs/index.md)
|
|
519
|
+
- 🧭 **Pages sœurs** : [Serveurs](servers.md) — `trustProxy`, `trustedHosts`, TLS ; [Rate-limit](rate-limit.md).
|
|
520
|
+
- Où le proxy se branche dans le trajet d'une requête → [pipeline-requete](../../../../../docs/architecture/pipeline-requete.md).
|
|
521
|
+
- Déployer derrière un ingress (nginx, HAProxy) qui termine le TLS → [docker-cloud-native](../../../../../docs/guides/docker-cloud-native.md).
|
|
522
|
+
- Configuration d'application (`defineConfig`, `use`, env) → [configuration](../../../../../docs/guides/configuration.md).
|