Les callables WSGI dans Forge¶
Ce document décrit les points d'entrée WSGI qui permettent de servir Forge en production.
En production, Forge se sert derrière un serveur WSGI comme Gunicorn et un reverse proxy.
Ce module fournit les callables WSGI qui enveloppent l'Application.
Le fichier de code correspondant est core/app/wsgi.py.
1. Rôle¶
Un serveur WSGI externe attend un objet appelable qui prend environ et start_response, puis retourne un itérable d'octets.
Ce module adapte l'Application Forge à ce contrat.
create_wsgi_app(application) enveloppe une Application déjà construite.
C'est le point d'entrée de production : il sert l'application armée, celle que construit app.py, avec ses middlewares et son magasin de sessions.
create_configured_wsgi_app() construit une application depuis config.py seul, puis retourne le callable.
Il ne voit pas ce que app.py câble, config.py ne portant que des valeurs, jamais des objets construits.
Depuis l'ADR-092, il refuse de construire quand app.py déclare un câblage qu'il ignorerait.
Le module applique aussi le socle de headers de sécurité partagé avec le serveur de développement, et émet une fois les avertissements de production au démarrage.
Le périmètre est volontairement limité.
Il ne remplace pas le serveur de développement, ne sert pas les fichiers statiques (rôle du reverse proxy) et ne couvre pas la production complète.
Deux chemins font exception, et pour la même raison.
GET /health et GET /media/... sont servis avant le routage, ici comme sur le serveur de développement.
Ce sont des préfixes, pas des routes, et leur interception ne vivait que dans le serveur de développement : une application déployée répondait 404 sur sa sonde, puis sur tous ses médias.
La réponse vient désormais de core.http.health et de core.http.media, chacune source unique de son chemin.
2. Vue d'ensemble rapide¶
| Élément | Valeur |
|---|---|
| Module Python | core.app.wsgi |
| Couche | bootstrap applicatif, frontière WSGI |
| Rôle | exposer l'Application Forge comme callable WSGI |
| Dépend de | core.app.app_factory, core.app.prod_warnings, Request, Response, les headers de sécurité, la CSP |
| API publique | create_wsgi_app(application), create_configured_wsgi_app(...) |
| Objet lié | Application en entrée, callable WSGI en sortie |
| Sécurité | headers de sécurité par défaut ; HSTS conditionné à wsgi.url_scheme == "https" |
3. Schéma de séquence¶
Le module transforme un environnement WSGI en Request, dispatche via l'Application, puis convertit la Response en réponse WSGI.
sequenceDiagram
participant Gunicorn as Serveur WSGI
participant Wsgi as callable WSGI
participant Req as Request
participant App as Application
participant Resp as Response
Gunicorn->>Wsgi: app(environ, start_response)
Wsgi->>Req: construit Request depuis environ
alt Requête invalide
Wsgi-->>Gunicorn: 400 Bad Request (avec headers de sécurité)
else Requête valide
Wsgi->>App: dispatch(request)
App-->>Wsgi: Response
Wsgi->>Resp: pose les headers de sécurité par défaut
Wsgi-->>Gunicorn: status, headers, corps
end
À retenir :
- l'environnement WSGI est adapté en
Requestvia un stub de handler interne ; - même une requête invalide reçoit le socle de headers de sécurité (réponse 400) ;
- les headers de sécurité sont posés en
setdefault: une route qui les définit garde la main ; - HSTS n'est ajouté que si la connexion est en HTTPS ; derrière un proxy TLS-terminé, c'est le proxy qui pose HSTS.
4. API publique¶
| Fonction | Signature | Rôle |
|---|---|---|
create_wsgi_app |
create_wsgi_app(application: Any, *, emit_prod_warnings: bool = True, logger: logging.Logger | None = None) -> Callable |
enveloppe une Application déjà construite en callable WSGI |
create_configured_wsgi_app |
create_configured_wsgi_app(*, emit_prod_warnings: bool = True, logger: logging.Logger | None = None) -> Callable |
construit l'Application depuis config.py et retourne son callable WSGI |
Avertissements de production
Avec emit_prod_warnings=True (défaut), les deux fonctions émettent une seule fois, à la construction, les avertissements de production (par exemple un store de session en mémoire en APP_ENV=prod).
Ils vivaient dans la seule fabrique générique, ce qui les aurait fait disparaître du chemin recommandé.
Passer emit_prod_warnings=False pour les tests qui ne veulent pas polluer le logger.
5. Contextes d'utilisation¶
| Besoin | Élément |
|---|---|
| Servir en production avec Gunicorn | create_wsgi_app(application) dans le wsgi.py du projet, sur l'application de app.py |
Servir un projet dont tout le câblage tient dans config.py |
create_configured_wsgi_app() |
| Éviter les warnings dans un test | l'une ou l'autre, avec emit_prod_warnings=False |
6. Exemples d'utilisation¶
Fichier wsgi.py du projet, exposé à Gunicorn :
from app import application as _application
from core.app.wsgi import create_wsgi_app
application = create_wsgi_app(_application)
app.py doit exposer son Application sous un nom public.
Servir create_configured_wsgi_app() ici donnerait une application privée de ses middlewares, qui démarrerait quand même.
Lancement avec Gunicorn :
Envelopper une Application construite manuellement :
from core.app.application import Application
from core.app.wsgi import create_wsgi_app
app = Application(router)
wsgi_app = create_wsgi_app(app)
7. Le refus de servir une application désarmée¶
build_wsgi_application() refuse de construire quand il ne peut pas garantir que l'application servira avec ses gardes (ADR-092).
Le refus lève UnarmedApplicationError, au démarrage et jamais en cours de service.
Deux situations le déclenchent, et le message dit laquelle.
| Message | Ce qui se passe | Le geste |
|---|---|---|
| « construirait une application DÉSARMÉE » | app.py câble des middlewares ou un magasin de sessions que ce chemin ne verra pas |
Déplacer ce câblage dans bootstrap.py, que les deux points d'entrée lisent |
| « ne peut pas savoir si l'application serait ARMÉE » | app.py ne s'analyse pas : erreur de syntaxe, ou fichier qui n'est pas de l'UTF-8 |
Corriger le fichier ; python -m py_compile app.py nomme la faute |
Le fichier est lu sur son arbre syntaxique, jamais importé : l'importer serait exécuter précisément ce que ce chemin cherche à éviter.
Un exemple de câblage laissé en commentaire n'est donc pas pris pour une déclaration.
Le second cas n'est pas un excès de zèle
Une erreur de syntaxe se lisait auparavant comme un fichier vide, donc comme « rien à signaler » (WSGI-WIRING-GUARD-UNPARSABLE-001).
Le même app.py câblant deux middlewares était refusé quand il s'analysait, et servi avec une parenthèse en trop.
Ce chemin n'important jamais app.py, personne d'autre n'aurait vu l'erreur : l'application serait partie sans CSRF ni RBAC, en répondant 200.
Un fichier qu'on ne sait pas lire ne dit pas qu'il ne câble rien : il ne dit rien du tout, et l'ignorance ne vaut pas l'absence.
Ce qui laisse servir
Un app.py absent, vide, ou qui ne câble rien.
Ce sont des états connus : un projet peut n'avoir pas d'app.py, et le chemin WSGI se suffit alors à lui même.
8. Sécurité et limites¶
Périmètre de production
Le callable WSGI ne sert pas les fichiers statiques : confier ce rôle au reverse proxy.
Les médias (/media/) font exception et sont servis par l'application, leur résolution anti-traversal et leurs tranches HTTP Range étant hors de portée du proxy.
Il ne couvre pas tous les aspects de la production ; consulter le guide de déploiement WSGI du projet pour la configuration complète.
Voir aussi¶
- La fabrique d'application : appelée par
create_configured_wsgi_app, et le câblagebootstrap.py. - L'application : l'objet enveloppé par le callable WSGI.
- Les avertissements de production : émis au démarrage en production.