@nodefony/http 10.0.0-alpha.1
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/LICENSE +544 -0
- package/README.md +77 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js +8 -0
- package/dist/index.js +108 -0
- package/dist/nodefony/command/assetsPublishCommand.js +102 -0
- package/dist/nodefony/command/certificatesCommand.js +47 -0
- package/dist/nodefony/command/networkCommand.js +27 -0
- package/dist/nodefony/command/proxyGenerateCommand.js +66 -0
- package/dist/nodefony/config/config.js +335 -0
- package/dist/nodefony/config/defineModuleConfig.js +93 -0
- package/dist/nodefony/interfaces/IContext.js +1 -0
- package/dist/nodefony/interfaces/ICookie.js +1 -0
- package/dist/nodefony/interfaces/IErrorRenderer.js +1 -0
- package/dist/nodefony/interfaces/IHttpConfig.js +1 -0
- package/dist/nodefony/interfaces/IHttpKernel.js +1 -0
- package/dist/nodefony/interfaces/IRequest.js +1 -0
- package/dist/nodefony/interfaces/IRequestLogger.js +1 -0
- package/dist/nodefony/interfaces/IResponse.js +1 -0
- package/dist/nodefony/interfaces/ISession.js +1 -0
- package/dist/nodefony/interfaces/IUpload.js +1 -0
- package/dist/nodefony/interfaces/index.js +1 -0
- package/dist/nodefony/service/HttpAdminApi.js +376 -0
- package/dist/nodefony/service/ProfilerAdminApi.js +73 -0
- package/dist/nodefony/service/audit-logger.js +159 -0
- package/dist/nodefony/service/certificates.js +545 -0
- package/dist/nodefony/service/error-renderer.js +320 -0
- package/dist/nodefony/service/http-kernel.js +948 -0
- package/dist/nodefony/service/pretty-request-logger.js +72 -0
- package/dist/nodefony/service/request-logger.js +54 -0
- package/dist/nodefony/service/servers/clientError.js +20 -0
- package/dist/nodefony/service/servers/server-http.js +135 -0
- package/dist/nodefony/service/servers/server-https.js +204 -0
- package/dist/nodefony/service/servers/server-static.js +192 -0
- package/dist/nodefony/service/servers/server-websocket-secure.js +104 -0
- package/dist/nodefony/service/servers/server-websocket.js +104 -0
- package/dist/nodefony/service/servers/serverShutdown.js +31 -0
- package/dist/nodefony/service/servers/wsHeartbeat.js +64 -0
- package/dist/nodefony/service/sessions/sessions-service.js +580 -0
- package/dist/nodefony/service/trace.js +72 -0
- package/dist/nodefony/service/upload/upload-service.js +171 -0
- package/dist/nodefony/src/assets/collectAssets.js +34 -0
- package/dist/nodefony/src/assets/prebuiltUi.js +125 -0
- package/dist/nodefony/src/context/Context.js +415 -0
- package/dist/nodefony/src/context/domainMatcher.js +88 -0
- package/dist/nodefony/src/context/forwarded.js +185 -0
- package/dist/nodefony/src/context/http/HttpContext.js +309 -0
- package/dist/nodefony/src/context/http/Request.js +543 -0
- package/dist/nodefony/src/context/http/Response.js +368 -0
- package/dist/nodefony/src/context/http/parser.js +188 -0
- package/dist/nodefony/src/context/http/urlFastPath.js +103 -0
- package/dist/nodefony/src/context/http2/Request.js +29 -0
- package/dist/nodefony/src/context/http2/Response.js +97 -0
- package/dist/nodefony/src/context/metaData.js +47 -0
- package/dist/nodefony/src/context/requestId.js +41 -0
- package/dist/nodefony/src/context/trustProxy.js +167 -0
- package/dist/nodefony/src/context/websocket/Response.js +181 -0
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +389 -0
- package/dist/nodefony/src/context/websocket/wsBackpressure.js +56 -0
- package/dist/nodefony/src/context/websocket/wsLogContent.js +68 -0
- package/dist/nodefony/src/cookies/cookie.js +258 -0
- package/dist/nodefony/src/errors/httpError.js +69 -0
- package/dist/nodefony/src/profiler/FrameProfile.js +95 -0
- package/dist/nodefony/src/profiler/Profiler.js +139 -0
- package/dist/nodefony/src/proxy/generateProxyConfig.js +157 -0
- package/dist/nodefony/src/rateLimit/IRateLimitStore.js +1 -0
- package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +146 -0
- package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +64 -0
- package/dist/nodefony/src/rateLimit/rateLimitFilters.js +20 -0
- package/dist/nodefony/src/servers/portBinder.js +114 -0
- package/dist/nodefony/src/session/session.js +390 -0
- package/dist/nodefony/src/session/storage/MemorySessionStorage.js +185 -0
- package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +137 -0
- package/dist/nodefony/src/session/storage/sessionFilters.js +83 -0
- package/dist/nodefony/src/session/storage/sessionSort.js +53 -0
- package/dist/types/index.d.ts +83 -0
- package/dist/types/nodefony/command/assetsPublishCommand.d.ts +23 -0
- package/dist/types/nodefony/command/certificatesCommand.d.ts +17 -0
- package/dist/types/nodefony/command/networkCommand.d.ts +8 -0
- package/dist/types/nodefony/command/proxyGenerateCommand.d.ts +19 -0
- package/dist/types/nodefony/config/config.d.ts +197 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +39 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +138 -0
- package/dist/types/nodefony/interfaces/ICookie.d.ts +47 -0
- package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +55 -0
- package/dist/types/nodefony/interfaces/IHttpConfig.d.ts +12 -0
- package/dist/types/nodefony/interfaces/IHttpKernel.d.ts +10 -0
- package/dist/types/nodefony/interfaces/IRequest.d.ts +35 -0
- package/dist/types/nodefony/interfaces/IRequestLogger.d.ts +31 -0
- package/dist/types/nodefony/interfaces/IResponse.d.ts +39 -0
- package/dist/types/nodefony/interfaces/ISession.d.ts +283 -0
- package/dist/types/nodefony/interfaces/IUpload.d.ts +66 -0
- package/dist/types/nodefony/interfaces/index.d.ts +7 -0
- package/dist/types/nodefony/service/HttpAdminApi.d.ts +18 -0
- package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +23 -0
- package/dist/types/nodefony/service/audit-logger.d.ts +143 -0
- package/dist/types/nodefony/service/certificates.d.ts +246 -0
- package/dist/types/nodefony/service/error-renderer.d.ts +74 -0
- package/dist/types/nodefony/service/http-kernel.d.ts +377 -0
- package/dist/types/nodefony/service/pretty-request-logger.d.ts +25 -0
- package/dist/types/nodefony/service/request-logger.d.ts +18 -0
- package/dist/types/nodefony/service/servers/clientError.d.ts +14 -0
- package/dist/types/nodefony/service/servers/server-http.d.ts +42 -0
- package/dist/types/nodefony/service/servers/server-https.d.ts +41 -0
- package/dist/types/nodefony/service/servers/server-static.d.ts +62 -0
- package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +29 -0
- package/dist/types/nodefony/service/servers/server-websocket.d.ts +29 -0
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +27 -0
- package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +46 -0
- package/dist/types/nodefony/service/sessions/sessions-service.d.ts +218 -0
- package/dist/types/nodefony/service/trace.d.ts +39 -0
- package/dist/types/nodefony/service/upload/upload-service.d.ts +61 -0
- package/dist/types/nodefony/src/assets/collectAssets.d.ts +35 -0
- package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +99 -0
- package/dist/types/nodefony/src/context/Context.d.ts +195 -0
- package/dist/types/nodefony/src/context/domainMatcher.d.ts +67 -0
- package/dist/types/nodefony/src/context/forwarded.d.ts +95 -0
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +85 -0
- package/dist/types/nodefony/src/context/http/Request.d.ts +203 -0
- package/dist/types/nodefony/src/context/http/Response.d.ts +68 -0
- package/dist/types/nodefony/src/context/http/parser.d.ts +65 -0
- package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +52 -0
- package/dist/types/nodefony/src/context/http2/Request.d.ts +14 -0
- package/dist/types/nodefony/src/context/http2/Response.d.ts +20 -0
- package/dist/types/nodefony/src/context/metaData.d.ts +58 -0
- package/dist/types/nodefony/src/context/requestId.d.ts +28 -0
- package/dist/types/nodefony/src/context/trustProxy.d.ts +77 -0
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +53 -0
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +125 -0
- package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +73 -0
- package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +37 -0
- package/dist/types/nodefony/src/cookies/cookie.d.ts +88 -0
- package/dist/types/nodefony/src/errors/httpError.d.ts +15 -0
- package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +110 -0
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +192 -0
- package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +76 -0
- package/dist/types/nodefony/src/rateLimit/IRateLimitStore.d.ts +98 -0
- package/dist/types/nodefony/src/rateLimit/MemoryRateLimitStore.d.ts +40 -0
- package/dist/types/nodefony/src/rateLimit/WsConnectionCounter.d.ts +37 -0
- package/dist/types/nodefony/src/rateLimit/rateLimitFilters.d.ts +18 -0
- package/dist/types/nodefony/src/servers/portBinder.d.ts +102 -0
- package/dist/types/nodefony/src/session/session.d.ts +171 -0
- package/dist/types/nodefony/src/session/storage/MemorySessionStorage.d.ts +77 -0
- package/dist/types/nodefony/src/session/storage/RevocationGuardStorage.d.ts +81 -0
- package/dist/types/nodefony/src/session/storage/sessionFilters.d.ts +102 -0
- package/dist/types/nodefony/src/session/storage/sessionSort.d.ts +45 -0
- package/docs/cookies.md +365 -0
- package/docs/index.md +163 -0
- package/docs/observabilite.md +460 -0
- package/docs/rate-limit.md +372 -0
- package/docs/servers.md +935 -0
- package/docs/session.md +768 -0
- package/docs/upload.md +460 -0
- package/package.json +101 -0
package/docs/upload.md
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Upload & corps de requête — multipart, JSON, bornes de payload"
|
|
3
|
+
navTitle: "Upload & corps de requête"
|
|
4
|
+
lang: fr
|
|
5
|
+
module: "@nodefony/http"
|
|
6
|
+
topic: upload
|
|
7
|
+
section: "Cœur runtime"
|
|
8
|
+
audience: [developer]
|
|
9
|
+
tags:
|
|
10
|
+
[
|
|
11
|
+
upload,
|
|
12
|
+
multipart,
|
|
13
|
+
busboy,
|
|
14
|
+
body,
|
|
15
|
+
json,
|
|
16
|
+
urlencoded,
|
|
17
|
+
xml,
|
|
18
|
+
payload,
|
|
19
|
+
413,
|
|
20
|
+
fichiers,
|
|
21
|
+
]
|
|
22
|
+
version: "doc"
|
|
23
|
+
status: stable
|
|
24
|
+
updated: 2026-07-21
|
|
25
|
+
source: "src/packages/@nodefony/http/docs/upload.md"
|
|
26
|
+
coverageModule: http
|
|
27
|
+
coverageFiles: context/http/Request.ts,context/http/parser.ts,service/upload/upload-service.ts,interfaces/IUpload.ts,config/config.ts
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
# Upload & corps de requête — multipart, JSON, bornes de payload
|
|
31
|
+
|
|
32
|
+
> Ce qui arrive après le `?` de l'URL : le **corps** d'une requête `POST`/`PUT`/`PATCH`/`DELETE`. Selon
|
|
33
|
+
> son `Content-Type`, Nodefony le parse de quatre façons — JSON, formulaire urlencodé, XML, ou
|
|
34
|
+
> **multipart** (les fichiers uploadés) — et le pose là où ton contrôleur le lit. Cette page décrit ce
|
|
35
|
+
> parsing, comment on accède aux **champs** et aux **fichiers**, l'API d'un fichier uploadé
|
|
36
|
+
> (`nom`, `taille`, `mimetype`, `move`), et les **deux budgets de taille** qui protègent le processus
|
|
37
|
+
> d'un corps trop gros. Chaque fait est ancré sur le code.
|
|
38
|
+
|
|
39
|
+
📍 [Documentation](../../../../../docs/index.md) › [@nodefony/http](index.md) › **Upload & corps de requête**
|
|
40
|
+
|
|
41
|
+
## 🧠 Le modèle mental — un corps, quatre parsers, deux budgets
|
|
42
|
+
|
|
43
|
+
Le corps d'une requête n'est **jamais** interprété par le serveur ni par le routeur : c'est
|
|
44
|
+
`HttpRequest` qui, selon le `Content-Type`, choisit **un** parser et remplit deux emplacements que ton
|
|
45
|
+
contrôleur consomme — `queryPost` (les champs) et `queryFile` (les fichiers).
|
|
46
|
+
|
|
47
|
+
```mermaid
|
|
48
|
+
flowchart TD
|
|
49
|
+
REQ["POST / PUT / PATCH / DELETE<br/>+ Content-Type"] --> RT["HttpRequest.parseRequest()"]
|
|
50
|
+
RT -->|"application/json · *+json"| J["ParserJson"]
|
|
51
|
+
RT -->|"x-www-form-urlencoded"| Q["ParserQs"]
|
|
52
|
+
RT -->|"application/xml · text/xml"| X["ParserXml"]
|
|
53
|
+
RT -->|"multipart/form-data"| M["busboy → disque (temp)"]
|
|
54
|
+
RT -->|"autre / brut"| B["Parser brut"]
|
|
55
|
+
J --> QP["queryPost<br/>@Body() · this.queryPost"]
|
|
56
|
+
Q --> QP
|
|
57
|
+
X --> QP
|
|
58
|
+
M --> QF["queryFile<br/>@UploadedFiles() · this.queryFile"]
|
|
59
|
+
M --> QP
|
|
60
|
+
B --> DATA["request.data (Buffer)"]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Trois idées portent tout le reste :
|
|
64
|
+
|
|
65
|
+
1. **Le `Content-Type` décide, pas la méthode.** `parseRequest()` (`context/http/Request.ts:429`)
|
|
66
|
+
aiguille vers `ParserJson` / `ParserQs` / `ParserXml` / **busboy** / parser brut selon l'en-tête.
|
|
67
|
+
2. **Seul le multipart écrit sur disque.** Les fichiers sont **streamés** au fil de l'eau vers un
|
|
68
|
+
fichier temporaire (`parseMultipart()`, `context/http/Request.ts:499`) — jamais bufferisés en RAM.
|
|
69
|
+
JSON / urlencoded / XML restent en mémoire (petits corps).
|
|
70
|
+
3. **Deux budgets distincts, jamais confondus.** Le corps **non-multipart** est borné par `maxBodySize`
|
|
71
|
+
(défaut **1 MiB**) ; le multipart a **ses propres** limites busboy (`maxFileSize`, `maxTotalFileSize`,
|
|
72
|
+
défaut **500 MB**). Régler l'un ne change rien à l'autre — c'est le piège n° 1.
|
|
73
|
+
|
|
74
|
+
## 📖 Lexique
|
|
75
|
+
|
|
76
|
+
| Terme | Sens |
|
|
77
|
+
| --------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
78
|
+
| Corps (body) | Les octets envoyés **après** les en-têtes d'une requête (le contenu d'un `POST`). |
|
|
79
|
+
| `Content-Type` | En-tête déclarant le format du corps (`application/json`, `multipart/form-data`…). Il décide du parser. |
|
|
80
|
+
| `multipart/form-data` | Format d'un formulaire qui contient des **fichiers** : le corps est découpé en _parts_ séparées (RFC 7578). |
|
|
81
|
+
| Boundary | Chaîne-séparateur (`--…`) qui délimite chaque _part_ d'un corps multipart. |
|
|
82
|
+
| Part | Un bloc d'un corps multipart : soit un **champ texte** (`field`), soit un **fichier** (`file`). |
|
|
83
|
+
| Field / File | Champ texte (→ `queryPost`) vs fichier uploadé (→ `queryFile`), les deux issus du même corps multipart. |
|
|
84
|
+
| busboy | La bibliothèque de parsing multipart **en flux** (`@fastify/busboy`) : elle lit le corps sans le charger en RAM. |
|
|
85
|
+
| Streaming (au fil de l'eau) | Écrire le fichier sur disque **pendant** sa réception, octet par octet — au lieu d'attendre le corps entier. |
|
|
86
|
+
| Fichier temporaire (temp) | Le fichier écrit par busboy dans `uploadDir`, nommé par un UUID. Le contrôleur le déplace ou le laisse expirer. |
|
|
87
|
+
| `urlencoded` | `application/x-www-form-urlencoded` : un formulaire simple `a=1&b=2` (pas de fichier). |
|
|
88
|
+
| Payload | Synonyme de corps de requête, surtout quand on parle de sa **taille**. |
|
|
89
|
+
| 413 « Content Too Large » | Le statut HTTP renvoyé quand le corps dépasse une borne (RFC 9110 §15.5.14). |
|
|
90
|
+
| `Content-Length` | En-tête annonçant la taille du corps. Sert au pré-contrôle 413 **avant** de lire. |
|
|
91
|
+
| Chunked | Corps envoyé sans `Content-Length` (`Transfer-Encoding: chunked`) : la taille n'est connue qu'en cours de lecture. |
|
|
92
|
+
| Path traversal | Attaque où un nom de fichier (`../../etc/passwd`) fait écrire **hors** du dossier prévu. |
|
|
93
|
+
| Hash d'intégrité | Empreinte (`sha256`…) calculée pendant le stream, pour vérifier qu'un fichier n'a pas été altéré. |
|
|
94
|
+
| `Readable` | Un flux Node lisible. `@Body({ stream: true })` en injecte un — le corps brut, non parsé. |
|
|
95
|
+
|
|
96
|
+
## Qu'est-ce qu'un upload, ici ?
|
|
97
|
+
|
|
98
|
+
Imagine un **guichet de dépôt**. Le client arrive avec une enveloppe (`Content-Type`) : dedans, soit un
|
|
99
|
+
formulaire à plat (JSON, urlencodé, XML) que le guichetier **recopie sur une fiche** en mémoire, soit un
|
|
100
|
+
**colis** (multipart) qu'il pose **directement dans une consigne** (le disque) sans jamais le tenir à
|
|
101
|
+
bout de bras. Dans les deux cas, ton contrôleur ne voit pas l'enveloppe : il reçoit la fiche
|
|
102
|
+
(`queryPost`) et le bordereau de consigne (`queryFile`).
|
|
103
|
+
|
|
104
|
+
Concrètement, la couche a quatre responsabilités, et rien d'autre :
|
|
105
|
+
|
|
106
|
+
1. **Choisir le parser** d'après le `Content-Type`.
|
|
107
|
+
2. **Décoder les champs** (JSON, urlencoded, XML) → `queryPost`, lus par `@Body()`.
|
|
108
|
+
3. **Streamer les fichiers** multipart vers le disque, sans pic RAM → `queryFile`, lus par
|
|
109
|
+
`@UploadedFiles()`.
|
|
110
|
+
4. **Borner la taille** — rejeter en `413` un corps qui menace la mémoire ou le disque.
|
|
111
|
+
|
|
112
|
+
Le routage, le firewall, le contrôleur viennent après : ils reçoivent un corps déjà parsé et déjà borné.
|
|
113
|
+
|
|
114
|
+
## La vision Nodefony
|
|
115
|
+
|
|
116
|
+
Trois choix structurent l'implémentation, et chacun a une raison de sécurité ou de performance.
|
|
117
|
+
|
|
118
|
+
**Le multipart ne touche jamais la RAM.** Là où l'ancien chemin bufferisait le corps entier, busboy lit
|
|
119
|
+
le flux et écrit chaque fichier au fil de l'eau dans le dossier temporaire (`streamMultipart()`,
|
|
120
|
+
`context/http/Request.ts:537`). Un upload de 1 Go ne coûte donc pas 1 Go de heap — seuls les petits
|
|
121
|
+
champs texte restent en mémoire. C'est ce qui rend un endpoint d'upload public tenable.
|
|
122
|
+
|
|
123
|
+
**Le nom du fichier temporaire n'est jamais celui du client.** Chaque fichier reçu est écrit sous un nom
|
|
124
|
+
`randomUUID()` + extension d'origine (`context/http/Request.ts:594`) : un nom malveillant
|
|
125
|
+
(`../../etc/passwd`) ne peut pas influencer le **chemin** d'écriture. Le nom d'origine est conservé en
|
|
126
|
+
**métadonnée** (`filename`), pas dans le chemin.
|
|
127
|
+
|
|
128
|
+
**Deux budgets, secure-by-default.** Le corps non-multipart est plafonné à **1 MiB** par défaut
|
|
129
|
+
(`maxBodySize`, `http/nodefony/config/config.ts:1005`) — un `POST` JSON géant est rejeté avant d'être bufferisé. Le
|
|
130
|
+
multipart, lui, a ses propres bornes busboy (par fichier, cumul, nombre) qui coupent le flux et
|
|
131
|
+
nettoient les temporaires déjà posés au moindre dépassement (`context/http/Request.ts:481`).
|
|
132
|
+
|
|
133
|
+
> [!IMPORTANT]
|
|
134
|
+
> `@nodefony/http` ne peut pas importer `@nodefony/framework` (cycle). Les **décorateurs**
|
|
135
|
+
> (`@Body`, `@UploadedFiles`…) vivent donc dans `@nodefony/framework`, mais ils lisent les emplacements
|
|
136
|
+
> (`queryPost`, `queryFile`) remplis par `@nodefony/http`. Une page, deux modules — c'est le prix du
|
|
137
|
+
> découplage.
|
|
138
|
+
|
|
139
|
+
## 🚀 Démarrage rapide
|
|
140
|
+
|
|
141
|
+
Dans une application générée par `nodefony create app`, le parsing du corps est **déjà branché** : tu
|
|
142
|
+
n'écris que tes bornes (si les défauts ne conviennent pas) et le contrôleur qui reçoit l'upload.
|
|
143
|
+
|
|
144
|
+
### 1. Les bornes de payload
|
|
145
|
+
|
|
146
|
+
Réglées sur le module `@nodefony/http`, colocalisées dans le manifeste via `use()`. Toutes ces clés
|
|
147
|
+
sont facultatives : ce sont les défauts du schéma, écrits ici pour les rendre visibles et les resserrer.
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
// nodefony.config.ts — bornes de payload de l'application
|
|
151
|
+
export default defineConfig(() => ({
|
|
152
|
+
modules: [
|
|
153
|
+
use("@nodefony/http", {
|
|
154
|
+
// Corps NON-multipart (JSON, urlencoded, XML, brut) : 1 MiB par défaut.
|
|
155
|
+
// Au-delà → 413 « Content Too Large ». 0 = illimité.
|
|
156
|
+
maxBodySize: 1_048_576,
|
|
157
|
+
upload: {
|
|
158
|
+
// Chaque fichier (busboy limits.fileSize) — dépassement → 413.
|
|
159
|
+
maxFileSize: 10 * 1024 * 1024, // 10 MiB
|
|
160
|
+
// Cumul de TOUS les fichiers d'une même requête (compteur Nodefony).
|
|
161
|
+
maxTotalFileSize: 30 * 1024 * 1024, // 30 MiB
|
|
162
|
+
maxFiles: 10, // nombre de fichiers — anti-DoS
|
|
163
|
+
// Intégrité optionnelle : hash calculé PENDANT le stream (0 relecture).
|
|
164
|
+
hashAlgorithm: "sha256",
|
|
165
|
+
},
|
|
166
|
+
}),
|
|
167
|
+
"@nodefony/framework",
|
|
168
|
+
],
|
|
169
|
+
}));
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### 2. Le contrôleur qui reçoit l'upload
|
|
173
|
+
|
|
174
|
+
Le pipeline a déjà streamé les fichiers sur disque et parsé les champs texte. Le contrôleur les lit par
|
|
175
|
+
paramètres décorés — `@UploadedFiles()` pour les fichiers, `@Body("champ")` pour un champ — puis déplace
|
|
176
|
+
chaque fichier vers son emplacement définitif.
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
// nodefony/controller/UploadController.ts — reçoit un upload multipart
|
|
180
|
+
import {
|
|
181
|
+
Controller,
|
|
182
|
+
controller,
|
|
183
|
+
Post,
|
|
184
|
+
Body,
|
|
185
|
+
UploadedFiles,
|
|
186
|
+
} from "@nodefony/framework";
|
|
187
|
+
import type { ContextType, IUploadedFile } from "@nodefony/http";
|
|
188
|
+
import path from "node:path";
|
|
189
|
+
|
|
190
|
+
@controller("/avatars")
|
|
191
|
+
class UploadController extends Controller {
|
|
192
|
+
constructor(context: ContextType) {
|
|
193
|
+
super("UploadController", context);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// multipart/form-data : les fichiers arrivent DÉJÀ écrits en temp (streaming
|
|
197
|
+
// busboy), les champs texte via @Body(). @UploadedFile() donnerait le premier.
|
|
198
|
+
@Post("/")
|
|
199
|
+
async upload(
|
|
200
|
+
@UploadedFiles() files: IUploadedFile[] | undefined,
|
|
201
|
+
@Body("label") label: string | undefined,
|
|
202
|
+
) {
|
|
203
|
+
const saved: Array<{ name: string; size: number }> = [];
|
|
204
|
+
for (const file of files ?? []) {
|
|
205
|
+
// SÉCURITÉ : le temp est nommé par un UUID (jamais le nom client). Pour la
|
|
206
|
+
// DESTINATION, on impose NOTRE nom — jamais file.filename brut (traversal).
|
|
207
|
+
const safeName = `${Date.now()}-${path.basename(file.filename)}`;
|
|
208
|
+
const dest = path.resolve("/var/app/uploads", safeName);
|
|
209
|
+
await file.moveAsync(dest); // variante non bloquante (recommandée)
|
|
210
|
+
saved.push({ name: file.filename, size: file.size });
|
|
211
|
+
}
|
|
212
|
+
return this.renderJson({ label: label ?? null, files: saved });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export default UploadController;
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### 3. Vérifier depuis le terminal
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
# multipart : un champ texte + un fichier, sur le port TLS
|
|
223
|
+
curl -k -F "label=profil" -F "file=@./photo.png" https://127.0.0.1:5152/avatars/
|
|
224
|
+
# {"label":"profil","files":[{"name":"photo.png","size":12345}]}
|
|
225
|
+
|
|
226
|
+
# corps JSON non-multipart > maxBodySize (1 MiB) → rejet avant lecture
|
|
227
|
+
curl -k -X POST -H "content-type: application/json" \
|
|
228
|
+
--data-binary @big.json https://127.0.0.1:5152/avatars/
|
|
229
|
+
# HTTP/2 413 (Request body too large)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## 🏗️ Architecture interne — le trajet d'un corps multipart
|
|
233
|
+
|
|
234
|
+
Une requête multipart traverse `HttpRequest` → busboy → disque → `UploadService`, puis atterrit dans le
|
|
235
|
+
contrôleur avec `queryFile`/`queryPost` déjà remplis.
|
|
236
|
+
|
|
237
|
+
```mermaid
|
|
238
|
+
sequenceDiagram
|
|
239
|
+
participant C as Client
|
|
240
|
+
participant R as HttpRequest
|
|
241
|
+
participant B as busboy
|
|
242
|
+
participant D as disque (temp)
|
|
243
|
+
participant U as UploadService
|
|
244
|
+
participant Ctrl as Controller
|
|
245
|
+
C->>R: POST multipart/form-data
|
|
246
|
+
R->>R: parseRequest() — content-type = multipart
|
|
247
|
+
R->>B: request.pipe(busboy)
|
|
248
|
+
B->>D: chaque fichier → <uuid>.<ext> (au fil de l'eau)
|
|
249
|
+
B-->>R: field (texte) → queryPost
|
|
250
|
+
Note over B,D: limites fileSize / total / files → 413 + cleanup des temp
|
|
251
|
+
B->>R: finish
|
|
252
|
+
R->>U: createUploadFile() → UploadedFile (stat async)
|
|
253
|
+
R->>Ctrl: onRequestEnd — queryFile / queryPost prêts
|
|
254
|
+
Ctrl->>D: file.moveAsync(dest)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Les points d'implémentation qui expliquent des comportements surprenants :
|
|
258
|
+
|
|
259
|
+
1. **L'aiguillage lit le `Content-Type`, pas la méthode** — `parseRequest()`
|
|
260
|
+
(`context/http/Request.ts:292`). `PATCH` porte un corps comme `POST`/`PUT` : il figure dans la table
|
|
261
|
+
des méthodes parsées (`context/http/Request.ts:63`) — l'oubli laissait tout `PATCH` avec un corps vide.
|
|
262
|
+
2. **Le multipart draine sur un `finish`, après flush de tous les writes** — `streamMultipart()`
|
|
263
|
+
(`context/http/Request.ts:537`) accumule les `Promise` d'écriture disque et ne résout `{ fields, files }`
|
|
264
|
+
qu'une fois tous les fichiers fermés (`context/http/Request.ts:538`).
|
|
265
|
+
3. **Une limite dépassée nettoie les temporaires déjà posés** — `abort()`
|
|
266
|
+
(`context/http/Request.ts:557`) délie le flux, détruit les write-streams ouverts et `unlink` les temp
|
|
267
|
+
déjà écrits (`context/http/Request.ts:568`) avant de rejeter en `413` : pas d'orphelins sur le disque.
|
|
268
|
+
4. **Les autres formats drainent AVANT de concaténer** — la base `Parser.parse()` attend `end`
|
|
269
|
+
(`context/http/parser.ts:111`) avant `Buffer.concat` : sans ce drain, `ParserQs`/`ParserXml`
|
|
270
|
+
lisaient un corps partiel → `queryPost` vide (bug de régression, cf tests).
|
|
271
|
+
5. **Un multipart sans boundary exploitable ne crashe pas** — `new Busboy()` lève **synchroniquement** ;
|
|
272
|
+
c'est rattrapé et on bascule sur le `Parser` brut (`context/http/Request.ts:375`).
|
|
273
|
+
|
|
274
|
+
## ⚙️ Configuration — deux budgets, jamais confondus
|
|
275
|
+
|
|
276
|
+
C'est la distinction la plus utile de cette page. **Le multipart n'écoute pas `maxBodySize`** ; le
|
|
277
|
+
non-multipart n'écoute pas `upload.*`.
|
|
278
|
+
|
|
279
|
+
### Corps non-multipart — `maxBodySize`
|
|
280
|
+
|
|
281
|
+
| Option | Type | Défaut | Effet |
|
|
282
|
+
| ------------- | ------ | ------------------- | ------------------------------------------------------------------------------ |
|
|
283
|
+
| `maxBodySize` | octets | `1_048_576` (1 MiB) | Plafond d'un corps **JSON / urlencoded / XML / brut** → `413`. `0` = illimité. |
|
|
284
|
+
|
|
285
|
+
Deux rideaux, tous deux `runtimeMutable` (éditable à chaud) : un **pré-check** sur `Content-Length`
|
|
286
|
+
qui rejette **avant** de lire (`enforceBodyLimit()`, `context/http/Request.ts:296`), puis un **compteur
|
|
287
|
+
en streaming** qui coupe le socket si le corps déborde sans `Content-Length` honnête — chunked ou
|
|
288
|
+
menteur (`Parser.write()`, `context/http/parser.ts:33`, dépassement `context/http/parser.ts:44`).
|
|
289
|
+
Champ `maxBodySize` du schéma : `config/config.ts:1005`.
|
|
290
|
+
|
|
291
|
+
### Fichiers multipart — `upload.*`
|
|
292
|
+
|
|
293
|
+
Table dérivée de `uploadSchema` (`config/config.ts:144`). Toutes les bornes sont `runtimeMutable`.
|
|
294
|
+
|
|
295
|
+
| Option | Type | Défaut | Effet |
|
|
296
|
+
| ------------------ | -------------------------------- | ---------------------- | --------------------------------------------------------------------------------------------------- |
|
|
297
|
+
| `uploadDir` | chemin | `""` → `kernel.tmpDir` | Dossier de dépôt des temporaires. Vide = temp du kernel (`config/config.ts:146`). |
|
|
298
|
+
| `maxFileSize` | octets | `524_288_000` (500 MB) | Taille max d'**UN** fichier (busboy `limits.fileSize`) → `413` (`config/config.ts:155`). |
|
|
299
|
+
| `maxTotalFileSize` | octets | `524_288_000` (500 MB) | Taille **CUMULÉE** des fichiers d'une requête (compteur Nodefony) → `413` (`config/config.ts:167`). |
|
|
300
|
+
| `maxFiles` | entier | `1000` | Nombre max de fichiers (busboy `limits.files`) → `413` (`config/config.ts:179`). |
|
|
301
|
+
| `maxFields` | entier | `1000` | Nombre max de champs texte (busboy `limits.fields`) → `413` (`config/config.ts:190`). |
|
|
302
|
+
| `maxFieldsSize` | octets | `2_097_152` (2 MB) | Taille max d'un champ texte (busboy `limits.fieldSize`) (`config/config.ts:201`). |
|
|
303
|
+
| `hashAlgorithm` | `false` \| `sha256`/`sha1`/`md5` | `false` | Hash calculé pendant le stream (intégrité) (`config/config.ts:212`). |
|
|
304
|
+
| `encoding` | chaîne | `"utf-8"` | Encodage par défaut des champs texte (busboy `defCharset`) (`config/config.ts:219`). |
|
|
305
|
+
|
|
306
|
+
> [!WARNING]
|
|
307
|
+
> `uploadDir` vide est **résolu au boot** sur le répertoire temporaire du kernel par le builder de config
|
|
308
|
+
> (`defineModuleConfig.ts:43`) : un défaut vide n'est jamais un chemin vide en runtime. Ne dérérérence
|
|
309
|
+
> **jamais** le kernel au top-level d'un `config.ts` — c'est la raison du champ marqué `kernelDerived`.
|
|
310
|
+
|
|
311
|
+
## 🧰 API — accéder aux champs et aux fichiers
|
|
312
|
+
|
|
313
|
+
Depuis un contrôleur, deux surfaces équivalentes : les **décorateurs de paramètre** (déclaratif,
|
|
314
|
+
recommandé) et les **getters** de `Controller` (impératif). Les signatures exactes vivent dans
|
|
315
|
+
`.ai/symbols.json` — jamais recopiées ici.
|
|
316
|
+
|
|
317
|
+
| Accès | Source lue | Ancrage |
|
|
318
|
+
| -------------------------------------------------- | ------------------------------------ | ------------------------------------------------------------------- |
|
|
319
|
+
| `@UploadedFiles() f: IUploadedFile[]` | tous les fichiers (`queryFile`) | `resolveParamArg` `"files"` (`routerDecorators.ts:1240`) |
|
|
320
|
+
| `@UploadedFile() f: IUploadedFile` | le **premier** fichier | `resolveParamArg` `"file"` (`routerDecorators.ts:1239`) |
|
|
321
|
+
| `@Body() body` | tous les champs parsés (`queryPost`) | `resolveParamArg` `"body"` (`routerDecorators.ts:1178`) |
|
|
322
|
+
| `@Body("label") v` | un seul champ du body | même source, clé (`routerDecorators.ts:1178`) |
|
|
323
|
+
| `@Body({ stream: true }) s: NodeJS.ReadableStream` | le **flux brut**, parse **sauté** | `resolveParamArg` stream (`routerDecorators.ts:1227`) |
|
|
324
|
+
| `this.queryFile` | équivalent getter des fichiers | `Controller.queryFile` (`framework/nodefony/src/Controller.ts:205`) |
|
|
325
|
+
| `this.queryPost` | équivalent getter des champs | `Controller.queryPost` (`framework/nodefony/src/Controller.ts:214`) |
|
|
326
|
+
|
|
327
|
+
Les décorateurs `@UploadedFile` / `@UploadedFiles` sont des fabriques de paramètre
|
|
328
|
+
(`routerDecorators.ts:1240`), exportées par `@nodefony/framework` ; leurs interfaces `IUploadedFile` /
|
|
329
|
+
`IParsedUploadFile` viennent de `@nodefony/http` (`interfaces/IUpload.ts:49`, `interfaces/IUpload.ts:7`).
|
|
330
|
+
|
|
331
|
+
### Un fichier uploadé — `UploadedFile`
|
|
332
|
+
|
|
333
|
+
Chaque entrée de `queryFile` est un `UploadedFile` (`service/upload/upload-service.ts:96`), déjà écrit
|
|
334
|
+
sur disque. Ses membres utiles :
|
|
335
|
+
|
|
336
|
+
| Membre | Ce qu'il donne | Ancrage |
|
|
337
|
+
| ------------------------ | ----------------------------------------------------------------------- | ---------------------------------------------------------- |
|
|
338
|
+
| `filename` | Nom **d'origine** déclaré par le client (métadonnée, jamais le chemin). | `realName()` (`service/upload/upload-service.ts:147`) |
|
|
339
|
+
| `size` | Taille réellement écrite (octets). | `getSize()` (`service/upload/upload-service.ts:139`) |
|
|
340
|
+
| `prettySize` | Taille lisible (`« 1.2 MB »`). | `getPrettySize()` (`service/upload/upload-service.ts:143`) |
|
|
341
|
+
| `mimeType` | Type MIME déclaré (`image/png`…), sinon deviné de l'extension. | `getMimeType()` (`service/upload/upload-service.ts:153`) |
|
|
342
|
+
| `hash` / `hashAlgorithm` | Empreinte d'intégrité si `upload.hashAlgorithm` est réglé. | `interfaces/IUpload.ts:49` |
|
|
343
|
+
| `moveAsync(target)` | Déplace le temp — **non bloquant, recommandé** dans le pipeline. | `moveAsync()` (`service/upload/upload-service.ts:193`) |
|
|
344
|
+
| `move(target)` | Variante **synchrone** (compat) — bloque l'event-loop. | `move()` (`service/upload/upload-service.ts:160`) |
|
|
345
|
+
|
|
346
|
+
`move`/`moveAsync` acceptent un **fichier cible** ou un **dossier existant** : sur un dossier, la
|
|
347
|
+
destination est bâtie avec `filename` — d'où l'avertissement de sécurité ci-dessous.
|
|
348
|
+
|
|
349
|
+
### Gros upload sans pic mémoire — `@Body({ stream: true })`
|
|
350
|
+
|
|
351
|
+
Pour piper directement un très gros corps (vidéo, backup) vers le disque ou S3 sans passer par busboy ni
|
|
352
|
+
par la RAM, `@Body({ stream: true })` court-circuite le parse et injecte l'`IncomingMessage` brut (un
|
|
353
|
+
`Readable`). Le pipeline sait le sauter en amont via `routeExpectsBodyStream()`
|
|
354
|
+
(`routerDecorators.ts:1365`), mémoïsé sur la route.
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
// fragment — le contrôleur pipe le flux lui-même (0 parse, 0 pic RAM)
|
|
358
|
+
@Post("/backup")
|
|
359
|
+
async backup(@Body({ stream: true }) body: NodeJS.ReadableStream) {
|
|
360
|
+
await pipeline(body, createWriteStream("/var/app/backup.tar")); // node:stream/promises
|
|
361
|
+
return this.renderJson({ ok: true });
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
## 🔐 Sécurité
|
|
366
|
+
|
|
367
|
+
L'upload est une surface d'attaque classique : nom de fichier hostile, saturation disque/RAM, contenu
|
|
368
|
+
piégé. Les défenses en place, et **ce qui reste à ta charge**.
|
|
369
|
+
|
|
370
|
+
<!-- prettier-ignore -->
|
|
371
|
+
| Menace | Défense côté framework | À ta charge |
|
|
372
|
+
| --- | --- | --- |
|
|
373
|
+
| **Path traversal** (chemin d'écriture) | Le temp est nommé `randomUUID()` + extension — jamais le nom client (`context/http/Request.ts:594`). | La **destination** de `move()` (voir avertissement). |
|
|
374
|
+
| **Saturation RAM** | Multipart streamé (jamais bufferisé) ; corps non-multipart borné (`maxBodySize`). | Resserrer `maxBodySize` selon l'endpoint. |
|
|
375
|
+
| **Saturation disque** | `maxFileSize` + `maxTotalFileSize` + `maxFiles` ; `abort()` nettoie les temp à l'abandon (`context/http/Request.ts:557`). | Purger les temp non déplacés (TTL / cron). |
|
|
376
|
+
| **DoS par quantité** | `maxFields` / `maxFiles` / `parts` → `413` (`context/http/Request.ts:610`). | — |
|
|
377
|
+
| **Type de fichier hostile** | `mimeType` **déclaré** est exposé tel quel. | Valider le type/contenu réel (le MIME client est déclaratif). |
|
|
378
|
+
|
|
379
|
+
> [!WARNING]
|
|
380
|
+
> **Path traversal sur la destination.** `move(dir)` / `moveAsync(dir)` vers un **dossier** construit le
|
|
381
|
+
> chemin final avec `file.filename` — le nom **client** (`service/upload/upload-service.ts:197`). Un nom
|
|
382
|
+
> `../../etc/cron.d/x` s'échapperait du dossier. Le framework protège le chemin du **temporaire**, pas
|
|
383
|
+
> celui que **tu** choisis. Règle : passe une **cible complète** que tu contrôles, ou assainis toujours
|
|
384
|
+
> avec `path.basename(file.filename)` — exactement le `safeName` du Démarrage rapide.
|
|
385
|
+
|
|
386
|
+
## ⚡ Performance & mémoire
|
|
387
|
+
|
|
388
|
+
Le parsing du corps est sur le chemin de chaque requête écrivante — les choix visibles dans le code :
|
|
389
|
+
|
|
390
|
+
- **Multipart en flux pur** — plus de double-bufferisation : busboy écrit sur disque au fil de l'eau,
|
|
391
|
+
seuls les champs texte restent en mémoire (`context/http/Request.ts:400`).
|
|
392
|
+
- **Rejet AVANT lecture** — le pré-check `Content-Length` renvoie `413` sans lire un octet
|
|
393
|
+
(`context/http/Request.ts:400`) ; le compteur streaming **libère immédiatement** la RAM déjà
|
|
394
|
+
bufferisée au dépassement (`context/http/parser.ts:43`).
|
|
395
|
+
- **`stat` non bloquant** — `UploadedFile.create()` résout les stats du fichier en async
|
|
396
|
+
(`service/upload/upload-service.ts:130`), plus de `lstatSync` par fichier uploadé.
|
|
397
|
+
- **Listeners jumeaux nettoyés** — le drain de fin de corps retire ses écouteurs `end`/`error`/overflow
|
|
398
|
+
à la main (`once` n'auto-détache que celui qui fire) (`context/http/parser.ts:88`).
|
|
399
|
+
- **Hash opt-in** — `hashAlgorithm: false` par défaut : zéro coût CPU tant que l'intégrité n'est pas
|
|
400
|
+
demandée.
|
|
401
|
+
|
|
402
|
+
Rejouer une charge d'upload : skill `nodefony-load-test`. Gate mémoire avant tout commit touchant le
|
|
403
|
+
pipeline : `npm run test:memory` (skill `nodefony-check-memory-health`).
|
|
404
|
+
|
|
405
|
+
## 📜 Normes appliquées
|
|
406
|
+
|
|
407
|
+
| Domaine | Norme | Ancrage |
|
|
408
|
+
| ---------------------------------- | -------------------------------- | ------------------------------------------------------------- |
|
|
409
|
+
| Formulaire avec fichiers | RFC 7578 (`multipart/form-data`) | `parseMultipart()` via busboy (`context/http/Request.ts:499`) |
|
|
410
|
+
| Corps trop gros → 413 | RFC 9110 §15.5.14 | `enforceBodyLimit()` (`context/http/Request.ts:407`) |
|
|
411
|
+
| 413 en streaming (chunked/menteur) | RFC 9110 §15.5.14 | `Parser.write()` (`context/http/parser.ts:33`) |
|
|
412
|
+
| Bornes multipart → 413 | RFC 9110 §15.5.14 | `stream.on("limit")` (`context/http/Request.ts:481`) |
|
|
413
|
+
| Défense path traversal (nom temp) | OWASP — File Upload | `randomUUID()` (`context/http/Request.ts:594`) |
|
|
414
|
+
| Charset du corps honoré | RFC 9110 (Content-Type) | `getCharset()` (`context/http/Request.ts:799`) |
|
|
415
|
+
|
|
416
|
+
## ⚠️ Pièges
|
|
417
|
+
|
|
418
|
+
| Symptôme | Cause | Correction |
|
|
419
|
+
| ------------------------------------------------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
420
|
+
| `@Body()` est vide sur un upload de fichier | Les fichiers vont dans `queryFile`, **pas** dans `queryPost` | Lire les fichiers avec `@UploadedFiles()` / `this.queryFile` ; `@Body()` = champs texte |
|
|
421
|
+
| Un `POST` JSON de 1,5 Mo est refusé en `413` | `maxBodySize` vaut **1 MiB** par défaut | Augmenter `maxBodySize` (pas `upload.maxFileSize`) |
|
|
422
|
+
| Un gros fichier passe alors que `maxBodySize` est petit | Le multipart **n'écoute pas** `maxBodySize` | Régler `upload.maxFileSize` / `maxTotalFileSize` |
|
|
423
|
+
| `413` sur upload sans message clair | Une borne busboy atteinte en streaming (fichier, cumul, nombre) | Vérifier les bornes `upload.*` — 413 émis sur `stream.on("limit")` (`context/http/Request.ts:481`) |
|
|
424
|
+
| Un fichier écrit `../../etc/…` après un `move` | `move(dossier)` utilise le nom **client** (`upload-service.ts:197`) | Passer une cible complète, ou `path.basename(file.filename)` |
|
|
425
|
+
| Des fichiers temporaires s'accumulent dans `uploadDir` | Le contrôleur ne déplace jamais le temp | Appeler `moveAsync()` (ou purger l'ancien temp par TTL) |
|
|
426
|
+
| `queryPost` vide sur `PATCH` | Déjà géré : `PATCH` est dans la table des méthodes parsées | Aucune — corps `PATCH` parsé comme `POST` (`context/http/Request.ts:63`) |
|
|
427
|
+
| Corps `latin1` mal décodé | Déjà géré : le `charset=` du `Content-Type` est honoré | Aucune — `getCharset()` normalise (`context/http/Request.ts:799`) |
|
|
428
|
+
| `multipart` sans boundary fait planter | `new Busboy()` throw synchrone | Déjà géré : bascule sur le parser brut (`context/http/Request.ts:502`) |
|
|
429
|
+
|
|
430
|
+
## 🧪 Tests & couverture
|
|
431
|
+
|
|
432
|
+
Les chiffres exacts vivent dans la carte de tests de cette page (régénérée depuis vitest, jamais figés
|
|
433
|
+
dans le Markdown).
|
|
434
|
+
|
|
435
|
+
| Type | Où |
|
|
436
|
+
| --------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
437
|
+
| Unitaires — `UploadedFile` | `unit/UploadedFile.test.ts` — `create()` async, `move`/`moveAsync`, `mimeType`, `realName` |
|
|
438
|
+
| Unitaires — `@Body` stream | `framework` `unit/BodyStream.test.ts` — `@Body({ stream })`, `resolveParamArg`, `routeExpectsBodyStream` |
|
|
439
|
+
| Intégration — multipart | `http/upload.test.ts` — upload simple, multi-fichiers, mime non-octet, champ + fichier, `413` par fichier et cumulé |
|
|
440
|
+
| Intégration — content-types | `http/body-content-types.test.ts` — JSON, urlencoded, `@Body("champ")`, XML → `queryPost` |
|
|
441
|
+
| Intégration — bornes | `http/body-limit.test.ts` — sous la limite `200`, `Content-Length` > 1 MiB → `413`, chunked > 1 MiB refusé |
|
|
442
|
+
| Intégration — flux brut | `integration/bodyStream.test.ts` — `@Body({ stream })` bout-en-bout (TLS + clair), non-régression `@Body()` |
|
|
443
|
+
|
|
444
|
+
Ce qui **manque** aujourd'hui : pas de banc de **charge** dédié à l'upload (le streaming multipart n'est
|
|
445
|
+
exercé que fonctionnellement), pas de test **d'attaque** (`*.attack.test.ts`) sur le path traversal de
|
|
446
|
+
`move()` dans un dossier, et les bornes `maxFiles` / `maxFields` / `maxFieldsSize` ne sont pas couvertes
|
|
447
|
+
individuellement (seules `maxFileSize` et `maxTotalFileSize` le sont).
|
|
448
|
+
|
|
449
|
+
Suites : `npm test` (unitaires), `npm run test:integration` (serveur requis). Couverture :
|
|
450
|
+
`npm run coverage` dans `@nodefony/http` — le pourcentage vit dans le rapport vitest, jamais figé ici.
|
|
451
|
+
Skills associés : `nodefony-load-test`, `nodefony-check-memory-health`, `nodefony-security-review`.
|
|
452
|
+
|
|
453
|
+
## 🔗 Pour aller plus loin
|
|
454
|
+
|
|
455
|
+
- ⬆️ **Retour au hub** : [@nodefony/http — vue du module](index.md) · [Toute la documentation](../../../../../docs/index.md)
|
|
456
|
+
- 🧭 **Pages sœurs** : [Serveurs](servers.md) · [Sessions](session.md)
|
|
457
|
+
- Bornes de payload globales et transport → [Serveurs § Bornes de payload](servers.md#bornes-de-payload)
|
|
458
|
+
- Le pipeline qui reçoit ce corps parsé → [pipeline-requete](../../../../../docs/architecture/pipeline-requete.md)
|
|
459
|
+
- Routage, contrôleurs et décorateurs `@Body`/`@UploadedFiles` → [@nodefony/framework](../../framework/docs/index.md)
|
|
460
|
+
- Configuration d'application (`defineConfig`, `use`) → [configuration](../../../../../docs/guides/configuration.md)
|
package/package.json
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nodefony/http",
|
|
3
|
+
"version": "10.0.0-alpha.1",
|
|
4
|
+
"description": "Serveurs HTTP, HTTPS, HTTP/2 et WebSocket natifs pour Nodefony : sessions, contextes de requête, certificats TLS",
|
|
5
|
+
"author": "Christophe CAMENSULI <ccamensuli@gmail.com>",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"types": "./dist/types/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/types/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "rimraf dist && rolldown -c rolldown.config.ts && tsgo -p tsconfig.declarations.json",
|
|
18
|
+
"dev": "rolldown -c rolldown.config.ts --watch",
|
|
19
|
+
"clean": "rimraf dist",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
22
|
+
"test:load": "vitest run --config vitest.load.config.ts",
|
|
23
|
+
"test:memory": "vitest run --config vitest.load.config.ts nodefony/tests/http/memory.test.ts",
|
|
24
|
+
"coverage": "vitest run --coverage",
|
|
25
|
+
"typecheck": "tsgo --noEmit -p tsconfig.json && tsgo --noEmit -p tsconfig.tests.json"
|
|
26
|
+
},
|
|
27
|
+
"private": false,
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=24.0.0"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"nodefony",
|
|
33
|
+
"http",
|
|
34
|
+
"http2",
|
|
35
|
+
"websocket",
|
|
36
|
+
"https",
|
|
37
|
+
"tls",
|
|
38
|
+
"session",
|
|
39
|
+
"typescript",
|
|
40
|
+
"esm",
|
|
41
|
+
"nodejs"
|
|
42
|
+
],
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/nodefony/nodefony-core.git",
|
|
46
|
+
"directory": "src/packages/@nodefony/http"
|
|
47
|
+
},
|
|
48
|
+
"directories": {
|
|
49
|
+
"lib": "./nodefony",
|
|
50
|
+
"test": "./nodefony/tests"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@fastify/busboy": "3.2.2",
|
|
54
|
+
"cookie": "2.0.1",
|
|
55
|
+
"http-terminator": "3.2.0",
|
|
56
|
+
"mime-types": "3.0.2",
|
|
57
|
+
"ms": "^2.1.3",
|
|
58
|
+
"node-forge": "1.4.0",
|
|
59
|
+
"qs": "^6.15.3",
|
|
60
|
+
"serve-static": "2.2.1",
|
|
61
|
+
"tslib": "2.8.1",
|
|
62
|
+
"ws": "8.21.3",
|
|
63
|
+
"xml2js": "0.6.2"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@types/chai": "5.2.3",
|
|
67
|
+
"@types/mime-types": "3.0.1",
|
|
68
|
+
"@types/ms": "2.1.0",
|
|
69
|
+
"@types/node": "26.4.1",
|
|
70
|
+
"@types/node-forge": "1.3.14",
|
|
71
|
+
"@types/qs": "6.15.1",
|
|
72
|
+
"@types/serve-static": "2.2.0",
|
|
73
|
+
"@types/websocket": "1.0.10",
|
|
74
|
+
"@types/ws": "8.18.1",
|
|
75
|
+
"@types/xml2js": "0.4.14",
|
|
76
|
+
"@vitest/coverage-v8": "5.0.0",
|
|
77
|
+
"chai": "6.2.2",
|
|
78
|
+
"nodefony": "*",
|
|
79
|
+
"rimraf": "6.1.3",
|
|
80
|
+
"tsx": "4.23.13",
|
|
81
|
+
"vitest": "5.0.0"
|
|
82
|
+
},
|
|
83
|
+
"license": "CECILL-B",
|
|
84
|
+
"readmeFilename": "README.md",
|
|
85
|
+
"contributors": [],
|
|
86
|
+
"peerDependencies": {
|
|
87
|
+
"nodefony": "*",
|
|
88
|
+
"zod": "^4.4.3"
|
|
89
|
+
},
|
|
90
|
+
"files": [
|
|
91
|
+
"dist",
|
|
92
|
+
"docs"
|
|
93
|
+
],
|
|
94
|
+
"publishConfig": {
|
|
95
|
+
"access": "public"
|
|
96
|
+
},
|
|
97
|
+
"homepage": "https://nodefony.github.io/nodefony-core/",
|
|
98
|
+
"bugs": {
|
|
99
|
+
"url": "https://github.com/nodefony/nodefony-core/issues"
|
|
100
|
+
}
|
|
101
|
+
}
|