DelphiMVCFramework 3.5.0-silicon: von Middleware zu Filtern, was wird sich für deinen Server ändern?
🇮🇹 Italiano • 🇬🇧 English • 🇪🇸 Español • 🇫🇷 Français • 🇧🇷 Português

Eine klassische Middleware in DelphiMVCFramework hat vier Methoden. Ein HTTP-Filter hat nur eine Prozedur. Warum habe ich einen Mechanismus geändert, der seit Jahren funktionierte? Wohin wandern die vier Hooks, in welcher Reihenfolge registrierst du die Filter, und was passiert mit dem Server, den du schon in Produktion hast?
Seit Jahren sind in DelphiMVCFramework Middlewares der Weg, Code in den Ablauf einer Anfrage einzuhängen: mit AddMiddleware. Die HTTP-Filter, mit UseHTTPFilter, kamen mit der Minimal API von 3.5 dazu, die im Post zur RC6 beschrieben ist. Jetzt übernehmen die Filter den Platz der Middlewares, wo es eine gleichwertige gibt: Siebzehn Middlewares sind als deprecated markiert, jede mit ihrem Filter. Das wird in der RC9 von 3.5.0-silicon enthalten sein.
✅ Das ist kein Breaking Change. Deine bestehenden Server laufen genauso weiter, ohne Änderungen und ohne Probleme.
In 3.5.0-silicon kommen die Filter als zusätzliche Funktion hinzu: Die Middlewares bleiben, wo sie sind, und der Compiler zeigt für jede als deprecated markierte Middleware eine Warnung. Der Breaking Change kommt mit 4.0, wo die Middlewares vollständig entfernt werden.
DelphiMVCFramework ist auf GitHub das beliebteste Framework seiner Art für Delphi, mit über 50 Contributors, und es wird in kritischen Umgebungen eingesetzt: Krankenhäuser, Banken, Fabriken, Videospiele, ERP-Systeme mit Hunderten von Benutzern. Deshalb habe ich mich für einen sanften, schmerzfreien Übergang entschieden.
In diesem Artikel geht es um die HTTP-Filter, die vor dem Routing laufen.
Zwei Verantwortlichkeiten in einer Klasse
Bisher machte eine Middleware zwei Dinge. Mit OnBeforeRouting und OnAfterRouting nahm sie Einfluss auf das Routing: Sie konnte antworten, bevor der Router die Anfrage ansah, um eine statische Datei auszuliefern, einen Redirect zu schicken oder einen CORS-Preflight zu beantworten, und am Ende der Anfrage die Antwort beobachten oder ändern, wie es ETag und Analytics tun. Mit OnBeforeControllerAction und OnAfterControllerAction steuerte sie, wie die Actions ausgeführt wurden. Vier Methoden in einer einzigen Klasse, für zwei verschiedene Aufgaben.
Wer mich kennt oder einen meiner Kurse besucht hat, weiß, wie wichtig mir die SOLID-Prinzipien sind. Eine Klasse mit zwei Aufgaben in meinem beliebtesten Open-Source-Projekt zu finden, hat mich wirklich gestört. Jedes Mal, wenn ich eine Middleware in die Hand nahm, sah ich es: Entweder nutzte man das Methodenpaar des Routings (OnBeforeRouting und OnAfterRouting) und ließ die anderen beiden leer, oder man implementierte das Paar des Endpoints (OnBeforeControllerAction und OnAfterControllerAction), das Controller und Action schon kennt, weil es nach dem Routing läuft. Es war offensichtlich, dass dieser Baustein refaktoriert werden musste. Jetzt ist der Moment gekommen.
Das ist eine Verletzung des Single-Responsibility-Prinzips (SRP). Ich habe sie mit einem Mechanismus gelöst, den es in vielen Frameworks gibt: eine Funktion, die die Anfrage und ein next bekommt, das sie aufruft, um an das nächste Glied in der Kette zu übergeben. Ihn benutzen Express (next()) und Koa (await next()) in Node.js, ASP.NET Core (await next(context)) in C#, Laravel ($next($request)) in PHP, Starlette und FastAPI (call_next) in Python, Gin (c.Next()) in Go und axum (next.run(request)) in Rust.
Das ist die Chain of Responsibility in der Variante namens Intercepting Filter: Jedes Glied kann vorher handeln, mit next weiterreichen und danach handeln, oder anhalten. In DelphiMVCFramework heißt dieses next ANext. Dieser Artikel behandelt die erste Hälfte der Arbeit: die Filter, die vor dem Routing laufen.
Wie ein HTTP-Filter funktioniert
Ein HTTP-Filter ist diese Prozedur:

Code kopieren
procedure(const AContext: TWebContext; const ANext: TMVCHTTPFilterNext)
Er umhüllt die gesamte Anfrage: die klassischen Middlewares, das Routing und die Action. Was du vor ANext() schreibst, läuft vor allem anderen. Was du danach schreibst, läuft, wenn die Antwort fertig ist, also nach der Action. Um die Anfrage anzuhalten, rufst du ANext() nicht auf, und keine Action wird ausgeführt.
Er heißt HTTP-Filter, weil er auf Protokollebene arbeitet: Er sieht Methode, Header und Body von Anfrage und Antwort. Er funktioniert auch mit HTTPS, denn die TLS-Verschlüsselung liegt weiter unten und der Filter bekommt die Anfrage bereits entschlüsselt.
| Klassische Middleware | HTTP-Filter | |
|---|---|---|
| Schnittstelle | IMVCMiddleware, vier Methoden | eine Prozedur mit AContext und ANext |
| Registrierung mit | AEngine.AddMiddleware(...) | AEngine.UseHTTPFilter(...) |
| Ausführung | an den vier Punkten des Ablaufs, in der Reihenfolge der Registrierung | um die gesamte Anfrage herum: klassische Middlewares, Routing und Action |
| Davor und danach | zwei getrennte Methoden | dieselbe Prozedur: vor und nach ANext() |
| Um die Anfrage anzuhalten | Handled := True | du rufst ANext() nicht auf |
| Controller | ja | ja |
UseHTTPFilter ist eine Methode von TMVCEngine und gibt TMVCEngine zurück, du kannst die Aufrufe also verketten. Die fertigen Filter findest du in MVCFramework.Filters.
Der HTTP-Filter funktioniert mit Controllern und mit der Minimal API auf dieselbe Weise, und genau deshalb kann der Wizard in beiden Fällen dieselbe Kette verwenden. Die Filter werden in Kapitel 12 des offiziellen Handbuchs, 2. Auflage ausführlich behandelt.
Wohin wandern die vier Hooks?
OnBeforeRoutingwird zum Code vorANext().OnAfterRoutingwird zum Code nachANext(). Der Name täuscht: Er feuert am Ende von allem, nicht gleich nach dem Routing. Mit einemtry..finallyumANext()läuft dieser Code auch dann, wenn die Action eine Exception auslöst. In der klassischen Middleware wirdOnAfterRoutingin diesem Fall übersprungen.OnBeforeControllerActionundOnAfterControllerActionhaben in einem HTTP-Filter keine Entsprechung. Sie bekommen den Namen des Controllers und den der Action, die man erst nach dem Routing kennt, während der HTTP-Filter die ganze Anfrage umhüllt, ohne sie zu kennen. Der Filter, der nach dem Routing läuft,UseEndpointFilter, kennt sie: Das ist die zweite Verantwortlichkeit, von der ich am Ende spreche.
Ein von Hand geschriebener Filter
Um einen Filter zu schreiben, brauchst du keine Klasse: Du übergibst UseHTTPFilter eine anonyme Methode mit der Signatur, die du gerade gesehen hast. Dieser hier fügt jeder Antwort zwei Sicherheits-Header hinzu:

Code kopieren
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);
Die Header setzt du vor ANext(), damit eine Action, die einen anderen Wert will, ihn überschreibt. Der fertige Filter SecurityHeaders() setzt X-XSS-Protection: 0 und X-Content-Type-Options: nosniff. Dieser hier ist von Hand geschrieben, um die Form zu zeigen, und fügt X-Frame-Options hinzu.
Ein Filter kann die Anfrage auch anhalten. Dieser antwortet mit 400 auf POST, PUT und PATCH, deren Body leer ist oder nur aus Leerzeichen besteht, bevor das Routing sie ansieht:

Code kopieren
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() steht im else-Zweig: Wenn der Filter die Anfrage ablehnt, wird ANext() nicht aufgerufen und kein Controller sieht sie.
Die siebzehn als deprecated markierten Middlewares
Siebzehn Middlewares sind als deprecated markiert: vierzehn Klassen TMVC...Middleware und drei Funktionen Use...SessionMiddleware, die die Session-Middlewares erzeugen. Jede hat ihren Filter in MVCFramework.Filters:
| Deprecated Middleware | Filter | So registrierst du ihn |
|---|---|---|
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])), nach ActiveRecordFilter |
ActiveRecordFilter und DatabaseSessionFilter stehen nicht in MVCFramework.Filters, sondern in MVCFramework.Filters.ActiveRecord, Swagger in MVCFramework.Filters.Swagger und OpenAPI in MVCFramework.Filters.OpenAPI. So hängt die Basisschicht weder von FireDAC noch von SwagDoc noch vom OpenAPI-Emitter ab. Wer diese Filter benutzt, fügt die Unit den uses hinzu, die Signaturen ändern sich nicht.
WaitForShutdownOrConsoleReturn, das in der Shutdown-Middleware eine Klassenmethode war, ist jetzt eine Prozedur in MVCFramework.Filters. StaticFiles hat außerdem eine Überladung, die ein TMVCStaticFilesOptions nimmt, mit den Optionen der klassischen Middleware: Charset, Fallback für Single-Page-Anwendungen, Anpassung der MIME-Typen, Regeln pro Anfrage.
Die letzten fünf sind neue Filter, die Controller auch so benutzen können:
- Die Session-Filter setzen die Session-Factory bei jeder Anfrage, und
Context.Sessionbenutzt du wie immer.DatabaseSessionFiltermuss nachActiveRecordFilterregistriert werden. ActiveRecordFilterholt die Verbindung vor dem Dispatch und gibt sie in einemfinallywieder frei, also auch dann, wenn die Action eine Exception auslöst. Die zusätzlichen Verbindungen gelten für jede Instanz des Filters: Die Middleware aktivierte sie nur für die erste des Prozesses.RateLimitmitTMVCRateLimitOptionshat dieselben Optionen und dieselbe Antwort wie die Middleware: Schlüssel nach IP, Benutzer, API-Key oder benutzerdefiniert, ausgeschlossene Pfadpräfixe, Präfix derX-RateLimit-*-Header, ein Callback, wenn eine Anfrage abgelehnt wird. Die Antwort ist 429 mitRetry-Afterund einem JSON-Body.RateLimit(10, 60)bleibt, wie es war.
Wenn dein Code noch eine dieser Middlewares benutzt, sagt dir der Compiler Folgendes:
[dcc32 Warning] W1000 Symbol 'TMVCStaticFilesMiddleware' is deprecated: 'Use the StaticFiles HTTP filter (MVCFramework.Filters): AEngine.UseHTTPFilter(StaticFiles(...))'
Das ist eine Warnung, kein Fehler. Der Code kompiliert und läuft weiter.
Was der Wizard generiert
Entsprechend generiert der Wizard für neue Projekte (der aus dem Post zur RC7) die Filter anstelle dieser Middlewares. Für ein Controller-Projekt mit statischen Dateien, CORS, ETag, Kompression, Trace, Rate Limit, ActiveRecord und Sessions enthält EngineConfigU eine einzige Kette:

Code kopieren
// 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))));
Der Block ist ein Beispiel: Die Verbindungsnamen hängen vom generierten Projekt ab, und lRateLimitOptions wird kurz davor mit TMVCRateLimitOptions.Default vorbereitet.
Der zuerst registrierte ist der äußerste. Die Reihenfolge, die der Wizard verwendet, ist Redirect, StaticFiles, Analytics, Trace, CORS, ETag, Compression, RateLimit, die API-Dokumentation, ActiveRecordFilter und die Session-Filter. Sessions und Datenbankverbindung stehen ganz innen, damit eine mit 429 abgelehnte oder als statische Datei ausgelieferte Anfrage keine Verbindung öffnet.
ETag steht vor Compression, weil er den Hash berechnet, nachdem ANext() zurückgekehrt ist. Ist ETag weiter außen, hat die Kompression den Body schon neu geschrieben, und der ETag entspricht den Bytes, die der Client erhält.
Ob du einen Filter vor oder nach AddMiddleware registrierst, spielt keine Rolle: Die Filter umhüllen die klassischen Middlewares in jedem Fall, auch die, die selbst antworten, wie die für statische Dateien oder den Redirect.
Für OpenAPI und Swagger verwendet der Wizard UseHTTPFilter(Swagger(...)) für den Endpoint des Dokuments und UseHTTPFilter(StaticFiles(lSwaggerUIOptions)) für die Swagger UI unter /swagger. Dasselbe gilt für WebBroker-Projekte und für Minimal-API-Projekte, die die Kette schon benutzten.
In den uses schreibt der Wizard nicht mehr MVCFramework.Middleware.Redirect, .StaticFiles, .Analytics, .Trace, .CORS, .ETag, .Compression, .Swagger, .RateLimit, .Session und .ActiveRecord. Er fügt MVCFramework.Filters hinzu und, wenn nötig, MVCFramework.Filters.ActiveRecord, MVCFramework.Filters.Swagger und MVCFramework.Filters.OpenAPI, dazu MVCFramework.MinimalAPI in Minimal-API-Projekten.
Der in der IDE installierte Wizard generiert aus den Templates, die im Package stecken. Um die neue Kette zu sehen, brauchst du das Package der RC9.
Wenn du ein Projekt migrierst
Mit 4.0 gibt es die Middlewares nicht mehr. Bei denen, die schon einen Filter haben, muss der Wechsel also vor dem Update passieren. Für die drei häufigsten sieht er so aus (in den uses fügst du MVCFramework.Filters hinzu). StaticFiles ist in dieser Form nicht identisch mit der Middleware: Der Fallback für Single-Page-Anwendungen ist aus, dazu weiter unten mehr:

Code kopieren
// 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('*'));
Die Standardwerte einiger Filter stimmen nicht mit denen der Middleware überein. Bei diesen habe ich den Quelltext von Middleware und Filter verglichen, Filter für Filter.
CORS
TMVCCORSMiddleware | CORS | |
|---|---|---|
| Credentials | aktiv | aus |
| Erlaubte Header | Content-Type, Accept, jwtusername, jwtpassword, authentication, authorization | Content-Type,Authorization |
| Methoden | POST,GET,OPTIONS,PUT,DELETE,PATCH | GET,POST,PUT,DELETE,PATCH,OPTIONS |
| Max-Age | 60 Sekunden | 1728000 Sekunden |
Wenn ein Client von den alten Werten abhängt, übergibst du sie als Parameter:

Code kopieren
AEngine.UseHTTPFilter(CORS('*', True, '',
'Content-Type, Accept, jwtusername, jwtpassword, authentication, authorization',
'POST,GET,OPTIONS,PUT,DELETE,PATCH', 60));
Der Filter beantwortet den Preflight vor dem Routing und fügt die Header auch den Fehlerantworten hinzu: Ein 404 trägt Access-Control-Allow-Origin.
ETag
Der Filter schreibt den ETag in Anführungszeichen, wie es RFC 7232 verlangt, akzeptiert das Präfix W/ in If-None-Match und wirkt nur auf 2xx-Antworten. Die klassische Middleware schreibt den SHA-1-Digest ohne Anführungszeichen und vergleicht ihn unverändert mit If-None-Match. Hat die Action schon einen ETag gesetzt, lässt die klassische Middleware ihn stehen, der Filter ersetzt ihn durch seinen. In beiden Fällen bekommen den ETag nur Antworten mit einem ContentStream: Eine String-Antwort bekommt ihn nicht.
Redirect
Der Filter antwortet mit 302 (301 mit rdkPermanent, dem dritten Parameter) und behält den Query-String bei (APreserveQuery, standardmäßig True). Er vergleicht den Pfad ohne Unterscheidung von Groß- und Kleinschreibung; die Middleware vergleicht den exakten Text.
Analytics
Die CSV-Datei hat dieselben Spalten. Die fünfte, die in der Middleware Controller und Action enthält, enthält im Filter den PathInfo: Ein HTTP-Filter hat keinen Controller, den er nennen könnte.
Swagger
Der Filter Swagger, der in MVCFramework.Filters.Swagger steht, umhüllt dieselbe TMVCSwaggerMiddleware; OpenAPI steht in MVCFramework.Filters.OpenAPI. Die JWT-Middleware wird gesucht, wenn das Dokument erzeugt wird, also wenn die Anfrage eingetroffen ist, und die Reihenfolge der Registrierung spielt keine Rolle.
Sessions
Die zwei Booleans der Session-Middlewares, HttpOnly und Secure, werden in den Filtern zu einem Set, TMVCSessionCookieFlags, das standardmäßig [scfHttpOnly] ist. Es reicht also nicht, den Namen der Funktion zu ändern:
![UseFileSessionMiddleware(10, True, ‘sessions’, True) wird zu FileSessionFilter(10, ‘sessions’, [scfHttpOnly, scfSecure]): Die zwei Booleans werden zu einem Set von Flags](/images/dmvcframework-middleware-to-http-filters/file-session-before-after.png)
Code kopieren
// before
AEngine.AddMiddleware(UseFileSessionMiddleware(10, True, 'sessions', True));
// after
AEngine.UseHTTPFilter(FileSessionFilter(10, 'sessions', [scfHttpOnly, scfSecure]));
Wenn du die Parameter eins zu eins übernimmst, meckert der Compiler, denn wo ein Boolean stand, steht jetzt ein Set. Um das Set aus einem Boolean zu bauen, gibt es SessionCookieFlags: Es setzt immer HttpOnly und Secure nur, wenn der Parameter True ist. Genau das benutzt der vom Wizard generierte Code.
Für eine beliebige Session-Factory gibt es SessionFilter(factory), und Session(factory) für Minimal-API-Gruppen: Der Filter wird ihr Besitzer. MemorySessionFilter, FileSessionFilter und DatabaseSessionFilter sind darauf aufgebaut, und der letzte steht in MVCFramework.Filters.ActiveRecord.
Boolean und Enum
In den Signaturen der Filter habe ich Booleans vermieden, wenn das True sich nicht von selbst erklärt (die Boolean Trap). In UseFileSessionMiddleware(10, True, 'sessions', True) weiß der Leser nicht, welches True HttpOnly und welches Secure ist, und muss die Deklaration öffnen. Die Session-Filter nehmen stattdessen das Set TMVCSessionCookieFlags, das sich von selbst liest: [scfHttpOnly, scfSecure]. Dasselbe gilt für Redirect, der 302 und 301 mit TMVCRedirectKind (rdkTemporary, rdkPermanent) statt mit einem Boolean unterscheidet. Ich habe nicht alle umgestellt: Booleans bleiben zum Beispiel die Credentials von CORS und APreserveQuery von Redirect.
Rate Limit
RateLimit(10, 60) bleibt, wie es war. Für die Optionen bereitest du ein TMVCRateLimitOptions vor, wie es der vom Wizard generierte Code tut:

Code kopieren
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
Die einfache Form, StaticFiles('/static', lWwwPath), und die mit den Optionen, StaticFiles(TMVCStaticFilesOptions), sind dieselbe Implementierung. Der Hauptunterschied zu TMVCStaticFilesMiddleware ist der Fallback für Single-Page-Anwendungen: In der Middleware war er standardmäßig aktiv, im Filter ist er aus. In der einfachen Form schaltest du ihn mit sfmSPA ein:

Code kopieren
AEngine.UseHTTPFilter(StaticFiles('/', 'www/app', sfmSPA));
Mit sfmSPA lässt der Filter die Anfrage an den Router durch und antwortet mit der index.html des nächstgelegenen existierenden Ordners nur auf ein GET oder HEAD, für das die Anwendung keine Route hat. Die Routen der API werden nie verdeckt, die SPA kann also auf / liegen, mit der API daneben. Mit den Optionen setzt du SPAWebAppSupport := True, was in TMVCStaticFilesOptions.Default den Wert False hat. Der dritte Parameter der einfachen Form ist der Modus (sfmFiles oder sfmSPA), das Indexdokument, standardmäßig index.html, ist der vierte.
Zwei Einschränkungen. Du brauchst AllowUnhandledAction = false, das ist der Standardwert. Und der Router schreibt weiterhin “ROUTE NOT FOUND” ins Log, für jede clientseitige Route.
Der Rest ist wie in der Middleware: Das Präfix unterscheidet nicht zwischen Groß- und Kleinschreibung, mit oder ohne abschließendes /, die Dateien werden mit MIME-Typ und mit Last-Modified ausgeliefert (und 304 auf If-Modified-Since), ein Ordner ohne abschließendes / antwortet mit 301 und liefert dann seine index.html aus. Zwei Unterschiede bleiben. Die Middleware schrieb für jede ausgelieferte Datei eine Zeile ins Log, mit Methode, Status, Pfad, IP und Dauer, die Filter nicht. Der Filter liefert .webp, .woff, .woff2 und .ttf mit dem richtigen Typ aus, die die Middleware nicht kannte. Die übrigen Optionen der Middleware, der Charset, benutzerdefinierte MIME-Typen und die Regeln pro Anfrage, bekommt man mit TMVCStaticFilesOptions.
Compression
Die Schwelle von 1024 Byte, die Kompressionsstufe und die Wahl der Kodierung (die erste von gzip und deflate in der Reihenfolge des Headers Accept-Encoding) sind die der Middleware. Eine Sache ändert sich: Der Filter komprimiert eine Antwort nicht, die schon ein Content-Encoding hat. RangeMedia setzt bei Teilantworten ein Content-Encoding mit identity, und sie zu komprimieren würde Content-Range kaputt machen.
Die anderen
Trace verhält sich wie die Middleware. Jede Logzeile trägt die Korrelations-ID [rid:...], und dieselbe ID kommt im Antwort-Header X-Request-ID zurück: Es ist die des Aufrufers, falls vorhanden, sonst wird eine aus sechs Hexadezimalziffern erzeugt. Authorization erscheint mit dem Schema und drei Sternchen, Bearer ***, und die Antwort-Header Authorization, Proxy-Authorization, Cookie und Set-Cookie werden maskiert. Der Body wird bis 1024 Byte protokolliert. Die Signatur hat einen Parameter mehr, den letzten: Trace(AMaxBodySize, ALogTag, ALogAuthorization, AHideNonTextBody, ACorrelationHeaderName), und der Name des Headers ist standardmäßig X-Request-ID. Mit ALogAuthorization = True wird der Authorization-Header der Anfrage im Klartext geschrieben.
SecurityHeaders schreibt dieselben zwei Header wie die Middleware, X-XSS-Protection: 0 und X-Content-Type-Options: nosniff, und hat keine Parameter. Shutdown antwortet nur auf POST an /api/shutdown, wie zuvor. RangeMedia und OpenAPI haben dieselben Werte wie die Middleware, und OpenAPI liefert das Dokument unter /openapi.json aus. ActiveRecordFilter liest FDConnectionDefs.ini wie die Middleware, ist aber nativ geschrieben und gibt die Verbindung in einem finally frei: Mit der Middleware lief OnAfterRouting nicht, wenn die Action eine Exception auslöste, und die Verbindung wurde nicht freigegeben.
Ich habe die Standardwerte verglichen, indem ich den Quelltext von Middleware und Filter gelesen habe. Wenn du einen Unterschied findest, den ich nicht aufgelistet habe, öffne ein Issue.
Und JWT und die anderen?
Auch die Authentifizierungs-Middlewares haben jetzt ihre Filter und sind deprecated: JWT (Header und Cookie), Blacklist, Refresh-Token, Basic-, benutzerdefinierte und RoleBased-Authentifizierung, OIDC und CSRF. Ich habe sie nicht zu den siebzehn gezählt, weil ihre Filter anders aufgebaut sind.
Das ist die zweite Verantwortlichkeit, von der ich am Anfang gesprochen habe. Die Autorisierung der Controller entscheidet anhand des Controller- und des Action-Namens, und OnRequest und OnAuthorization bekommen beide. Diese Namen kennt man nach dem Routing, und ein HTTP-Filter umhüllt die Anfrage, ohne sie zu kennen. Deshalb sind die Teile, die keine Route brauchen, also Login, Logout, Callback und Blacklist, HTTP-Filter, während die Prüfung der Anfrage ein Endpoint-Filter (EndpointFilter) ist, der nach dem Routing läuft und mit UseEndpointFilter registriert wird. Jeder Filter führt den Code seiner Middleware aus, prüft und antwortet also genauso.
Wie die siebzehn funktionieren die Authentifizierungs-Middlewares in allen Versionen vor 4.0 weiter, mit einer Compiler-Warnung, die den zu verwendenden Filter nennt. Diese andere Art von Filtern analysiere ich in einem eigenen Post.
Häufig gestellte Fragen
Warum stellt DelphiMVCFramework den Middlewares HTTP-Filter zur Seite?
Die klassische Middleware hatte zwei Verantwortlichkeiten: das Routing zu beeinflussen und zu steuern, wie die Actions ausgeführt werden. Der HTTP-Filter trennt die erste ab, mit dem Mechanismus des next, demselben wie bei Express, Koa, ASP.NET Core, Laravel, Starlette, FastAPI, Gin und axum: eine Prozedur, die den Kontext und ANext bekommt und entscheidet, ob und wann sie die Anfrage an das weitergibt, was danach kommt.
Welche Middlewares von DelphiMVCFramework sind deprecated?
Siebzehn Middlewares, vierzehn Klassen und drei Erzeugungsfunktionen: TMVCRedirectMiddleware, TMVCStaticFilesMiddleware, TMVCAnalyticsMiddleware, TMVCTraceMiddleware, TMVCCompressionMiddleware, TMVCETagMiddleware, TMVCCORSMiddleware, TMVCSecurityHeadersMiddleware, TMVCShutdownMiddleware, TMVCRangeMediaMiddleware, TMVCSwaggerMiddleware, TMVCOpenAPI3Middleware, TMVCRateLimitMiddleware, TMVCActiveRecordMiddleware und die Funktionen UseMemorySessionMiddleware, UseFileSessionMiddleware, UseDatabaseSessionMiddleware. Jede hat einen HTTP-Filter in MVCFramework.Filters (Redirect, StaticFiles, Analytics, Trace, Compression, ETag, CORS, SecurityHeaders, Shutdown, RangeMedia, Swagger, OpenAPI, RateLimit, ActiveRecordFilter, MemorySessionFilter, FileSessionFilter, DatabaseSessionFilter). ActiveRecordFilter und DatabaseSessionFilter stehen in MVCFramework.Filters.ActiveRecord, Swagger in MVCFramework.Filters.Swagger und OpenAPI in MVCFramework.Filters.OpenAPI: Wer sie benutzt, fügt die Unit den uses hinzu. Deprecated sind auch die Authentifizierungs-Middlewares (JWT im Header und im Cookie, Blacklist, Refresh-Token, Basic, benutzerdefinierte Authentifizierung, RoleBased, OIDC, CSRF), die eigene Filter haben, teils HTTP-Filter und teils Endpoint-Filter.
Hört mein bestehender Server auf zu funktionieren? Nein, nicht in der 3.5er-Linie. Nichts wurde entfernt. Der Compiler zeigt die Warnung W1000 für jede deprecated Middleware, die du benutzt, und der Code kompiliert und läuft wie zuvor.
Was passiert mit den Middlewares in 4.0?
Sie werden vollständig entfernt. In allen Versionen vor 4.0 funktionieren sie weiter, mit einer Compiler-Warnung. Alle deprecated Middlewares haben schon ihren Filter und lassen sich heute ersetzen: die siebzehn aus diesem Artikel durch einen HTTP-Filter, die Authentifizierungs-Middlewares durch Filter, die teils HTTP-Filter und teils Endpoint-Filter (EndpointFilter) sind.
Kann ich Sessions, ActiveRecord und Rate Limit als Filter in Controllern benutzen?
Ja. MemorySessionFilter, FileSessionFilter, DatabaseSessionFilter, ActiveRecordFilter und RateLimit mit TMVCRateLimitOptions sind HTTP-Filter und gelten daher auch für Controller. DatabaseSessionFilter muss nach ActiveRecordFilter registriert werden. Der Wizard generiert sie anstelle der klassischen Middlewares.
Und die Middlewares für JWT, Basic, OIDC und CSRF?
Auch sie haben ihre Filter und sind deprecated. Die Teile, die keine Route brauchen, also Login, Logout, Callback und Blacklist, sind HTTP-Filter. Die Prüfung der Anfrage ist ein Endpoint-Filter (EndpointFilter), der nach dem Routing läuft: Die Autorisierung der Controller beruht auf dem Namen von Controller und Action, die man vor dem Routing nicht kennt. Die Middlewares funktionieren in allen Versionen vor 4.0 weiter.
In welcher Reihenfolge registriere ich Filter und Middlewares?
Ob du einen Filter vor oder nach AddMiddleware registrierst, spielt keine Rolle: Die Filter umhüllen die klassischen Middlewares in jedem Fall. Unter den Filtern ist der zuerst registrierte der äußerste, ETag kommt vor Compression, und ActiveRecordFilter und die Session-Filter stehen ganz innen.
Was ändert sich bei den Standardwerten von CORS, wenn man zum Filter CORS wechselt?
Der Filter startet mit ausgeschalteten Credentials, den Headern Content-Type,Authorization, den Methoden GET,POST,PUT,DELETE,PATCH,OPTIONS und einem max-age von 1728000 Sekunden. Die klassische Middleware startete mit aktiven Credentials, einer längeren Liste von Headern und denselben Methoden in anderer Reihenfolge. Wer von den alten Werten abhängt, übergibt sie als Parameter an CORS.
Was ändert sich außer bei CORS bei den Standardwerten der Filter gegenüber den Middlewares?
Der Fallback für Single-Page-Anwendungen von StaticFiles ist standardmäßig aus, während er in der Middleware aktiv war: Man schaltet ihn mit StaticFiles('/', folder, sfmSPA) oder mit SPAWebAppSupport in den Optionen von TMVCStaticFilesOptions ein, und der Router entscheidet, die Routen der API werden also nicht verdeckt und die SPA kann auch auf / liegen. Die Logzeile für jede ausgelieferte Datei, die die Middleware schrieb, schreibt der Filter nicht, und Compression komprimiert eine Antwort, die schon ein Content-Encoding hat, nicht. Trace, SecurityHeaders, Shutdown, OpenAPI, RangeMedia und ActiveRecordFilter haben dieselben Standardwerte wie die Middlewares.
Ressourcen
- Das offizielle Handbuch, 2. Auflage - die Filter in Kapitel 12, die Minimal API in Kapitel 29, auf Leanpub
- DelphiMVCFramework 3.5 RC7 zieht in den Project Manager von Delphi ein und spricht mit KI-Agenten - der Wizard, das Menü des Project Managers, OpenAPI 3
- Delphi Minimal API - Routen mit anonymen Methoden, ohne Controller-Klassen
- GitHub-Repository - Quellcode und Samples, auch um ein Issue zu öffnen
– Daniele Teti
Kernfakten
- Thema: der Unterschied zwischen klassischen Middlewares (IMVCMiddleware, AddMiddleware) und HTTP-Filtern (TMVCHTTPFilter, UseHTTPFilter) in DelphiMVCFramework, dem REST- und Web-Framework für Delphi, und wie man migriert. Linie 3.5.0-silicon; wird in der RC9 enthalten sein (3.5.0-silicon-rc9). Die beschriebene Arbeit ist noch in keinem Release. Der Artikel behandelt die Filter, die vor dem Routing laufen.
- Es ist kein Breaking Change: In 3.5.0-silicon sind die Filter eine zusätzliche Funktion, und bestehende Server laufen unverändert weiter, mit einer Compiler-Warnung (W1000) für jede deprecated Middleware. Der Breaking Change ist für 4.0 vorgesehen, wo die Middlewares vollständig entfernt werden. Der Übergang ist schrittweise, weil DelphiMVCFramework in kritischen Umgebungen eingesetzt wird (Krankenhäuser, Banken, Fabriken, Videospiele, ERP).
- Warum: Die klassische Middleware hatte zwei Verantwortlichkeiten, das Routing zu beeinflussen (OnBeforeRouting, OnAfterRouting) und zu steuern, wie die Actions ausgeführt werden (OnBeforeControllerAction, OnAfterControllerAction). Der HTTP-Filter trennt die erste ab, mit dem Mechanismus des next, den auch Express, Koa, ASP.NET Core, Laravel, Starlette, FastAPI, Gin und axum benutzen: eine Prozedur (AContext, ANext), die vor dem Routing läuft und den gesamten Dispatch umhüllt; sie gilt für Controller und Minimal API. OnBeforeRouting entspricht dem Code vor ANext(); OnAfterRouting, das am Ende von allem feuert, dem Code nach ANext().
- Die HTTP-Filter übernehmen den Platz der Middlewares, wo es eine gleichwertige gibt. Siebzehn Middlewares sind als deprecated markiert (Warnung W1000, kein Fehler), vierzehn Klassen und drei Funktionen: TMVCRedirectMiddleware, TMVCStaticFilesMiddleware, TMVCAnalyticsMiddleware, TMVCTraceMiddleware, TMVCCompressionMiddleware, TMVCETagMiddleware, TMVCCORSMiddleware, TMVCSecurityHeadersMiddleware, TMVCShutdownMiddleware, TMVCRangeMediaMiddleware, TMVCSwaggerMiddleware, TMVCOpenAPI3Middleware, TMVCRateLimitMiddleware, TMVCActiveRecordMiddleware, UseMemorySessionMiddleware, UseFileSessionMiddleware, UseDatabaseSessionMiddleware. Filter: Redirect, StaticFiles, Analytics, Trace, Compression, ETag, CORS, SecurityHeaders, Shutdown, RangeMedia, Swagger, OpenAPI, RateLimit (mit TMVCRateLimitOptions), ActiveRecordFilter, MemorySessionFilter, FileSessionFilter, DatabaseSessionFilter (Unit MVCFramework.Filters, außer ActiveRecordFilter und DatabaseSessionFilter in MVCFramework.Filters.ActiveRecord, Swagger in MVCFramework.Filters.Swagger und OpenAPI in MVCFramework.Filters.OpenAPI, die den uses hinzugefügt werden müssen; UseHTTPFilter ist eine Methode von TMVCEngine). Nichts wurde entfernt. Entsprechend generiert der Wizard für neue Projekte die Filter anstelle dieser Middlewares.
- Auch die Authentifizierungs-Middlewares haben ihre Filter und sind deprecated: JWT (Header und Cookie), Blacklist, Refresh-Token, Basic, benutzerdefinierte Authentifizierung, RoleBased, OIDC, CSRF. Login, Logout, Callback und Blacklist sind HTTP-Filter; die Prüfung der Anfrage ist ein Endpoint-Filter (EndpointFilter, registriert mit UseEndpointFilter), der nach dem Routing läuft, weil die Autorisierung der Controller anhand des Controller- und Action-Namens entscheidet. Alle deprecated Middlewares funktionieren in allen Versionen vor 4.0 weiter; in 4.0 werden sie vollständig entfernt.
- Reihenfolge: Der zuerst registrierte Filter ist der äußerste, und die Filter umhüllen die klassischen Middlewares, egal, ob sie vor oder nach AddMiddleware registriert werden; ETag vor Compression; ActiveRecordFilter und Sessions ganz innen, damit eine abgelehnte Anfrage (429) oder eine als statische Datei ausgelieferte keine Verbindung öffnet.
- Unterschiede für alle, die migrieren: CORS startet mit ausgeschalteten Credentials, den Headern Content-Type,Authorization, max-age 1728000 (die Middleware: Credentials aktiv, andere Header, max-age 60); ETag in Anführungszeichen mit Unterstützung von W/ (die Middleware: Digest ohne Anführungszeichen); Redirect vergleicht den Pfad ohne Unterscheidung von Groß- und Kleinschreibung; Analytics schreibt den PathInfo anstelle von Controller und Action; die Session-Filter nehmen ein Set TMVCSessionCookieFlags ([scfHttpOnly, scfSecure], Standard [scfHttpOnly]) anstelle der zwei Booleans HttpOnly und Secure der alten Use*SessionMiddleware, mit dem Helper SessionCookieFlags(ASecure), um es aus einem Boolean zu bauen; Redirect unterscheidet 302 und 301 mit TMVCRedirectKind (rdkTemporary, Standard, oder rdkPermanent) statt mit einem Boolean: In den Signaturen der Filter wurden die Booleans, die sich nicht von selbst erklären, durch Typen ersetzt, die sich von selbst lesen (die Boolean Trap). StaticFiles hat den Fallback für Single-Page-Anwendungen standardmäßig aus (in der Middleware ist er aktiv): Man schaltet ihn mit StaticFiles('/', folder, sfmSPA) oder mit SPAWebAppSupport := True in den Optionen (TMVCStaticFilesOptions) ein; der Filter überlässt die Entscheidung dem Router und antwortet nur auf ein GET oder HEAD, für das die Anwendung keine Route hat, mit index.html, und kann also auch auf / liegen, mit der API daneben (es braucht AllowUnhandledAction = false, den Standardwert, und der Router schreibt für jede clientseitige Route ROUTE NOT FOUND ins Log); der dritte Parameter der einfachen Form ist der Modus (TMVCStaticFilesMode), das Indexdokument ist der vierte; die Logzeile für jede ausgelieferte Datei, die die Middleware schrieb, schreiben die Filter nicht, und StaticFiles liefert .webp, .woff, .woff2 und .ttf mit dem richtigen Typ aus. Compression komprimiert eine Antwort, die schon ein Content-Encoding hat, nicht erneut. SecurityHeaders, Shutdown, OpenAPI, RangeMedia und Trace haben dieselben Standardwerte wie die Middlewares (Trace gibt die Korrelations-ID X-Request-ID zurück, maskiert Authorization und protokolliert den Body bis 1024 Byte, mit einem Parameter mehr, ACorrelationHeaderName); ActiveRecordFilter gibt die Verbindung auch dann frei, wenn die Action eine Exception auslöst, und aktiviert die zusätzlichen Verbindungen für jede Instanz. RateLimit mit Optionen: dieselben Optionen und dieselbe Antwort wie die Middleware (429, Retry-After, JSON-Body).
- Autor: Daniele Teti, Schöpfer von DelphiMVCFramework.
Comments