@nodefony/framework 10.0.0-alpha.7 → 10.0.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/index.js +5 -4
- package/dist/nodefony/config/config.js +2 -1
- package/dist/nodefony/controller/AdminApiController.js +24 -4
- package/dist/nodefony/controller/ApiKeyController.js +2 -1
- package/dist/nodefony/controller/TotpController.js +2 -1
- package/dist/nodefony/decorators/routerDecorators.js +4 -1
- package/dist/nodefony/service/AdminBroker.js +4 -3
- package/dist/nodefony/service/IdempotencyStore.js +2 -2
- package/dist/nodefony/service/router.js +3 -2
- package/dist/nodefony/src/KernelAdminApi.js +12 -7
- package/dist/nodefony/src/Resolver.js +61 -0
- package/dist/nodefony/src/Route.js +9 -2
- package/dist/types/nodefony/config/config.d.ts +1 -0
- package/dist/types/nodefony/controller/AdminApiController.d.ts +17 -1
- package/dist/types/nodefony/decorators/routerDecorators.d.ts +14 -0
- package/dist/types/nodefony/src/Resolver.d.ts +35 -10
- package/dist/types/nodefony/src/Route.d.ts +28 -0
- package/docs/admin.md +25 -25
- package/docs/controller.md +44 -44
- package/docs/decorateurs.md +79 -79
- package/docs/idempotence.md +70 -65
- package/docs/routing.md +40 -40
- package/docs/templates.md +7 -7
- package/package.json +10 -10
package/docs/controller.md
CHANGED
|
@@ -45,11 +45,11 @@ flowchart LR
|
|
|
45
45
|
|
|
46
46
|
Trois idées à retenir :
|
|
47
47
|
|
|
48
|
-
1. **Tu hérites de `Service`** — `Controller` étend `Service` (`Controller.ts:
|
|
48
|
+
1. **Tu hérites de `Service`** — `Controller` étend `Service` (`Controller.ts:146`). Tu récupères
|
|
49
49
|
donc gratuitement le container (`this.get()`), les logs (`this.log()`) et les événements.
|
|
50
50
|
2. **Tu ne construis rien toi-même** — le `Resolver` instancie ta classe via l'injecteur
|
|
51
|
-
(`Resolver.newController()`, `Resolver.ts:
|
|
52
|
-
3. **Ton `return` EST la réponse** — `Resolver.returnController()` (`Resolver.ts:
|
|
51
|
+
(`Resolver.newController()`, `Resolver.ts:254`), jamais un `new` direct.
|
|
52
|
+
3. **Ton `return` EST la réponse** — `Resolver.returnController()` (`Resolver.ts:827`) traduit la
|
|
53
53
|
valeur retournée : objet → JSON, string → corps brut, `void` → « j'ai répondu moi-même ».
|
|
54
54
|
|
|
55
55
|
## 📖 Lexique
|
|
@@ -196,14 +196,14 @@ Le tableau ci-dessous donne la séquence exacte, avec l'ancre qui la prouve :
|
|
|
196
196
|
| # | Étape | Où |
|
|
197
197
|
| --- | --------------------------------------- | --------------------------------------------------- |
|
|
198
198
|
| 1 | Appariement de la route | `router.resolve()` (`http-kernel.ts:1324`) |
|
|
199
|
-
| 2 | En-têtes de sécurité applicatifs | `applySecurityHeaders()` (`http-kernel.ts:
|
|
199
|
+
| 2 | En-têtes de sécurité applicatifs | `applySecurityHeaders()` (`http-kernel.ts:1381`) |
|
|
200
200
|
| 3 | Parse du corps (sauf `@Body({stream})`) | `http-kernel.ts:1316` |
|
|
201
201
|
| 4 | Armement de la route (sans instance) | `prepareFrontController()` (`http-kernel.ts:767`) |
|
|
202
|
-
| 5 | CSRF | `firewall.enforceCsrf()` (`http-kernel.ts:
|
|
203
|
-
| 6 | Session (reprise ou ouverture) | `HttpKernel.startSession()` (`http-kernel.ts:
|
|
204
|
-
| 7 | Firewall — **authentification** | `firewall.handleSecurity()` (`http-kernel.ts:
|
|
202
|
+
| 5 | CSRF | `firewall.enforceCsrf()` (`http-kernel.ts:1479`) |
|
|
203
|
+
| 6 | Session (reprise ou ouverture) | `HttpKernel.startSession()` (`http-kernel.ts:1152`) |
|
|
204
|
+
| 7 | Firewall — **authentification** | `firewall.handleSecurity()` (`http-kernel.ts:1490`) |
|
|
205
205
|
| 8 | Autorisation `@IsGranted` | `Resolver.executeAction()` (`Resolver.ts:334`) |
|
|
206
|
-
| 9 | **Instanciation DI + `initialize()`** | `Resolver.executeAction()` (`Resolver.ts:
|
|
206
|
+
| 9 | **Instanciation DI + `initialize()`** | `Resolver.executeAction()` (`Resolver.ts:335`) |
|
|
207
207
|
| 10 | **Ton action** | `controller[methodKey]()` (`Resolver.ts:382`) |
|
|
208
208
|
|
|
209
209
|
> [!IMPORTANT]
|
|
@@ -257,7 +257,7 @@ Si ton `initialize()` lève, l'exception remonte le pipeline et sort en réponse
|
|
|
257
257
|
Chaque étape est chronométrée sous le nom d'une **phase**, lisible dans la debug bar et le profileur :
|
|
258
258
|
`resolve` · `initialize` (DI + ton hook) · `parse` · `firewall` · `action` · `render` · `send`.
|
|
259
259
|
La phase `initialize` existe précisément pour que le temps passé dans ton hook et dans la résolution
|
|
260
|
-
DI **soit imputé à quelqu'un** au lieu de disparaître dans le bloc `action` (`Resolver.ts:
|
|
260
|
+
DI **soit imputé à quelqu'un** au lieu de disparaître dans le bloc `action` (`Resolver.ts:100`).
|
|
261
261
|
|
|
262
262
|
## 🔌 HTTP et WebSocket — le même contrôleur
|
|
263
263
|
|
|
@@ -300,7 +300,7 @@ class ChatController extends Controller {
|
|
|
300
300
|
| `initialize()` | À chaque requête | **Une seule fois**, au handshake |
|
|
301
301
|
|
|
302
302
|
La réutilisation de l'instance vient du cache posé sur le container du contexte
|
|
303
|
-
(`Resolver.newController()`, `Resolver.ts:
|
|
303
|
+
(`Resolver.newController()`, `Resolver.ts:254`) : le contexte WS étant partagé par la connexion, le
|
|
304
304
|
contrôleur l'est aussi. Un garde-fou vérifie que l'instance cachée est bien de la classe de la route
|
|
305
305
|
courante et la reconstruit sinon (`Resolver.ts:344-347`) — sans quoi un message invoquant une autre
|
|
306
306
|
action se tromperait d'objet.
|
|
@@ -310,9 +310,9 @@ action se tromperait d'objet.
|
|
|
310
310
|
> frame 2 — pratique pour un état de conversation, piège si tu comptais sur une instance neuve. En
|
|
311
311
|
> HTTP, l'inverse : chaque requête repart d'une instance vierge.
|
|
312
312
|
|
|
313
|
-
Côté WebSocket, l'ordre est encore plus marqué : `HttpKernel.onConnect()` (`http-kernel.ts:
|
|
313
|
+
Côté WebSocket, l'ordre est encore plus marqué : `HttpKernel.onConnect()` (`http-kernel.ts:1711`)
|
|
314
314
|
appelle `handleFrontController()` (donc `initialize()`) **avant** `startSession()`
|
|
315
|
-
(`http-kernel.ts:
|
|
315
|
+
(`http-kernel.ts:1152`), avant l'acceptation de la socket, et avant le firewall
|
|
316
316
|
(`http-kernel.ts:1457`).
|
|
317
317
|
|
|
318
318
|
## 🧠 D'où viennent `request`, `response`, `session`
|
|
@@ -322,16 +322,16 @@ sont des accesseurs qui dérivent du contexte **vivant**, selon le motif `champ
|
|
|
322
322
|
|
|
323
323
|
| Raccourci | Ce que tu obtiens | Ancre |
|
|
324
324
|
| ---------------- | -------------------------------------------------- | ------------------- |
|
|
325
|
-
| `this.context` | Le contexte transport de la requête courante | `Controller.ts:
|
|
325
|
+
| `this.context` | Le contexte transport de la requête courante | `Controller.ts:180` |
|
|
326
326
|
| `this.route` | La route matchée | `Controller.ts:158` |
|
|
327
327
|
| `this.request` | La requête (HTTP, HTTP/2 ou WS) | `Controller.ts:162` |
|
|
328
|
-
| `this.response` | La réponse du transport | `Controller.ts:
|
|
328
|
+
| `this.response` | La réponse du transport | `Controller.ts:203` |
|
|
329
329
|
| `this.method` | La méthode HTTP (ou `WEBSOCKET`) | `Controller.ts:212` |
|
|
330
330
|
| `this.queryGet` | Les paramètres de la query string | `Controller.ts:221` |
|
|
331
331
|
| `this.queryPost` | Le corps parsé | `Controller.ts:248` |
|
|
332
332
|
| `this.body` | Le corps parsé — alias de `queryPost` | `Controller.ts:248` |
|
|
333
333
|
| `this.queryFile` | Les fichiers uploadés | `Controller.ts:239` |
|
|
334
|
-
| `this.session` | La session **ou `null`** si elle n'est pas activée | `Controller.ts:
|
|
334
|
+
| `this.session` | La session **ou `null`** si elle n'est pas activée | `Controller.ts:279` |
|
|
335
335
|
|
|
336
336
|
Pourquoi des accesseurs plutôt que des champs recopiés au constructeur : **la fraîcheur et le coût**.
|
|
337
337
|
Une valeur recopiée vieillit dès que le pipeline modifie le contexte, et recopier quatre structures
|
|
@@ -340,7 +340,7 @@ gratuitement.
|
|
|
340
340
|
|
|
341
341
|
### La session est **lazy** — elle n'existe que si tu la demandes
|
|
342
342
|
|
|
343
|
-
`this.session` est un simple getter sur `context.session` (`Controller.ts:
|
|
343
|
+
`this.session` est un simple getter sur `context.session` (`Controller.ts:279`). Il n'y a **pas** de
|
|
344
344
|
`startSession()` à appeler depuis un contrôleur : l'activation se déclare sur la route, avec
|
|
345
345
|
`@UseSession()` (ou un paramètre `@Session()`, qui vaut déclaration implicite), et le pipeline
|
|
346
346
|
l'exécute à son point unique. Sans déclaration et sans cookie de session entrant, **aucune session
|
|
@@ -387,26 +387,26 @@ Ce que ça change, concrètement :
|
|
|
387
387
|
|
|
388
388
|
## 🧰 Répondre — ce que ton `return` déclenche
|
|
389
389
|
|
|
390
|
-
Le traducteur unique est `Resolver.returnController()` (`Resolver.ts:
|
|
390
|
+
Le traducteur unique est `Resolver.returnController()` (`Resolver.ts:827`). Il regarde le **type**
|
|
391
391
|
de ce que tu as retourné :
|
|
392
392
|
|
|
393
393
|
<!-- prettier-ignore -->
|
|
394
394
|
| Tu retournes… | Ce qui se passe | Ancre |
|
|
395
395
|
| --- | --- | --- |
|
|
396
|
-
| Une `Promise` / un thenable | Déballée puis re-traitée (récursif) | `Resolver.ts:
|
|
396
|
+
| Une `Promise` / un thenable | Déballée puis re-traitée (récursif) | `Resolver.ts:830-840` |
|
|
397
397
|
| Une `string` | Envoyée telle quelle en corps | `Resolver.ts:711` |
|
|
398
|
-
| Un objet simple ou un tableau | **Auto-JSON** : `application/json` + sérialisation | `Resolver.ts:
|
|
398
|
+
| Un objet simple ou un tableau | **Auto-JSON** : `application/json` + sérialisation | `Resolver.ts:870` |
|
|
399
399
|
| Un `number` / un `boolean` | Auto-JSON scalaire (RFC 8259 §2 : `42`, `true` sont des documents valides) | `Resolver.ts:734` |
|
|
400
|
-
| Un `Buffer` | Envoyé brut | `Resolver.ts:
|
|
401
|
-
| Une `Response` (via un `render*`) | Retournée telle quelle — l'envoi a déjà eu lieu | `Resolver.ts:
|
|
402
|
-
| `void`/`null` **et** statut 204/205/304 | Réponse **vide envoyée** (RFC 9110 : ces statuts n'ont pas de corps) | `NO_BODY_STATUS` (`Resolver.ts:
|
|
403
|
-
| `void`/`null` avec tout autre statut | `waitAsync` : « l'action enverra plus tard » | `Resolver.ts:
|
|
404
|
-
| Une instance de classe (entité ORM, DTO) | **Non sérialisée** → `waitAsync` (le teardown avertit du blocage) | `Resolver.ts:
|
|
400
|
+
| Un `Buffer` | Envoyé brut | `Resolver.ts:854` |
|
|
401
|
+
| Une `Response` (via un `render*`) | Retournée telle quelle — l'envoi a déjà eu lieu | `Resolver.ts:581` |
|
|
402
|
+
| `void`/`null` **et** statut 204/205/304 | Réponse **vide envoyée** (RFC 9110 : ces statuts n'ont pas de corps) | `NO_BODY_STATUS` (`Resolver.ts:948`) |
|
|
403
|
+
| `void`/`null` avec tout autre statut | `waitAsync` : « l'action enverra plus tard » | `Resolver.ts:932` |
|
|
404
|
+
| Une instance de classe (entité ORM, DTO) | **Non sérialisée** → `waitAsync` (le teardown avertit du blocage) | `Resolver.ts:906-913` |
|
|
405
405
|
|
|
406
406
|
> [!WARNING]
|
|
407
407
|
> **Le piège n° 1 : `return null` sur un statut à corps.** Le framework l'interprète comme « je
|
|
408
408
|
> répondrai moi-même » et attend — jusqu'au timeout. La distinction se fait sur le **statut** :
|
|
409
|
-
> `NO_BODY_STATUS` (`Resolver.ts:
|
|
409
|
+
> `NO_BODY_STATUS` (`Resolver.ts:948`) contient 204, 205 et 304. Donc un `@Delete` qui fait
|
|
410
410
|
> `@HttpCode(204)` puis `return null` répond bien 204 vide ; le même `return null` sans `@HttpCode`
|
|
411
411
|
> laisse la requête pendue.
|
|
412
412
|
|
|
@@ -419,12 +419,12 @@ Quand tu veux piloter l'envoi plutôt que retourner une valeur :
|
|
|
419
419
|
|
|
420
420
|
| Helper | Pour… | Ancre |
|
|
421
421
|
| -------------------------------------------- | -------------------------------------------------------- | ------------------- |
|
|
422
|
-
| `renderJson(obj, status?, headers?)` | JSON explicite avec statut/en-têtes | `Controller.ts:
|
|
422
|
+
| `renderJson(obj, status?, headers?)` | JSON explicite avec statut/en-têtes | `Controller.ts:481` |
|
|
423
423
|
| `render(data, encoding?, status?, headers?)` | Envoyer un corps quelconque via le contexte | `Controller.ts:377` |
|
|
424
424
|
| `renderView(path, params, status?)` | Rendre un template **Eta** (avec les helpers frontend) | `Controller.ts:410` |
|
|
425
425
|
| `renderResponse(data, encoding?, …)` | Poser statut + en-têtes, puis envoyer | `Controller.ts:392` |
|
|
426
|
-
| `redirect(url, status?, headers?)` | Rediriger | `Controller.ts:
|
|
427
|
-
| `forward("module:controller:action")` | Déléguer à une autre action **sans** aller-retour réseau | `Controller.ts:
|
|
426
|
+
| `redirect(url, status?, headers?)` | Rediriger | `Controller.ts:500` |
|
|
427
|
+
| `forward("module:controller:action")` | Déléguer à une autre action **sans** aller-retour réseau | `Controller.ts:534` |
|
|
428
428
|
| `setContextJson()` / `setContextHtml()` | Choisir le type de contenu avant d'envoyer | `Controller.ts:319` |
|
|
429
429
|
|
|
430
430
|
`renderView()` mesure sa propre phase `render` et injecte automatiquement les aides frontend
|
|
@@ -437,7 +437,7 @@ Quand tu veux piloter l'envoi plutôt que retourner une valeur :
|
|
|
437
437
|
> [!TIP]
|
|
438
438
|
> **Redirection : le code par défaut est 302** (Found), pas 301. Un statut absent ou hors de la liste
|
|
439
439
|
> RFC 9110 §15.4 (301, 302, 303, 307, 308) retombe sur 302 avec un log d'avertissement
|
|
440
|
-
> (`Response.redirect()`, `Response.ts:
|
|
440
|
+
> (`Response.redirect()`, `Response.ts:611`). Un 301 par défaut piégeait : les navigateurs le mettent
|
|
441
441
|
> en cache de façon quasi irréversible.
|
|
442
442
|
|
|
443
443
|
## 📁 Servir un fichier — téléchargement et flux média
|
|
@@ -446,7 +446,7 @@ Deux besoins distincts, deux helpers.
|
|
|
446
446
|
|
|
447
447
|
### Téléchargement — `renderFileDownload()`
|
|
448
448
|
|
|
449
|
-
`renderFileDownload(file, options?, headers?)` (`Controller.ts:
|
|
449
|
+
`renderFileDownload(file, options?, headers?)` (`Controller.ts:625`) pose
|
|
450
450
|
`Content-Disposition: attachment`, `Content-Length`, le type MIME du fichier, puis délègue au moteur
|
|
451
451
|
de flux. Le fichier est résolu **sans bloquer l'event loop** (`getFileAsync()`, `Controller.ts:586`) ;
|
|
452
452
|
la variante synchrone `getFile()` existe encore mais est marquée obsolète — elle appelle `lstatSync`
|
|
@@ -454,7 +454,7 @@ et gèle le process le temps du stat.
|
|
|
454
454
|
|
|
455
455
|
### Lecture en continu — `renderMediaStream()`
|
|
456
456
|
|
|
457
|
-
`renderMediaStream(file, headers?, options?)` (`Controller.ts:
|
|
457
|
+
`renderMediaStream(file, headers?, options?)` (`Controller.ts:780`) implémente les **requêtes par
|
|
458
458
|
plage** (RFC 9110 §14), ce qui permet à un lecteur vidéo de sauter dans le flux :
|
|
459
459
|
|
|
460
460
|
| Le client envoie… | Réponse |
|
|
@@ -474,7 +474,7 @@ donc testable sans serveur.
|
|
|
474
474
|
**nettoyage** : le flux est ouvert avec `autoClose: false`, et un client qui raccroche en plein
|
|
475
475
|
téléchargement laisserait sinon un descripteur de fichier ouvert et une promesse pendue à jamais. Un
|
|
476
476
|
écouteur `close` sur la réponse détruit le flux, ce qui déclenche la fermeture du descripteur et
|
|
477
|
-
résout la promesse (`Controller.ts:
|
|
477
|
+
résout la promesse (`Controller.ts:729-734`), puis se retire lui-même (`Controller.ts:746`). Un
|
|
478
478
|
téléchargement interrompu ne coûte donc **rien** en ressource retenue.
|
|
479
479
|
|
|
480
480
|
## ⚠️ Erreurs — lever, rendre, observer
|
|
@@ -489,11 +489,11 @@ throw new nodefonyError("Article introuvable", 404); // statut porté par l'erre
|
|
|
489
489
|
throw new HttpError("Not Found", 404, this.context); // variante enrichie du contexte
|
|
490
490
|
```
|
|
491
491
|
|
|
492
|
-
L'exception remonte jusqu'à `HttpKernel.onError()` (`http-kernel.ts:
|
|
492
|
+
L'exception remonte jusqu'à `HttpKernel.onError()` (`http-kernel.ts:879`), qui délègue la mise en
|
|
493
493
|
forme au rendeur d'erreurs. Ce qui en sort :
|
|
494
494
|
|
|
495
495
|
- **statut normalisé** — un code absent (ou l'ancien quirk `200`) devient **500**
|
|
496
|
-
(`normalizeHttpStatus()`, `error-renderer.ts:
|
|
496
|
+
(`normalizeHttpStatus()`, `error-renderer.ts:642`) ;
|
|
497
497
|
- **corps structuré** : `{ code, message, result: null, error: {…}, nodefony: {…} }`, l'enveloppe
|
|
498
498
|
`nodefony` portant l'environnement, l'URL et l'**identifiant de requête** — de quoi retrouver la
|
|
499
499
|
trace complète dans les logs ;
|
|
@@ -501,13 +501,13 @@ forme au rendeur d'erreurs. Ce qui en sort :
|
|
|
501
501
|
ne tente pas de rendre — il journalise et s'arrête (`http-kernel.ts:770-775`).
|
|
502
502
|
|
|
503
503
|
En **WebSocket**, il n'y a pas de statut : l'erreur devient un **code de fermeture** RFC 6455
|
|
504
|
-
(`renderWebsocket()`, `error-renderer.ts:
|
|
504
|
+
(`renderWebsocket()`, `error-renderer.ts:518`) — 401/403 → 1008 (violation de politique),
|
|
505
505
|
5xx → 1011 (erreur interne), le reste → 4004 (plage privée). Si la socket n'est pas encore acceptée,
|
|
506
506
|
c'est un **rejet** de handshake.
|
|
507
507
|
|
|
508
508
|
> [!NOTE]
|
|
509
509
|
> Les erreurs de ton action remontent **seules** : le Resolver n'enveloppe pas l'appel dans un
|
|
510
|
-
> `try/catch` inutile (`Resolver.ts:
|
|
510
|
+
> `try/catch` inutile (`Resolver.ts:444-445`). Inutile d'attraper pour re-lever — sauf si tu veux
|
|
511
511
|
> vraiment traduire l'erreur en un autre statut.
|
|
512
512
|
|
|
513
513
|
## 🧩 Services injectés — trois façons
|
|
@@ -521,7 +521,7 @@ explicite :
|
|
|
521
521
|
const catalog = this.get<CatalogService>("catalog"); // null si absent ou container nettoyé
|
|
522
522
|
```
|
|
523
523
|
|
|
524
|
-
`Service.get()` (`Service.ts:
|
|
524
|
+
`Service.get()` (`Service.ts:518`) est une **façade sûre** : elle retourne `null` au lieu de lever si
|
|
525
525
|
le container a déjà été détaché. C'est le style à privilégier dans `initialize()`.
|
|
526
526
|
|
|
527
527
|
### 2. Injection par le constructeur — `@inject`
|
|
@@ -568,7 +568,7 @@ code du framework applique — et attend de toi — les règles suivantes :
|
|
|
568
568
|
(`Controller.ts:574`).
|
|
569
569
|
- **Métadonnées d'action figées** : `@HttpCode`, `@Header`, les paramètres décorés et l'intention de
|
|
570
570
|
session sont calculés **une fois** par route puis mémorisés, au lieu d'être relus par `Reflect` à
|
|
571
|
-
chaque requête (`resolveActionMeta()` appelé en `Resolver.ts:
|
|
571
|
+
chaque requête (`resolveActionMeta()` appelé en `Resolver.ts:469`).
|
|
572
572
|
- **Gardes payées seulement si présentes** : sans `@IsGranted`, la vérification d'autorisation est
|
|
573
573
|
un test de nullité (`Resolver.ts:334`) — 0 lookup, 0 `await`, 0 allocation.
|
|
574
574
|
- **Ta part du contrat** : pas de structure allouée « au cas où » dans le constructeur ni dans
|
|
@@ -578,13 +578,13 @@ code du framework applique — et attend de toi — les règles suivantes :
|
|
|
578
578
|
|
|
579
579
|
| Domaine | Norme | Comment le code s'y conforme |
|
|
580
580
|
| -------------------------------- | ------------------------ | -------------------------------------------------------------- |
|
|
581
|
-
| Statuts sans corps (204/205/304) | RFC 9110 §15.3.5/§15.4.5 | `NO_BODY_STATUS` (`Resolver.ts:
|
|
581
|
+
| Statuts sans corps (204/205/304) | RFC 9110 §15.3.5/§15.4.5 | `NO_BODY_STATUS` (`Resolver.ts:948`) |
|
|
582
582
|
| Requêtes par plage | RFC 9110 §14.1.2, §14.2 | `parseByteRange()` (`Controller.ts:107`) |
|
|
583
583
|
| Plage insatisfiable → 416 | RFC 9110 §15.5.17 | `renderResponse()` avec 416 (`Controller.ts:392`) |
|
|
584
584
|
| Redirections | RFC 9110 §15.4 | Liste blanche + repli 302 (`Response.ts:534`) |
|
|
585
|
-
| Média JSON sans `charset` | RFC 8259 §11 | Auto-JSON (`Resolver.ts:
|
|
585
|
+
| Média JSON sans `charset` | RFC 8259 §11 | Auto-JSON (`Resolver.ts:892`), vérifié par le banc `auto-json` |
|
|
586
586
|
| Scalaire JSON de premier niveau | RFC 8259 §2 | `number`/`boolean` rendus (`Resolver.ts:734`) |
|
|
587
|
-
| Codes de fermeture WebSocket | RFC 6455 §7.4 | `renderWebsocket()` (`error-renderer.ts:
|
|
587
|
+
| Codes de fermeture WebSocket | RFC 6455 §7.4 | `renderWebsocket()` (`error-renderer.ts:518`) |
|
|
588
588
|
|
|
589
589
|
## 📡 Observabilité — Studio
|
|
590
590
|
|
|
@@ -601,11 +601,11 @@ code du framework applique — et attend de toi — les règles suivantes :
|
|
|
601
601
|
<!-- prettier-ignore -->
|
|
602
602
|
| Symptôme | Cause (dans le code) | Correction |
|
|
603
603
|
| --- | --- | --- |
|
|
604
|
-
| La requête pend puis expire, alors que l'action a bien tourné | `return null`/`undefined` avec un statut à corps → `waitAsync` (`Resolver.ts:
|
|
605
|
-
| Réponse vide alors qu'on retourne une entité ORM | Instance de classe **non** sérialisée → `waitAsync` (`Resolver.ts:
|
|
604
|
+
| La requête pend puis expire, alors que l'action a bien tourné | `return null`/`undefined` avec un statut à corps → `waitAsync` (`Resolver.ts:906`) | Retourner une valeur, ou poser `@HttpCode(204)` |
|
|
605
|
+
| Réponse vide alors qu'on retourne une entité ORM | Instance de classe **non** sérialisée → `waitAsync` (`Resolver.ts:906`) | Retourner un objet simple, ou `renderJson(entity.toJSON())` |
|
|
606
606
|
| `Route Action not found` | L'action porte un nom déjà utilisé par un membre de `Controller` | Renommer : `session`, `request`, `response`, `context`, `route`, `method`, `query*`, `get`, `set`, `render*`, `redirect`, `forward` sont réservés |
|
|
607
607
|
| `this.session` est `null` dans `initialize()` | La session est activée **après** (`http-kernel.ts:1142`) | Lire la session dans l'action, pas dans le hook |
|
|
608
|
-
| Effet de bord exécuté pour une requête finalement 401 | `initialize()` tourne avant `firewall.handleSecurity()` (`http-kernel.ts:
|
|
608
|
+
| Effet de bord exécuté pour une requête finalement 401 | `initialize()` tourne avant `firewall.handleSecurity()` (`http-kernel.ts:1490`) | Déplacer l'effet de bord dans l'action |
|
|
609
609
|
| Redirection permanente non voulue | Un statut invalide retombe sur 302, un `301` explicite reste 301 | Passer le code voulu : `this.redirect(url, 302)` |
|
|
610
610
|
| WS : l'état d'une frame « bave » sur la suivante | L'instance est partagée par toute la connexion (`Resolver.ts:262`) | Réinitialiser l'état en tête d'action, ou le porter par message |
|
|
611
611
|
| WS : l'action n'est jamais appelée | Route sans transport `WEBSOCKET` déclaré | `requirements: { methods: ["WEBSOCKET"] }` |
|