DelphiMVCFramework 3.5.0-silicon : des middleware aux filtres, qu'est-ce qui va changer pour ton serveur ?
🇮🇹 Italiano • 🇬🇧 English • 🇪🇸 Español • 🇩🇪 Deutsch • 🇧🇷 Português

Un middleware classique de DelphiMVCFramework a quatre méthodes. Un filtre HTTP a une seule procédure. Pourquoi ai-je changé un mécanisme qui fonctionnait depuis des années, où vont les quatre hooks, dans quel ordre enregistres-tu les filtres, et que devient le serveur que tu as déjà en production ?
Pendant des années, dans DelphiMVCFramework, on a accroché du code au cycle d’une requête avec les middleware, via AddMiddleware. Les filtres HTTP, avec UseHTTPFilter, sont arrivés avec la Minimal API de la 3.5, celle du post sur la RC6. Désormais, les filtres prennent la place des middleware là où il en existe un équivalent : dix-sept middleware sont marqués deprecated, chacun avec son filtre. Ce sera dans la RC9 de la 3.5.0-silicon.
✅ Ce n'est pas un breaking change. Les serveurs actuels continuent de fonctionner de la même façon, sans modification et sans problème.
Dans la 3.5.0-silicon, les filtres sont une fonctionnalité supplémentaire : les middleware restent où ils sont, et le compilateur affiche un avertissement pour chaque middleware marqué deprecated. Il y aura un breaking change avec la 4.0, où les middleware seront supprimés complètement.
DelphiMVCFramework est le framework le plus populaire de son genre sur GitHub pour Delphi, avec plus de 50 contributeurs, et il est utilisé dans des environnements critiques : hôpitaux, banques, usines, jeux vidéo, ERP avec des centaines d'utilisateurs. C'est pour cela que j'ai choisi une transition douce, sans traumatisme.
Dans cet article, je parle des filtres HTTP, ceux qui s’exécutent avant le routing.
Deux responsabilités dans une classe
Jusqu’ici, un middleware faisait deux choses. Avec OnBeforeRouting et OnAfterRouting, il influençait le routing : il pouvait répondre avant que le routeur regarde la requête, pour servir un fichier statique, faire une redirection ou répondre à un preflight CORS, et, à la fin de la requête, observer ou modifier la réponse, comme le font ETag et analytics. Avec OnBeforeControllerAction et OnAfterControllerAction, il contrôlait la manière dont les actions étaient exécutées. Quatre méthodes dans une seule classe, pour deux tâches différentes.
Ceux qui me connaissent, ou qui ont suivi l’un de mes cours, savent l’importance que j’accorde aux principes SOLID. Trouver une classe à deux tâches dans mon projet open source le plus populaire me dérangeait vraiment. Chaque fois que je touchais à un middleware, je le voyais : soit on utilisait la paire de méthodes du routing (OnBeforeRouting et OnAfterRouting) en laissant les deux autres vides, soit on implémentait la paire de l’endpoint (OnBeforeControllerAction et OnAfterControllerAction), qui connaît déjà controller et action parce qu’elle s’exécute après le routing. Il était évident que ce composant devait être refactorisé. Le moment est venu.
La violation est celle du principe de responsabilité unique (SRP). Je l’ai résolue avec un mécanisme qui existe dans beaucoup de frameworks : une fonction qui reçoit la requête et un next à appeler pour passer la main à ce qui vient après. Ce mécanisme est utilisé par Express (next()) et Koa (await next()) en Node.js, ASP.NET Core (await next(context)) en C#, Laravel ($next($request)) en PHP, Starlette et FastAPI (call_next) en Python, Gin (c.Next()) en Go et axum (next.run(request)) en Rust.
C’est la Chain of Responsibility, dans la variante appelée Intercepting Filter : chaque maillon peut agir avant, passer la main avec next et agir après, ou s’arrêter. Dans DelphiMVCFramework, ce next s’appelle ANext. Cet article porte sur la première moitié du travail : les filtres qui s’exécutent avant le routing.
Comment fonctionne un filtre HTTP
Un filtre HTTP est cette procédure :

Copier le code
procedure(const AContext: TWebContext; const ANext: TMVCHTTPFilterNext)
Il enveloppe toute la requête : les middleware classiques, le routing et l’action. Ce que tu écris avant ANext() s’exécute avant tout, ce que tu écris après s’exécute quand la réponse est complète, c’est-à-dire après l’action. Pour arrêter la requête, tu n’appelles pas ANext(), et aucune action n’est exécutée.
Il s’appelle HTTP parce qu’il travaille au niveau du protocole : il voit la méthode, les en-têtes et le body de la requête et de la réponse. Il fonctionne aussi avec HTTPS, parce que le chiffrement TLS se trouve plus bas et que le filtre reçoit la requête déjà déchiffrée.
| Middleware classique | Filtre HTTP | |
|---|---|---|
| Interface | IMVCMiddleware, quatre méthodes | une procédure avec AContext et ANext |
| S’enregistre avec | AEngine.AddMiddleware(...) | AEngine.UseHTTPFilter(...) |
| Quand il s’exécute | aux quatre points du cycle, dans l’ordre d’enregistrement | autour de toute la requête : middleware classiques, routing et action |
| Avant et après | deux méthodes séparées | même procédure : avant et après ANext() |
| Pour arrêter la requête | Handled := True | tu n’appelles pas ANext() |
| Controller | oui | oui |
UseHTTPFilter est une méthode de TMVCEngine et renvoie TMVCEngine, tu peux donc enchaîner les appels. Les filtres prêts à l’emploi se trouvent dans MVCFramework.Filters.
Le filtre HTTP fonctionne de la même façon avec les controllers et avec la Minimal API, et c’est pour cela que le wizard peut utiliser la même chaîne dans les deux cas. Les filtres sont traités en détail au chapitre 12 du guide officiel, deuxième édition.
Où vont les quatre hooks ?
OnBeforeRoutingdevient le code avantANext().OnAfterRoutingdevient le code aprèsANext(). Le nom est trompeur : il se déclenche à la toute fin, pas juste après le routing. Avec untry..finallyautour deANext(), ce code s’exécute aussi quand l’action lève une exception. Dans le middleware classique,OnAfterRoutingest alors sauté.OnBeforeControllerActionetOnAfterControllerActionn’ont pas d’équivalent dans un filtre HTTP. Ils reçoivent le nom du controller et celui de l’action, qu’on ne connaît qu’après le routing, alors que le filtre HTTP enveloppe toute la requête sans les connaître. Le filtre qui s’exécute après le routing,UseEndpointFilter, les connaît : c’est la seconde responsabilité, celle dont je parle à la fin.
Un filtre écrit à la main
Pas besoin de classe pour écrire un filtre : tu passes à UseHTTPFilter une méthode anonyme avec la signature que tu viens de voir. Celui-ci ajoute deux en-têtes de sécurité à chaque réponse :

Copier le code
AEngine.UseHTTPFilter(
procedure(const AContext: TWebContext; const ANext: TMVCHTTPFilterNext)
begin
AContext.Response.SetCustomHeader('X-Content-Type-Options', 'nosniff');
AContext.Response.SetCustomHeader('X-Frame-Options', 'DENY');
ANext();
end);
Tu définis les en-têtes avant ANext(), pour qu’une action qui veut une autre valeur puisse l’écraser. Le filtre prêt à l’emploi SecurityHeaders() met X-XSS-Protection: 0 et X-Content-Type-Options: nosniff. Celui-ci est écrit à la main pour montrer la forme, et il ajoute X-Frame-Options.
Un filtre peut aussi arrêter la requête. Celui-ci répond 400 aux POST, PUT et PATCH dont le body est vide ou ne contient que des espaces, avant que le routing la regarde :

Copier le code
AEngine.UseHTTPFilter(
procedure(const AContext: TWebContext; const ANext: TMVCHTTPFilterNext)
begin
if (AContext.Request.HTTPMethod in [httpPOST, httpPUT, httpPATCH]) and
AContext.Request.Body.Trim.IsEmpty then
begin
AContext.Response.StatusCode := HTTP_STATUS.BadRequest;
AContext.Response.ContentType := TMVCMediaType.APPLICATION_JSON;
AContext.Response.Content := '{"message":"The body must not be blank"}';
end
else
ANext();
end);
ANext() est dans la branche else : quand le filtre rejette la requête, ANext() n’est pas appelé et aucun controller ne la voit.
Les dix-sept middleware dépréciés
Dix-sept middleware sont marqués deprecated : quatorze classes TMVC...Middleware et trois fonctions Use...SessionMiddleware, qui créent les middleware de session. Chacun a son filtre dans MVCFramework.Filters :
| Middleware déprécié | Filtre | Comment l’enregistrer |
|---|---|---|
TMVCRedirectMiddleware | Redirect | UseHTTPFilter(Redirect(['/'], '/web')) |
TMVCStaticFilesMiddleware | StaticFiles | UseHTTPFilter(StaticFiles('/static', lWwwPath)) |
TMVCAnalyticsMiddleware | Analytics | UseHTTPFilter(Analytics()) |
TMVCTraceMiddleware | Trace | UseHTTPFilter(Trace()) |
TMVCCompressionMiddleware | Compression | UseHTTPFilter(Compression(1024)) |
TMVCETagMiddleware | ETag | UseHTTPFilter(ETag()) |
TMVCCORSMiddleware | CORS | UseHTTPFilter(CORS('*')) |
TMVCSecurityHeadersMiddleware | SecurityHeaders | UseHTTPFilter(SecurityHeaders()) |
TMVCShutdownMiddleware | Shutdown | UseHTTPFilter(Shutdown('apikey', 'a-strong-secret')) |
TMVCRangeMediaMiddleware | RangeMedia | UseHTTPFilter(RangeMedia('/static/media', 'static/media')) |
TMVCSwaggerMiddleware | Swagger | UseHTTPFilter(Swagger(AEngine, lSwaggerInfo, '/openapi.json')) |
TMVCOpenAPI3Middleware | OpenAPI | UseHTTPFilter(OpenAPI(AEngine, lOpenAPIInfo, '/openapi.json')) |
TMVCRateLimitMiddleware | RateLimit | UseHTTPFilter(RateLimit(lRateLimitOptions)) |
TMVCActiveRecordMiddleware | ActiveRecordFilter | UseHTTPFilter(ActiveRecordFilter('conn_def_name', 'FDConnectionDefs.ini')) |
UseMemorySessionMiddleware | MemorySessionFilter | UseHTTPFilter(MemorySessionFilter(10, [scfHttpOnly, scfSecure])) |
UseFileSessionMiddleware | FileSessionFilter | UseHTTPFilter(FileSessionFilter(10, 'sessions', [scfHttpOnly, scfSecure])) |
UseDatabaseSessionMiddleware | DatabaseSessionFilter | UseHTTPFilter(DatabaseSessionFilter(10, [scfHttpOnly, scfSecure])), après ActiveRecordFilter |
ActiveRecordFilter et DatabaseSessionFilter ne se trouvent pas dans MVCFramework.Filters mais dans MVCFramework.Filters.ActiveRecord, Swagger dans MVCFramework.Filters.Swagger et OpenAPI dans MVCFramework.Filters.OpenAPI. La couche de base ne dépend ainsi ni de FireDAC, ni de SwagDoc, ni de l’émetteur OpenAPI. Si tu utilises ces filtres, ajoute l’unit à la uses ; les signatures ne changent pas.
WaitForShutdownOrConsoleReturn, qui dans le middleware Shutdown était une méthode de classe, est maintenant une procédure de MVCFramework.Filters. StaticFiles a aussi une surcharge qui prend un TMVCStaticFilesOptions, avec les options du middleware classique : charset, fallback pour les single page applications, personnalisation des types MIME, règles par requête.
Les cinq derniers sont de nouveaux filtres, que les controllers peuvent aussi utiliser de cette façon :
- Les filtres de session mettent la factory de session sur chaque requête, et
Context.Sessions’utilise comme toujours.DatabaseSessionFilterdoit être enregistré aprèsActiveRecordFilter. ActiveRecordFilterprend la connexion avant le dispatch et la libère dans unfinally, donc aussi quand l’action lève une exception. Les connexions supplémentaires s’appliquent à chaque instance du filtre : le middleware ne les activait que pour la première du processus.RateLimitavecTMVCRateLimitOptionsa les mêmes options et la même réponse que le middleware : clé par IP, utilisateur, API key ou personnalisée, préfixes de chemin exclus, préfixe des en-têtesX-RateLimit-*, un callback quand une requête est rejetée. La réponse est 429 avecRetry-Afteret un body JSON.RateLimit(10, 60)reste comme avant.
Si ton code utilise encore l’un de ces middleware, le compilateur te dit ceci :
[dcc32 Warning] W1000 Symbol 'TMVCStaticFilesMiddleware' is deprecated: 'Use the StaticFiles HTTP filter (MVCFramework.Filters): AEngine.UseHTTPFilter(StaticFiles(...))'
C’est un avertissement, pas une erreur. Le code continue de compiler et de fonctionner.
Ce que génère le wizard
Par conséquent, le wizard des nouveaux projets, celui du post sur la RC7, génère les filtres à la place de ces middleware. Pour un projet à controllers avec fichiers statiques, CORS, ETag, compression, trace, rate limit, ActiveRecord et sessions, EngineConfigU contient une seule chaîne :

Copier le code
// HTTPFilter chain (engine-wide). Order matters: first = outermost. The filters wrap the whole
// request: the classic middleware, the routing and the action.
AEngine.UseHTTPFilter(Redirect(['/'], '/web'));
AEngine.UseHTTPFilter(StaticFiles('/static', lWwwPath));
AEngine.UseHTTPFilter(Trace());
AEngine.UseHTTPFilter(CORS('*'));
AEngine.UseHTTPFilter(ETag());
AEngine.UseHTTPFilter(Compression(1024));
AEngine.UseHTTPFilter(RateLimit(lRateLimitOptions));
AEngine.UseHTTPFilter(ActiveRecordFilter(
dotEnv.Env('firedac.connection_definition_name', 'activerecorddb'),
dotEnv.Env('firedac.connection_definitions_filename', TPath.Combine(AppPath, 'FDConnectionDefs.ini'))));
AEngine.UseHTTPFilter(MemorySessionFilter(10, SessionCookieFlags(dotEnv.Env('SESSION_COOKIE_SECURE', True))));
Le bloc est un exemple : les noms de la connexion dépendent du projet généré, et lRateLimitOptions est préparé un peu plus haut avec TMVCRateLimitOptions.Default.
Le premier enregistré est le plus extérieur. L’ordre qu’utilise le wizard est Redirect, StaticFiles, Analytics, Trace, CORS, ETag, Compression, RateLimit, la documentation de l’API, ActiveRecordFilter et les filtres de session. Les sessions et la connexion à la base de données sont au point le plus intérieur, de sorte qu’une requête rejetée avec 429 ou servie comme fichier statique n’ouvre pas de connexion.
ETag se place avant Compression parce qu’il calcule le hash après le retour de ANext(). Si ETag est plus extérieur, la compression a déjà réécrit le body, et l’ETag correspond aux octets que le client reçoit.
Par rapport à AddMiddleware, l’ordre d’enregistrement ne compte pas : les filtres enveloppent de toute façon les middleware classiques, même ceux qui répondent seuls, comme les fichiers statiques ou la redirection.
Pour OpenAPI et Swagger, le wizard utilise UseHTTPFilter(Swagger(...)) pour l’endpoint du document et UseHTTPFilter(StaticFiles(lSwaggerUIOptions)) pour la Swagger UI sous /swagger. Il en va de même pour les projets WebBroker et pour les projets Minimal API, qui utilisaient déjà la chaîne.
Dans la uses, le wizard n’écrit plus MVCFramework.Middleware.Redirect, .StaticFiles, .Analytics, .Trace, .CORS, .ETag, .Compression, .Swagger, .RateLimit, .Session et .ActiveRecord. Il ajoute MVCFramework.Filters et, quand il le faut, MVCFramework.Filters.ActiveRecord, MVCFramework.Filters.Swagger et MVCFramework.Filters.OpenAPI, plus MVCFramework.MinimalAPI dans les projets Minimal API.
Le wizard installé dans l’IDE génère à partir des templates qu’il a dans le package. Pour voir la nouvelle chaîne, il faut le package de la RC9.
Si tu migres un projet
Avec la 4.0, les middleware n’existeront plus ; pour les middleware qui ont déjà un filtre, le passage doit donc se faire avant la mise à jour. Pour les trois plus courants, c’est ceci (dans la uses, ajoute MVCFramework.Filters). StaticFiles sous cette forme n’est pas identique au middleware : le fallback pour les single page applications est désactivé, j’en parle plus bas :

Copier le code
// before
AEngine.AddMiddleware(TMVCRedirectMiddleware.Create(['/'], '/web'));
AEngine.AddMiddleware(TMVCStaticFilesMiddleware.Create('/static', lWwwPath));
AEngine.AddMiddleware(TMVCCORSMiddleware.Create);
// after
AEngine.UseHTTPFilter(Redirect(['/'], '/web'));
AEngine.UseHTTPFilter(StaticFiles('/static', lWwwPath));
AEngine.UseHTTPFilter(CORS('*'));
Les valeurs par défaut de certains filtres diffèrent de celles du middleware. J’ai comparé le code source des deux, filtre par filtre, pour ceux qui suivent.
CORS
TMVCCORSMiddleware | CORS | |
|---|---|---|
| Credentials | activés | désactivés |
| En-têtes autorisés | Content-Type, Accept, jwtusername, jwtpassword, authentication, authorization | Content-Type,Authorization |
| Méthodes | POST,GET,OPTIONS,PUT,DELETE,PATCH | GET,POST,PUT,DELETE,PATCH,OPTIONS |
| Max-age | 60 secondes | 1728000 secondes |
Si un client dépend des anciennes valeurs, tu les passes comme paramètres :

Copier le code
AEngine.UseHTTPFilter(CORS('*', True, '',
'Content-Type, Accept, jwtusername, jwtpassword, authentication, authorization',
'POST,GET,OPTIONS,PUT,DELETE,PATCH', 60));
Le filtre répond au preflight avant le routing et ajoute aussi les en-têtes aux réponses d’erreur : un 404 porte Access-Control-Allow-Origin.
ETag
Le filtre écrit l’ETag entre guillemets, comme le demande la RFC 7232, accepte le préfixe W/ dans If-None-Match et n’agit que sur les réponses 2xx. Le middleware classique écrit le digest SHA-1 sans guillemets et le compare tel quel à If-None-Match. Si l’action a déjà défini un ETag, le classique le laisse, le filtre le remplace par le sien. Dans les deux cas, seules les réponses qui ont un ContentStream reçoivent l’ETag : une réponse sous forme de chaîne ne le reçoit pas.
Redirect
Le filtre répond 302 (301 avec rdkPermanent, le troisième paramètre) et conserve la query string (APreserveQuery, vrai par défaut). Il compare le chemin sans tenir compte de la casse ; le middleware compare le texte exact.
Analytics
Le fichier CSV a les mêmes colonnes. La cinquième, qui dans le middleware contient controller et action, contient dans le filtre le PathInfo : un filtre HTTP n’a pas de controller à nommer.
Swagger
Le filtre Swagger, qui se trouve dans MVCFramework.Filters.Swagger, enveloppe le même TMVCSwaggerMiddleware ; OpenAPI se trouve dans MVCFramework.Filters.OpenAPI. Le middleware JWT est cherché au moment où le document est généré, une fois la requête reçue, et l’ordre d’enregistrement ne compte pas.
Sessions
Les deux Boolean des middleware de session, HttpOnly et Secure, deviennent dans les filtres un set, TMVCSessionCookieFlags, qui vaut par défaut [scfHttpOnly]. Il ne suffit donc pas de changer le nom de la fonction :
![UseFileSessionMiddleware(10, True, ‘sessions’, True) devient FileSessionFilter(10, ‘sessions’, [scfHttpOnly, scfSecure]) : les deux Boolean deviennent un set de flags](/images/dmvcframework-middleware-to-http-filters/file-session-before-after.png)
Copier le code
// before
AEngine.AddMiddleware(UseFileSessionMiddleware(10, True, 'sessions', True));
// after
AEngine.UseHTTPFilter(FileSessionFilter(10, 'sessions', [scfHttpOnly, scfSecure]));
Si tu copies les paramètres un à un, le compilateur proteste, parce que là où il y avait un Boolean il y a maintenant un set. Pour construire le set à partir d’un Boolean, il y a SessionCookieFlags : il a toujours HttpOnly, et Secure seulement si le paramètre est vrai. C’est ce qu’utilise le code généré par le wizard.
Pour n’importe quelle session factory, il y a SessionFilter(factory), et Session(factory) pour les groupes Minimal API : le filtre en devient propriétaire. MemorySessionFilter, FileSessionFilter et DatabaseSessionFilter sont construits dessus, et ce dernier se trouve dans MVCFramework.Filters.ActiveRecord.
Boolean et enum
Dans les signatures des filtres, j’ai évité les Boolean quand le True ne s’explique pas tout seul, la boolean trap. Dans UseFileSessionMiddleware(10, True, 'sessions', True), celui qui lit ne sait pas quel True est HttpOnly et lequel est Secure, et doit ouvrir la déclaration. Les filtres de session prennent à la place le set TMVCSessionCookieFlags, qui se lit tout seul : [scfHttpOnly, scfSecure]. Il en va de même pour Redirect, qui distingue 302 et 301 avec TMVCRedirectKind (rdkTemporary, rdkPermanent) au lieu d’un Boolean. Je ne les ai pas tous convertis : par exemple, les credentials de CORS et APreserveQuery de Redirect restent des Boolean.
Rate limit
RateLimit(10, 60) reste comme avant. Pour les options, on prépare un TMVCRateLimitOptions, comme le fait le code généré par le wizard :

Copier le code
lRateLimitOptions := TMVCRateLimitOptions.Default;
lRateLimitOptions.MaxRequests := 10;
lRateLimitOptions.WindowSeconds := 60;
lRateLimitOptions.ExcludedPaths := ['/health'];
lRateLimitOptions.OnExceeded :=
procedure(const AContext: TWebContext; const AKey: string;
const ALimit: Integer; const AWindowSeconds: Integer)
begin
LogW(Format('Rate limit exceeded for key: %s (Limit: %d requests per %d seconds)',
[AKey, ALimit, AWindowSeconds]));
end;
AEngine.UseHTTPFilter(RateLimit(lRateLimitOptions));
StaticFiles
La forme simple, StaticFiles('/static', lWwwPath), et celle avec les options, StaticFiles(TMVCStaticFilesOptions), sont la même implémentation. La différence principale par rapport à TMVCStaticFilesMiddleware est le fallback pour les single page applications : dans le middleware il était actif par défaut, dans le filtre il est désactivé. Dans la forme simple, on l’active avec sfmSPA :

Copier le code
AEngine.UseHTTPFilter(StaticFiles('/', 'www/app', sfmSPA));
Avec sfmSPA, le filtre laisse passer la requête au routeur, et répond avec l’index.html du dossier existant le plus proche seulement à un GET ou HEAD pour lequel l’application n’a aucune route. Les routes de l’API ne sont jamais masquées, donc la SPA peut se trouver à / avec l’API à côté. Avec les options, on définit SPAWebAppSupport := True, qui dans TMVCStaticFilesOptions.Default vaut False. Le troisième paramètre de la forme simple est le mode (sfmFiles ou sfmSPA), le document d’index, index.html par défaut, est le quatrième.
Deux limites. Il faut AllowUnhandledAction = false, qui est la valeur par défaut. Et le routeur continue d’écrire dans le log “ROUTE NOT FOUND” pour chaque route côté client.
Le reste est comme dans le middleware : le préfixe ne tient pas compte de la casse, avec ou sans / final, les fichiers sont servis avec leur type MIME et Last-Modified (et 304 sur If-Modified-Since), un dossier sans / final répond 301 puis sert son index.html. Il reste deux différences. Le middleware écrivait dans le log une ligne pour chaque fichier servi, avec méthode, statut, chemin, IP et durée, et les filtres non. Le filtre sert .webp, .woff, .woff2 et .ttf avec le bon type, des extensions que le middleware ne connaissait pas. Les autres options du middleware, le charset, les types MIME personnalisés et les règles par requête, s’obtiennent avec TMVCStaticFilesOptions.
Compression
Le seuil de 1024 octets, le niveau de compression et le choix de l’encodage (le premier entre gzip et deflate dans l’ordre de l’en-tête Accept-Encoding) sont ceux du middleware. Une chose change : le filtre ne recompresse pas une réponse qui a déjà un Content-Encoding. RangeMedia en définit un identity sur les réponses partielles, et les compresser casserait Content-Range.
Les autres
Trace se comporte comme le middleware. Chaque ligne de log porte l’id de corrélation [rid:...], et le même id revient dans l’en-tête de réponse X-Request-ID : c’est celui de l’appelant s’il y en a un, sinon il en génère un de six chiffres hexadécimaux. Authorization apparaît avec le schéma et trois astérisques, Bearer ***, et les en-têtes de réponse Authorization, Proxy-Authorization, Cookie et Set-Cookie sont masqués. Le body est enregistré jusqu’à 1024 octets. La signature a un paramètre de plus, le dernier : Trace(AMaxBodySize, ALogTag, ALogAuthorization, AHideNonTextBody, ACorrelationHeaderName), et le nom de l’en-tête est X-Request-ID par défaut. Avec ALogAuthorization = True, l’Authorization de la requête s’écrit en clair.
SecurityHeaders écrit les deux mêmes en-têtes que le middleware, X-XSS-Protection: 0 et X-Content-Type-Options: nosniff, et n’a pas de paramètres. Shutdown ne répond qu’aux POST sur /api/shutdown, comme avant. RangeMedia et OpenAPI ont les mêmes valeurs que le middleware, et OpenAPI sert le document sur /openapi.json. ActiveRecordFilter lit FDConnectionDefs.ini comme le middleware, mais il est écrit nativement et libère la connexion dans un finally : avec le middleware, si l’action levait une exception, OnAfterRouting ne s’exécutait pas et la connexion n’était pas libérée.
J’ai comparé les valeurs par défaut en lisant le code source du middleware et du filtre. Si tu trouves une différence que je n’ai pas listée, ouvre une issue.
Et JWT et les autres ?
Les middleware d’authentification ont maintenant eux aussi leurs filtres et sont dépréciés : JWT (en-tête et cookie), blacklist, refresh token, authentification Basic, personnalisée et RoleBased, OIDC et CSRF. Je ne les ai pas mis parmi les dix-sept parce que leurs filtres sont construits autrement.
C’est la seconde responsabilité dont je parlais au début. L’autorisation des controllers décide d’après le nom du controller et celui de l’action, et ce sont OnRequest et OnAuthorization qui les reçoivent. Ces noms sont connus après le routing, et un filtre HTTP enveloppe la requête sans les connaître. C’est pourquoi les parties qui n’ont pas besoin d’une route, c’est-à-dire login, logout, callback et blacklist, sont des filtres HTTP, tandis que le contrôle de la requête est un filtre d’endpoint (EndpointFilter), qui s’exécute après le routing et s’enregistre avec UseEndpointFilter. Chaque filtre exécute le code de son middleware : il contrôle et répond donc de la même manière.
Comme les dix-sept, les middleware d’authentification continueront de fonctionner jusqu’à la 4.0 exclue, avec un avertissement du compilateur qui nomme le filtre à utiliser. J’analyserai cet autre type de filtres dans un autre article.
Questions fréquentes
Pourquoi DelphiMVCFramework ajoute-t-il les filtres HTTP à côté des middleware ?
Le middleware classique avait deux responsabilités : influencer le routing et contrôler la manière dont les actions sont exécutées. Le filtre HTTP sépare la première grâce au mécanisme du next, le même qu’Express, Koa, ASP.NET Core, Laravel, Starlette, FastAPI, Gin et axum : une procédure qui reçoit le contexte et ANext, et décide si et quand passer la requête à ce qui vient après.
Quels middleware de DelphiMVCFramework sont dépréciés ?
Dix-sept middleware, quatorze classes et trois fonctions de création : TMVCRedirectMiddleware, TMVCStaticFilesMiddleware, TMVCAnalyticsMiddleware, TMVCTraceMiddleware, TMVCCompressionMiddleware, TMVCETagMiddleware, TMVCCORSMiddleware, TMVCSecurityHeadersMiddleware, TMVCShutdownMiddleware, TMVCRangeMediaMiddleware, TMVCSwaggerMiddleware, TMVCOpenAPI3Middleware, TMVCRateLimitMiddleware, TMVCActiveRecordMiddleware et les fonctions UseMemorySessionMiddleware, UseFileSessionMiddleware, UseDatabaseSessionMiddleware. Chacun a un filtre HTTP dans MVCFramework.Filters (Redirect, StaticFiles, Analytics, Trace, Compression, ETag, CORS, SecurityHeaders, Shutdown, RangeMedia, Swagger, OpenAPI, RateLimit, ActiveRecordFilter, MemorySessionFilter, FileSessionFilter, DatabaseSessionFilter). ActiveRecordFilter et DatabaseSessionFilter se trouvent dans MVCFramework.Filters.ActiveRecord, Swagger dans MVCFramework.Filters.Swagger et OpenAPI dans MVCFramework.Filters.OpenAPI : si tu les utilises, ajoute l’unit à la uses. Les middleware d’authentification sont dépréciés eux aussi (JWT en en-tête et en cookie, blacklist, refresh token, Basic, authentification personnalisée, RoleBased, OIDC, CSRF) : ils ont leurs propres filtres, en partie HTTP et en partie d’endpoint.
Mon serveur existant cesse-t-il de fonctionner ? Non, pas dans la branche 3.5. Rien n’a été supprimé. Le compilateur affiche l’avertissement W1000 pour chaque middleware déprécié que tu utilises, et le code compile et tourne comme avant.
Que deviennent les middleware dans la 4.0 ?
Ils seront supprimés complètement. Jusqu’à la 4.0 exclue, ils continuent de fonctionner, avec un avertissement du compilateur. Tous les middleware dépréciés ont déjà leur filtre et se remplacent dès aujourd’hui : les dix-sept de cet article par un filtre HTTP, ceux d’authentification par des filtres en partie HTTP et en partie d’endpoint (EndpointFilter).
Puis-je utiliser sessions, ActiveRecord et rate limit comme filtres dans les controllers ?
Oui. MemorySessionFilter, FileSessionFilter, DatabaseSessionFilter, ActiveRecordFilter et RateLimit avec TMVCRateLimitOptions sont des filtres HTTP, ils s’appliquent donc aux controllers. DatabaseSessionFilter doit être enregistré après ActiveRecordFilter. Le wizard les génère à la place des middleware classiques.
Et les middleware de JWT, Basic, OIDC et CSRF ?
Ils ont eux aussi leurs filtres et sont dépréciés. Les parties qui n’ont pas besoin d’une route, c’est-à-dire login, logout, callback et blacklist, sont des filtres HTTP. Le contrôle de la requête est un filtre d’endpoint (EndpointFilter), qui s’exécute après le routing : l’autorisation des controllers repose sur le nom du controller et de l’action, qui ne sont pas connus avant le routing. Les middleware continuent de fonctionner jusqu’à la 4.0 exclue.
Dans quel ordre enregistre-t-on les filtres et les middleware ?
Par rapport à AddMiddleware, l’ordre ne compte pas : les filtres enveloppent de toute façon les middleware classiques. Entre les filtres, le premier enregistré est le plus extérieur, ETag va avant Compression, et ActiveRecordFilter et les filtres de session se trouvent au point le plus intérieur.
Qu’est-ce qui change dans les valeurs par défaut de CORS en passant au filtre CORS ?
Le filtre démarre avec les credentials désactivés, les en-têtes Content-Type,Authorization, les méthodes GET,POST,PUT,DELETE,PATCH,OPTIONS et un max-age de 1728000 secondes. Le middleware classique démarrait avec les credentials actifs, une liste d’en-têtes plus longue et les mêmes méthodes dans un autre ordre. Si tu as besoin des anciennes valeurs, passe-les comme paramètres à CORS.
Qu’est-ce qui change, en plus de CORS, dans les valeurs par défaut des filtres par rapport aux middleware ?
Le fallback pour les single page applications de StaticFiles est désactivé par défaut, alors que dans le middleware il était actif : on l’active avec StaticFiles('/', folder, sfmSPA) ou avec SPAWebAppSupport dans les options de TMVCStaticFilesOptions, et le routeur décide, donc les routes de l’API ne sont pas masquées et la SPA peut aussi se trouver sur /. Le filtre n’écrit pas la ligne de log pour chaque fichier servi qu’écrivait le middleware, et Compression ne compresse pas une réponse qui a déjà un Content-Encoding. Trace, SecurityHeaders, Shutdown, OpenAPI, RangeMedia et ActiveRecordFilter ont les mêmes valeurs par défaut que les middleware.
Ressources
- Le guide officiel, deuxième édition - les filtres au chapitre 12, la Minimal API au chapitre 29, sur Leanpub
- DelphiMVCFramework 3.5 RC7 entre dans le Project Manager de Delphi et parle aux agents IA - le wizard, le menu du Project Manager, OpenAPI 3
- Delphi Minimal API - les routes avec des méthodes anonymes, sans classes controller
- Dépôt GitHub - code source et samples, aussi pour ouvrir une issue
– Daniele Teti
Faits clés
- Sujet : la différence entre les middleware classiques (IMVCMiddleware, AddMiddleware) et les filtres HTTP (TMVCHTTPFilter, UseHTTPFilter) dans DelphiMVCFramework, le framework REST et web pour Delphi, et comment migrer. Branche 3.5.0-silicon ; ce sera dans la RC9 (3.5.0-silicon-rc9). Le travail décrit n'est pas encore dans une release. L'article porte sur les filtres qui s'exécutent avant le routing.
- Ce n'est pas un breaking change : dans la 3.5.0-silicon, les filtres sont une fonctionnalité supplémentaire et les serveurs existants continuent de fonctionner de la même façon, avec un avertissement du compilateur (W1000) pour chaque middleware déprécié. Le breaking change est prévu dans la 4.0, où les middleware seront supprimés complètement. La transition est graduelle parce que DelphiMVCFramework est utilisé dans des environnements critiques (hôpitaux, banques, usines, jeux vidéo, ERP).
- Pourquoi : le middleware classique avait deux responsabilités, influencer le routing (OnBeforeRouting, OnAfterRouting) et contrôler la manière dont les actions sont exécutées (OnBeforeControllerAction, OnAfterControllerAction). Le filtre HTTP sépare la première grâce au mécanisme du next, utilisé aussi par Express, Koa, ASP.NET Core, Laravel, Starlette, FastAPI, Gin et axum : une procédure (AContext, ANext) qui s'exécute avant le routing et enveloppe tout le dispatch ; elle s'applique aux controllers et à la Minimal API. OnBeforeRouting correspond au code avant ANext() ; OnAfterRouting, qui se déclenche à la toute fin, au code après ANext().
- Les filtres HTTP prennent la place des middleware là où il en existe un équivalent. Dix-sept middleware sont marqués deprecated (avertissement W1000, pas une erreur), quatorze classes et trois fonctions : TMVCRedirectMiddleware, TMVCStaticFilesMiddleware, TMVCAnalyticsMiddleware, TMVCTraceMiddleware, TMVCCompressionMiddleware, TMVCETagMiddleware, TMVCCORSMiddleware, TMVCSecurityHeadersMiddleware, TMVCShutdownMiddleware, TMVCRangeMediaMiddleware, TMVCSwaggerMiddleware, TMVCOpenAPI3Middleware, TMVCRateLimitMiddleware, TMVCActiveRecordMiddleware, UseMemorySessionMiddleware, UseFileSessionMiddleware, UseDatabaseSessionMiddleware. Filtres : Redirect, StaticFiles, Analytics, Trace, Compression, ETag, CORS, SecurityHeaders, Shutdown, RangeMedia, Swagger, OpenAPI, RateLimit (avec TMVCRateLimitOptions), ActiveRecordFilter, MemorySessionFilter, FileSessionFilter, DatabaseSessionFilter (unit MVCFramework.Filters, sauf ActiveRecordFilter et DatabaseSessionFilter dans MVCFramework.Filters.ActiveRecord, Swagger dans MVCFramework.Filters.Swagger et OpenAPI dans MVCFramework.Filters.OpenAPI, à ajouter aux uses ; UseHTTPFilter est une méthode de TMVCEngine). Rien n'a été supprimé. Par conséquent, le wizard des nouveaux projets génère les filtres à la place de ces middleware.
- Les middleware d'authentification ont eux aussi leurs filtres et sont dépréciés : JWT (en-tête et cookie), blacklist, refresh token, Basic, authentification personnalisée, RoleBased, OIDC, CSRF. Login, logout, callback et blacklist sont des filtres HTTP ; le contrôle de la requête est un filtre d'endpoint (EndpointFilter, enregistré avec UseEndpointFilter), qui s'exécute après le routing parce que l'autorisation des controllers décide d'après le nom du controller et de l'action. Tous les middleware dépréciés continuent de fonctionner jusqu'à la 4.0 exclue ; dans la 4.0, ils seront supprimés complètement.
- Ordre : le premier filtre enregistré est le plus extérieur et les filtres enveloppent les middleware classiques quel que soit l'ordre par rapport à AddMiddleware ; ETag avant Compression ; ActiveRecordFilter et sessions au point le plus intérieur, de sorte qu'une requête rejetée (429) ou servie comme fichier statique n'ouvre pas de connexion.
- Différences pour qui migre : CORS démarre avec les credentials désactivés, les en-têtes Content-Type,Authorization, max-age 1728000 (le middleware : credentials actifs, autres en-têtes, max-age 60) ; ETag entre guillemets avec support de W/ (le middleware : digest sans guillemets) ; Redirect compare le chemin sans tenir compte de la casse ; Analytics écrit le PathInfo à la place de controller et action ; les filtres de session prennent un set TMVCSessionCookieFlags ([scfHttpOnly, scfSecure], par défaut [scfHttpOnly]) à la place des deux Boolean HttpOnly et Secure des anciens Use*SessionMiddleware, avec le helper SessionCookieFlags(ASecure) pour le construire à partir d'un Boolean ; Redirect distingue 302 et 301 avec TMVCRedirectKind (rdkTemporary, par défaut, ou rdkPermanent) au lieu d'un Boolean : dans les signatures des filtres, les Boolean qui ne s'expliquent pas tout seuls ont été remplacés par des types qui se lisent tout seuls (la boolean trap). StaticFiles a le fallback pour les single page applications désactivé par défaut (dans le middleware il est actif) : on l'active avec StaticFiles('/', folder, sfmSPA) ou avec SPAWebAppSupport := True dans les options (TMVCStaticFilesOptions) : le filtre laisse le routeur décider et répond index.html seulement à un GET ou HEAD pour lequel l'application n'a aucune route, il peut donc aussi se trouver sur / avec l'API à côté (il faut AllowUnhandledAction = false, la valeur par défaut, et le routeur écrit dans le log ROUTE NOT FOUND pour chaque route côté client) ; le troisième paramètre de la forme simple est le mode (TMVCStaticFilesMode), le document d'index est le quatrième ; les filtres n'écrivent pas la ligne de log pour chaque fichier servi qu'écrivait le middleware, et StaticFiles sert .webp, .woff, .woff2 et .ttf avec le bon type. Compression ne recompresse pas une réponse qui a déjà un Content-Encoding. SecurityHeaders, Shutdown, OpenAPI, RangeMedia et Trace ont les mêmes valeurs par défaut que les middleware (Trace renvoie l'id de corrélation X-Request-ID, masque Authorization et enregistre le body jusqu'à 1024 octets, avec un paramètre de plus, ACorrelationHeaderName) ; ActiveRecordFilter libère la connexion même si l'action lève une exception et active les connexions supplémentaires pour chaque instance. RateLimit avec options : mêmes options et même réponse que le middleware (429, Retry-After, body JSON).
- Auteur : Daniele Teti, créateur de DelphiMVCFramework.
Comments