@nodefony/framework 10.0.0-beta.5 → 10.0.0-beta.7
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.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorateParam.js +7 -0
- package/dist/index.js +14 -10
- package/dist/nodefony/config/config.js +16 -2
- package/dist/nodefony/controller/AdminApiController.js +18 -5
- package/dist/nodefony/controller/BenchController.js +1 -1
- package/dist/nodefony/controller/CspReportController.js +70 -0
- package/dist/nodefony/controller/IssuerMetadataController.js +3 -3
- package/dist/nodefony/controller/LoginPageController.js +5 -7
- package/dist/nodefony/controller/ProtectedResourceMetadataController.js +75 -10
- package/dist/nodefony/controller/SessionAuthController.js +6 -7
- package/dist/nodefony/controller/TokenAuthController.js +3 -3
- package/dist/nodefony/decorators/routerDecorators.js +103 -15
- package/dist/nodefony/grpc/GrpcController.js +138 -0
- package/dist/nodefony/grpc/GrpcService.js +75 -0
- package/dist/nodefony/grpc/declarations.js +33 -0
- package/dist/nodefony/grpc/index.js +3 -0
- package/dist/nodefony/service/AdminBroker.js +1 -1
- package/dist/nodefony/service/IdempotencyStore.js +1 -1
- package/dist/nodefony/service/router.js +15 -12
- package/dist/nodefony/src/Controller.js +330 -115
- package/dist/nodefony/src/PlaygroundAdminApi.js +1 -1
- package/dist/nodefony/src/Resolver.js +85 -51
- package/dist/nodefony/src/Route.js +45 -45
- package/dist/nodefony/src/SecurityApiController.js +30 -7
- package/dist/nodefony/src/conditionalRequest.js +103 -0
- package/dist/nodefony/src/docsReader.js +1 -1
- package/dist/nodefony/src/idempotency.js +1 -1
- package/dist/nodefony/src/loginPage.js +3 -2
- package/dist/nodefony/src/scopeCatalog.js +1 -1
- package/dist/nodefony/src/viewNonce.js +58 -0
- package/dist/types/index.d.ts +4 -2
- package/dist/types/nodefony/config/config.d.ts +20 -0
- package/dist/types/nodefony/controller/AdminApiController.d.ts +4 -2
- package/dist/types/nodefony/controller/CspReportController.d.ts +35 -0
- package/dist/types/nodefony/controller/LoginPageController.d.ts +3 -2
- package/dist/types/nodefony/controller/ProtectedResourceMetadataController.d.ts +21 -1
- package/dist/types/nodefony/controller/SessionAuthController.d.ts +5 -6
- package/dist/types/nodefony/controller/TokenAuthController.d.ts +3 -3
- package/dist/types/nodefony/decorators/routerDecorators.d.ts +2 -1
- package/dist/types/nodefony/grpc/GrpcController.d.ts +75 -0
- package/dist/types/nodefony/grpc/GrpcService.d.ts +51 -0
- package/dist/types/nodefony/grpc/declarations.d.ts +12 -0
- package/dist/types/nodefony/grpc/index.d.ts +13 -0
- package/dist/types/nodefony/interfaces/IController.d.ts +4 -4
- package/dist/types/nodefony/service/router.d.ts +8 -6
- package/dist/types/nodefony/src/Controller.d.ts +124 -14
- package/dist/types/nodefony/src/Resolver.d.ts +35 -8
- package/dist/types/nodefony/src/Route.d.ts +35 -0
- package/dist/types/nodefony/src/SecurityApiController.d.ts +17 -4
- package/dist/types/nodefony/src/conditionalRequest.d.ts +82 -0
- package/dist/types/nodefony/src/loginPage.d.ts +1 -1
- package/dist/types/nodefony/src/viewNonce.d.ts +33 -0
- package/docs/controller.md +234 -35
- package/docs/decorateurs.md +1 -1
- package/docs/grpc.md +690 -0
- package/docs/idempotence.md +13 -8
- package/docs/index.md +16 -10
- package/docs/routing.md +30 -2
- package/docs/templates.md +15 -0
- package/package.json +28 -11
package/docs/controller.md
CHANGED
|
@@ -404,31 +404,111 @@ class MeController extends Controller {
|
|
|
404
404
|
## 🧰 Répondre — ce que ton `return` déclenche
|
|
405
405
|
|
|
406
406
|
Le traducteur unique est `Resolver.returnController()` (`Resolver.ts#Resolver.returnController`). Il regarde le **type**
|
|
407
|
-
de ce que tu as retourné :
|
|
407
|
+
de ce que tu as retourné — dans l'ordre de fréquence : la promesse, puis le tableau et l'objet simple,
|
|
408
|
+
le cas courant, qui ne paient aucun test de plus que nécessaire (`Resolver.ts#Resolver.returnObject`) :
|
|
408
409
|
|
|
409
410
|
<!-- prettier-ignore -->
|
|
410
411
|
| Tu retournes… | Ce qui se passe | Ancre |
|
|
411
412
|
| --- | --- | --- |
|
|
412
|
-
| Une `Promise` / un thenable | Déballée puis re-traitée (récursif) | `Resolver.ts#Resolver.
|
|
413
|
-
| Une `string` | Envoyée telle quelle en corps | `Resolver.ts#Resolver.
|
|
414
|
-
| Un objet simple ou un tableau | **Auto-JSON** : `application/json` + sérialisation | `Resolver.ts#Resolver.
|
|
415
|
-
| Un `number` / un `boolean` | Auto-JSON scalaire (RFC 8259 §2 : `42`, `true` sont des documents valides) | `Resolver.ts#Resolver.
|
|
416
|
-
| Un `Buffer` | Envoyé brut | `Resolver.ts#Resolver.
|
|
417
|
-
| Une
|
|
418
|
-
| `
|
|
419
|
-
|
|
|
420
|
-
|
|
|
413
|
+
| Une `Promise` / un thenable | Déballée puis re-traitée (récursif) | `Resolver.ts#Resolver.returnController` |
|
|
414
|
+
| Une `string` | Envoyée telle quelle en corps | `Resolver.ts#Resolver.returnController` |
|
|
415
|
+
| Un objet simple ou un tableau | **Auto-JSON** : `application/json` + sérialisation | `Resolver.ts#Resolver.returnObject` |
|
|
416
|
+
| Un `number` / un `boolean` | Auto-JSON scalaire (RFC 8259 §2 : `42`, `true` sont des documents valides) | `Resolver.ts#Resolver.returnController` |
|
|
417
|
+
| Un `Buffer` | Envoyé brut | `Resolver.ts#Resolver.returnObject` |
|
|
418
|
+
| Une réponse Nodefony (rendue par un `render*`) | Retournée telle quelle — l'envoi a déjà eu lieu | `Resolver.ts#Resolver.returnObject` |
|
|
419
|
+
| Une **`Response` Web standard** — `fetch()`, `new Response(…)`, `Response.json(…)` | **Envoyée en flux** : son statut, ses en-têtes, son corps (HTTP seulement) — détail ci-dessous | `HttpContext.openWebResponse()` (`HttpContext.ts#HttpContext.openWebResponse`, `@nodefony/http`) |
|
|
420
|
+
| Un **flux d'octets nu** — `Readable` Node, `ReadableStream` Web | **Envoyé en flux**, comme `this.renderStream(source)` sans option (HTTP seulement) | `Resolver.ts#Resolver.returnObject` |
|
|
421
|
+
| `void`/`null` **et** statut 204/205/304 | Réponse **vide envoyée** (RFC 9110 : ces statuts n'ont pas de corps) | `isContentlessStatus` (`Response.ts#isContentlessStatus`, `@nodefony/http`) |
|
|
422
|
+
| `void`/`null` avec tout autre statut | `waitAsync` : « l'action enverra plus tard » | `Resolver.ts#Resolver.returnNothing` |
|
|
423
|
+
| Une `Date`, une `RegExp`, une `Error` | Comme `void` : rien à rendre | `Resolver.ts#Resolver.returnNothing` |
|
|
424
|
+
| Une instance de classe (entité ORM, DTO) | **Non sérialisée** → `waitAsync` (le teardown avertit du blocage) | `Resolver.ts#Resolver.returnObject` |
|
|
421
425
|
|
|
422
426
|
> [!WARNING]
|
|
423
427
|
> **Le piège n° 1 : `return null` sur un statut à corps.** Le framework l'interprète comme « je
|
|
424
428
|
> répondrai moi-même » et attend — jusqu'au timeout. La distinction se fait sur le **statut** :
|
|
425
|
-
> `
|
|
429
|
+
> `isContentlessStatus` (`Response.ts#isContentlessStatus`, liste unique de `@nodefony/http`) rend vrai pour 204, 205 et 304. Donc un `@Delete` qui fait
|
|
426
430
|
> `@HttpCode(204)` puis `return null` répond bien 204 vide ; le même `return null` sans `@HttpCode`
|
|
427
431
|
> laisse la requête pendue.
|
|
428
432
|
|
|
429
433
|
Même règle pour une **instance de classe** (une entité ORM renvoyée telle quelle) : elle n'est
|
|
430
434
|
volontairement pas passée à `JSON.stringify`. Retourne un objet simple — ou appelle `renderJson()`.
|
|
431
435
|
|
|
436
|
+
### Renvoyer une `Response` Web ou un flux
|
|
437
|
+
|
|
438
|
+
La `Response` est l'objet standard que rend `fetch()` — dans le navigateur, et dans Node depuis la
|
|
439
|
+
version 18. Beaucoup de bibliothèques en produisent une toute prête (un kit d'IA qui rend son texte
|
|
440
|
+
en flux, un client d'API). Ton action peut la **renvoyer telle quelle** : le framework envoie son
|
|
441
|
+
statut, ses en-têtes et son corps EN FLUX, sans le lire d'avance.
|
|
442
|
+
|
|
443
|
+
```ts
|
|
444
|
+
import { Readable } from "node:stream";
|
|
445
|
+
import { Controller, controller, route } from "@nodefony/framework";
|
|
446
|
+
import type { Context } from "@nodefony/http";
|
|
447
|
+
|
|
448
|
+
@controller("/examples")
|
|
449
|
+
export class ExamplesController extends Controller {
|
|
450
|
+
constructor(context: Context) {
|
|
451
|
+
super("ExamplesController", context);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** Relayer une API tierce : sa réponse, telle quelle. */
|
|
455
|
+
@route("weather", { path: "/weather" })
|
|
456
|
+
weather() {
|
|
457
|
+
// Le signal coupe l'appel amont si le client s'en va.
|
|
458
|
+
return fetch("https://api.exemple.org/meteo", {
|
|
459
|
+
signal: this.context?.signal ?? null,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** Une réponse construite : statut, en-têtes et cookie à toi. */
|
|
464
|
+
@route("export", { path: "/export.csv" })
|
|
465
|
+
export() {
|
|
466
|
+
return new Response("id;libellé\n1;Cordage\n", {
|
|
467
|
+
status: 200,
|
|
468
|
+
headers: [
|
|
469
|
+
["content-type", "text/csv; charset=utf-8"],
|
|
470
|
+
["content-disposition", 'attachment; filename="export.csv"'],
|
|
471
|
+
["set-cookie", "export=fait; Path=/; HttpOnly"],
|
|
472
|
+
],
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** Un flux d'octets renvoyé nu. */
|
|
477
|
+
@route("lines", { path: "/lines" })
|
|
478
|
+
lines() {
|
|
479
|
+
return Readable.from(["une\n", "deux\n"], { objectMode: false });
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
Ce que le framework fait de la réponse (`webResponse.ts#webResponseParts`, `@nodefony/http`) :
|
|
485
|
+
|
|
486
|
+
| Origine de la `Response` | En-têtes envoyés |
|
|
487
|
+
| ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
488
|
+
| **Construite** par ton code (`new Response`, `Response.json`, `Response.redirect`) | tous les siens, **cookies compris** |
|
|
489
|
+
| **Relayée** — rendue par `fetch()` | les siens, **sauf** `content-encoding` et `content-length` (`fetch` a déjà décompressé le corps : ils décriraient des octets qui ne sont plus là) et `set-cookie` (les cookies d'un service tiers seraient posés sous TON domaine) |
|
|
490
|
+
|
|
491
|
+
Dans les deux cas, les en-têtes de connexion (`connection`, `keep-alive`, `transfer-encoding`,
|
|
492
|
+
`upgrade`… — RFC 9110 §7.6.1) ne partent jamais : ils décrivent un saut, et HTTP/2 les refuse.
|
|
493
|
+
La différence se lit sur `response.type` : `"default"` pour une réponse construite, `"basic"` ou
|
|
494
|
+
`"cors"` pour une réponse de `fetch`.
|
|
495
|
+
|
|
496
|
+
> [!TIP]
|
|
497
|
+
> **Quand préférer `this.renderStream()`** : dès qu'il faut des options — délai de silence
|
|
498
|
+
> (`idleTimeout`), durée maximale (`maxDuration`), écouteur de fin (`onClose`), ou ne recopier que
|
|
499
|
+
> certains en-têtes de l'amont. `return response` est le geste court du cas courant ;
|
|
500
|
+
> `renderStream` est le réglage fin. Les deux passent par la même tuyauterie
|
|
501
|
+
> (`HttpContext.ts#HttpContext.openStream`) : contre-pression, arrêt au départ du client, `HEAD` sans
|
|
502
|
+
> lire le corps.
|
|
503
|
+
|
|
504
|
+
> [!WARNING]
|
|
505
|
+
>
|
|
506
|
+
> - **`Response.error()`** ne porte aucune réponse (statut 0) : le client reçoit un **502**.
|
|
507
|
+
> - **WebSocket** : une `Response` ou un flux renvoyé par une action de message n'est pas envoyé — une
|
|
508
|
+
> trame n'a ni statut ni en-têtes. Renvoie un objet (envoyé en trame JSON) ou un `Buffer`.
|
|
509
|
+
> - **Réponse déjà partie** (tu as appelé `render*` ou `streamFile` ET renvoyé une `Response`) : rien
|
|
510
|
+
> n'est réécrit, et le corps de la `Response` est annulé pour libérer l'amont.
|
|
511
|
+
|
|
432
512
|
### Les helpers de rendu
|
|
433
513
|
|
|
434
514
|
Quand tu veux piloter l'envoi plutôt que retourner une valeur :
|
|
@@ -463,8 +543,10 @@ Deux besoins distincts, deux helpers.
|
|
|
463
543
|
### Téléchargement — `renderFileDownload()`
|
|
464
544
|
|
|
465
545
|
`renderFileDownload(file, options?, headers?)` (`Controller.ts#Controller.renderFileDownload`) pose
|
|
466
|
-
`Content-Disposition: attachment`, `Content-Length`, le type MIME du fichier
|
|
467
|
-
de flux.
|
|
546
|
+
`Content-Disposition: attachment`, `Content-Length`, le type MIME du fichier et un `Expires` déjà
|
|
547
|
+
passé, puis délègue au moteur de flux. Un nom de fichier accentué ou porteur de `"` part en
|
|
548
|
+
`filename*` UTF-8, précédé d'un repli ASCII (RFC 6266 §4.3, RFC 8187) — un nom quelconque ne fait
|
|
549
|
+
jamais lever la réponse. Le fichier est résolu **sans bloquer l'event loop** (`getFileAsync()`, `Controller.ts#Controller.getFileAsync`) ;
|
|
468
550
|
la variante synchrone `getFile()` existe encore mais est marquée obsolète — elle appelle `lstatSync`
|
|
469
551
|
et gèle le process le temps du stat.
|
|
470
552
|
|
|
@@ -473,25 +555,136 @@ et gèle le process le temps du stat.
|
|
|
473
555
|
`renderMediaStream(file, headers?, options?)` (`Controller.ts#Controller.renderMediaStream`) implémente les **requêtes par
|
|
474
556
|
plage** (RFC 9110 §14), ce qui permet à un lecteur vidéo de sauter dans le flux :
|
|
475
557
|
|
|
476
|
-
| Le client envoie…
|
|
477
|
-
|
|
|
478
|
-
| Pas de `Range`
|
|
479
|
-
| `Range: bytes=0-499`
|
|
480
|
-
| `Range: bytes=-500` (suffixe)
|
|
481
|
-
| Plage hors fichier
|
|
482
|
-
| Syntaxe invalide, multi-plage, unité inconnue
|
|
558
|
+
| Le client envoie… | Réponse |
|
|
559
|
+
| ------------------------------------------------ | --------------------------------------------------------------- |
|
|
560
|
+
| Pas de `Range` | 200 + fichier complet, `Accept-Ranges: bytes` |
|
|
561
|
+
| `Range: bytes=0-499` | **206** + `Content-Range`, bornes clampées à la taille réelle |
|
|
562
|
+
| `Range: bytes=-500` (suffixe) | 206 sur les 500 derniers octets |
|
|
563
|
+
| Plage hors fichier | **416** + `Content-Range: bytes */<taille>` (RFC 9110 §15.5.17) |
|
|
564
|
+
| Syntaxe invalide, multi-plage, unité inconnue | En-tête **ignoré** → 200 complet (jamais un 500) |
|
|
565
|
+
| `HEAD` avec `Range` | `Range` **ignoré** : en-têtes du GET complet (RFC 9110 §14.2) |
|
|
566
|
+
| `If-Range` égal à l'`ETag` ou au `Last-Modified` | 206 : la reprise est sûre, le fichier n'a pas changé |
|
|
567
|
+
| `If-Range` différent, ou étiquette faible | `Range` **ignoré** → 200 complet (RFC 9110 §13.1.5) |
|
|
568
|
+
|
|
569
|
+
Une condition `If-Range` fausse sert le fichier entier — plutôt qu'une plage d'un fichier qui a
|
|
570
|
+
changé, que le client recollerait à l'ancienne. Seuls un `ETag` FORTEMENT égal, ou une date égale
|
|
571
|
+
à un `Last-Modified` vieux d'au moins une seconde, autorisent la reprise.
|
|
483
572
|
|
|
484
573
|
La logique est isolée dans une fonction pure exportée, `parseByteRange()` (`Controller.ts#parseByteRange`) —
|
|
485
574
|
donc testable sans serveur.
|
|
486
575
|
|
|
487
576
|
### Ce que `streamFile()` garantit
|
|
488
577
|
|
|
489
|
-
`streamFile()` (`Controller.ts#Controller.streamFile`) est le moteur commun
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
578
|
+
`streamFile()` (`Controller.ts#Controller.streamFile`) est le moteur commun, et il repose sur le
|
|
579
|
+
même cycle de vie que `renderStream()` ci-dessous (`HttpContext.openStream`) :
|
|
580
|
+
|
|
581
|
+
- **la réponse est prise à l'appel**, les en-têtes partent au premier morceau ;
|
|
582
|
+
- **un client qui raccroche détruit le flux**, donc ferme le descripteur : un téléchargement
|
|
583
|
+
interrompu ne retient rien ;
|
|
584
|
+
- **un fichier illisible à l'ouverture** (`EACCES`) rend la page d'erreur ordinaire, 500 ; une
|
|
585
|
+
lecture qui échoue en route coupe la connexion, jamais une fin propre ;
|
|
586
|
+
- **la longueur annoncée est tenue** : la lecture est bornée à la taille relevée (un fichier qui
|
|
587
|
+
grossit pendant l'envoi n'envoie pas plus que `Content-Length`), et un extrait `start`/`end`
|
|
588
|
+
annonce sa propre longueur ;
|
|
589
|
+
- **un fichier n'est pas un flux vivant** : ni `X-Accel-Buffering: no`, ni délai d'inactivité.
|
|
590
|
+
|
|
591
|
+
### Requêtes conditionnelles — 304, 412, reprise
|
|
592
|
+
|
|
593
|
+
Les trois helpers posent sur le fichier ENTIER une `ETag` forte et un `Last-Modified`, tirés de sa
|
|
594
|
+
taille et de sa date de modification, sans le lire. Les préconditions sont évaluées dans l'ordre de
|
|
595
|
+
RFC 9110 §13.2.2, AVANT d'ouvrir le fichier (`conditionalRequest.ts#evaluatePreconditions`) :
|
|
596
|
+
|
|
597
|
+
| Le client envoie… | Réponse |
|
|
598
|
+
| ------------------------------------------------------------ | ------------------------------------------- |
|
|
599
|
+
| `If-None-Match` égal (comparaison faible), ou `*` | **304** en GET/HEAD, sans corps ; 412 sinon |
|
|
600
|
+
| `If-Modified-Since` ≥ date du fichier (sans `If-None-Match`) | **304** en GET/HEAD |
|
|
601
|
+
| `If-Match` différent (comparaison forte) | **412** `Precondition Failed` |
|
|
602
|
+
| `If-Unmodified-Since` < date du fichier (sans `If-Match`) | **412** |
|
|
603
|
+
|
|
604
|
+
Le 304 porte ce qu'aurait porté le 200 : `ETag`, `Last-Modified`, `Cache-Control`, `Expires`,
|
|
605
|
+
`Vary` (RFC 9110 §15.4.5). Le helper rend alors `null` : aucun fichier n'a été ouvert.
|
|
606
|
+
|
|
607
|
+
**`Cache-Control: private, no-cache` par défaut.** Sans lui, `Last-Modified` rendrait la réponse
|
|
608
|
+
« heuristiquement cacheable » (RFC 9111 §4.2.2) : un cache partagé pourrait garder le fichier et le
|
|
609
|
+
resservir sans redemander — or un fichier servi par une action est le plus souvent protégé.
|
|
610
|
+
`private` ferme les caches partagés, `no-cache` fait revalider avant chaque réutilisation, ce qui
|
|
611
|
+
ne coûte qu'un 304. Un fichier public fixe le sien :
|
|
612
|
+
|
|
613
|
+
```ts
|
|
614
|
+
return this.renderMediaStream(file, {
|
|
615
|
+
"Cache-Control": "public, max-age=3600",
|
|
616
|
+
});
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
Une `ETag` ou un `Last-Modified` passés dans les en-têtes (un hachage du contenu, une version)
|
|
620
|
+
remplacent ceux du fichier ET servent à la comparaison. Un extrait `streamFile(f, h, { start, end })`
|
|
621
|
+
n'est pas la représentation entière : il ne reçoit ni validateur ni évaluation.
|
|
622
|
+
|
|
623
|
+
**Ce que l'application peut changer** :
|
|
624
|
+
|
|
625
|
+
| Besoin | Geste |
|
|
626
|
+
| ----------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
627
|
+
| Un autre `Cache-Control` (fichier public, `no-store`) | le passer dans les en-têtes, ou `@Header("Cache-Control", …)` sur l'action |
|
|
628
|
+
| Sa propre `ETag` (hachage du contenu, version) | la passer dans les en-têtes, ou `@Header("ETag", …)` — elle sert au 304 |
|
|
629
|
+
| Aucun validateur, aucun 304, aucun `Cache-Control` | `{ validators: false }` dans les options |
|
|
630
|
+
| Le défaut de toute l'application | la section `fileServing` de la config du module (ci-dessous) |
|
|
631
|
+
|
|
632
|
+
Un en-tête posé AVANT l'appel (`@Header`, `this.response.setHeader`) est respecté comme s'il était
|
|
633
|
+
passé en argument.
|
|
634
|
+
|
|
635
|
+
**Pour toute l'application**, la section `fileServing` de la config du module fixe le défaut
|
|
636
|
+
(`config.ts#fileServingSchema`) ; un appel le remplace toujours :
|
|
637
|
+
|
|
638
|
+
```ts
|
|
639
|
+
use("@nodefony/framework", {
|
|
640
|
+
fileServing: {
|
|
641
|
+
validators: true, // false : aucun validateur, aucun 304, aucun Cache-Control par défaut
|
|
642
|
+
cacheControl: "private, no-cache", // null : n'en poser aucun
|
|
643
|
+
},
|
|
644
|
+
});
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
`cacheControl` est validé au démarrage : une valeur porteuse d'un retour à la ligne (qui forgerait
|
|
648
|
+
un en-tête de plus) l'interrompt en nommant la clé. `{ validators: true }` dans les options d'un
|
|
649
|
+
appel rétablit les validateurs quand la config les a coupés.
|
|
650
|
+
|
|
651
|
+
⚠️ **La date d'un fichier n'est pas toujours fiable.** Une archive décompressée ou un build
|
|
652
|
+
reproductible donne la même date à tous les fichiers : deux contenus de même taille portent alors
|
|
653
|
+
la même `ETag`, et le client garde l'ancien sur un 304 sans que rien ne le signale. Pour ces
|
|
654
|
+
fichiers, passer une `ETag` tirée du contenu — le 304 reste juste — ou couper les validateurs :
|
|
655
|
+
|
|
656
|
+
```ts
|
|
657
|
+
// le 304 garde son sens : l'étiquette suit le contenu
|
|
658
|
+
return this.renderMediaStream(file, { ETag: `"${hash}"` });
|
|
659
|
+
// ou : aucune requête conditionnelle, le fichier part toujours entier
|
|
660
|
+
return this.renderMediaStream(file, {}, { validators: false });
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
### Une source qui n'est pas un fichier — `renderStream()`
|
|
664
|
+
|
|
665
|
+
La sortie d'un processus (ffmpeg, `tar`), le corps d'un `fetch()` relayé, une archive construite à
|
|
666
|
+
la volée : `renderStream(source, options?)` (`Controller.ts#Controller.renderStream`) répond par
|
|
667
|
+
ce flux d'octets, en HTTP/1.1 comme en HTTP/2. La réponse est prise à l'appel, ses en-têtes partent
|
|
668
|
+
au premier morceau, et la source est **détruite** à la fin quelle qu'en soit la raison. La promesse
|
|
669
|
+
ne rejette jamais : elle rend `{ reason, bytes }`.
|
|
670
|
+
|
|
671
|
+
```typescript
|
|
672
|
+
const ff = spawn("ffmpeg", args, {
|
|
673
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
674
|
+
windowsHide: true,
|
|
675
|
+
});
|
|
676
|
+
try {
|
|
677
|
+
await this.renderStream(ff.stdout, {
|
|
678
|
+
headers: { "content-type": "video/mp4" },
|
|
679
|
+
});
|
|
680
|
+
} finally {
|
|
681
|
+
ff.kill("SIGKILL"); // le flux est fini, quelle qu'en soit la raison
|
|
682
|
+
}
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
Page complète — options, raisons de fin, erreurs avant et après le premier octet, relais `fetch`,
|
|
686
|
+
pièges : [Flux d'octets](../../http/docs/byte-stream.md). Pour des **événements texte** lus par
|
|
687
|
+
`EventSource`, voir [Flux SSE](../../http/docs/sse.md).
|
|
495
688
|
|
|
496
689
|
## ⚠️ Erreurs — lever, rendre, observer
|
|
497
690
|
|
|
@@ -594,15 +787,21 @@ code du framework applique — et attend de toi — les règles suivantes :
|
|
|
594
787
|
|
|
595
788
|
## 📜 Normes appliquées
|
|
596
789
|
|
|
597
|
-
| Domaine
|
|
598
|
-
|
|
|
599
|
-
| Statuts sans corps (204/205/304)
|
|
600
|
-
| Requêtes par plage
|
|
601
|
-
| Plage insatisfiable → 416
|
|
602
|
-
|
|
|
603
|
-
|
|
|
604
|
-
|
|
|
605
|
-
|
|
|
790
|
+
| Domaine | Norme | Comment le code s'y conforme |
|
|
791
|
+
| --------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------- |
|
|
792
|
+
| Statuts sans corps (204/205/304) | RFC 9110 §15.3.5/§15.3.6/§15.4.5 | `isContentlessStatus` (`Response.ts#isContentlessStatus`) — aucun corps ne part |
|
|
793
|
+
| Requêtes par plage | RFC 9110 §14.1.2, §14.2 | `parseByteRange()` (`Controller.ts#parseByteRange`) |
|
|
794
|
+
| Plage insatisfiable → 416 | RFC 9110 §15.5.17 | `renderResponse()` avec 416 (`Controller.ts#Controller.renderResponse`) |
|
|
795
|
+
| `Range` hors GET, sous `If-Range` | RFC 9110 §14.2, §13.1.5 | `renderMediaStream()` (`Controller.ts#Controller.renderMediaStream`) |
|
|
796
|
+
| Validateurs d'un fichier | RFC 9110 §8.8.2, §8.8.3 | `fileValidators()` (`conditionalRequest.ts#fileValidators`) |
|
|
797
|
+
| Ordre des préconditions → 304/412 | RFC 9110 §13.1, §13.2.2 | `evaluatePreconditions()` (`conditionalRequest.ts#evaluatePreconditions`) |
|
|
798
|
+
| Cache d'un fichier protégé | RFC 9111 §4.2.2, §5.2.2 | `Cache-Control: private, no-cache` par défaut (`Controller.ts#Controller.streamFile`) |
|
|
799
|
+
| Nom de fichier téléchargé | RFC 6266 §4.3, RFC 8187 | `contentDisposition()` (`Controller.ts#contentDisposition`) |
|
|
800
|
+
| Longueur du corps tenue | RFC 9112 §6.3, RFC 9113 §8.1.1 | `streamFile()` (`Controller.ts#Controller.streamFile`) + `ByteStream.ts#ByteStream` |
|
|
801
|
+
| Redirections | RFC 9110 §15.4 | Liste blanche + repli 302 (`Response.ts#HttpResponse.send`) |
|
|
802
|
+
| Média JSON sans `charset` | RFC 8259 §11 | Auto-JSON (`Resolver.ts#Resolver.returnController`), vérifié par le banc `auto-json` |
|
|
803
|
+
| Scalaire JSON de premier niveau | RFC 8259 §2 | `number`/`boolean` rendus (`Resolver.ts#Resolver._callWithIdempotency`) |
|
|
804
|
+
| Codes de fermeture WebSocket | RFC 6455 §7.4 | `renderWebsocket()` (`error-renderer.ts#DefaultErrorRenderer.renderWebsocket`) |
|
|
606
805
|
|
|
607
806
|
## 📡 Observabilité — Studio
|
|
608
807
|
|
package/docs/decorateurs.md
CHANGED
|
@@ -431,7 +431,7 @@ update(input: RouteInput<{ params: typeof postParams; body: typeof postBody }>)
|
|
|
431
431
|
**Ce que fait le framework, dans l'ordre :**
|
|
432
432
|
|
|
433
433
|
- chaque partie déclarée est un [Standard Schema](https://standardschema.dev) — Zod, Valibot,
|
|
434
|
-
ArkType… ; l'interface est recopiée au cœur (`runtime/
|
|
434
|
+
ArkType… ; l'interface est recopiée au cœur (`runtime/standardSchemaTypes.ts#IStandardSchemaV1`), aucune
|
|
435
435
|
dépendance n'est imposée ;
|
|
436
436
|
- la validation a lieu **après** les gardes (`@IsGranted` : un 403 ne révèle pas le contrat) et
|
|
437
437
|
**avant** l'instanciation du contrôleur : une entrée invalide ne construit rien
|