DelphiMVCFramework 3.5.0-silicon: dai middleware ai filtri, cosa cambierà per il tuo server?
🇬🇧 English • 🇪🇸 Español • 🇩🇪 Deutsch • 🇧🇷 Português • 🇫🇷 Français

Un middleware classico di DelphiMVCFramework ha quattro metodi. Un filtro HTTP ha una procedura sola. Perché ho cambiato un meccanismo che funzionava da anni, dove vanno a finire i quattro hook, in che ordine registri i filtri, e cosa succede al server che hai già in produzione?
Per anni in DelphiMVCFramework il modo di agganciare codice al ciclo di una richiesta sono stati i middleware, con AddMiddleware. I filtri HTTP, con UseHTTPFilter, sono arrivati con la Minimal API della 3.5, quella del post sulla RC6. Ora i filtri prendono il posto dei middleware dove ne esiste uno equivalente: diciassette middleware sono marcati deprecated, ciascuno con il suo filtro. Sarà nella RC9 della 3.5.0-silicon.
✅ Non è un breaking change. I server attuali continuano a funzionare nello stesso modo, senza modifiche e senza problemi.
Nella 3.5.0-silicon i filtri sono una funzionalità aggiuntiva: i middleware restano dove sono, e il compilatore mostra un avviso per ogni middleware marcato deprecated. Il breaking change ci sarà nella 4.0, dove i middleware saranno eliminati del tutto.
DelphiMVCFramework è il framework più popolare del suo tipo su GitHub per Delphi, con oltre 50 contributors, ed è usato in ambienti critici: ospedali, banche, fabbriche, videogiochi, ERP con centinaia di utenti. Per questo ho deciso una transizione soft, senza traumi.
In questo articolo parlo dei filtri HTTP, quelli che girano prima del routing.
Due responsabilità in una classe
Finora un middleware faceva due cose. Con OnBeforeRouting e OnAfterRouting influenzava il routing: poteva rispondere prima che il router guardasse la richiesta, per servire un file statico, fare un redirect o rispondere a un preflight CORS, e a fine richiesta osservare o modificare la risposta, come fanno ETag e analytics. Con OnBeforeControllerAction e OnAfterControllerAction controllava come le action venivano eseguite. Quattro metodi in una classe sola, per due compiti diversi.
Chi mi conosce, o ha seguito qualche mio corso, sa quanta importanza do ai principi SOLID. Trovare una classe con due compiti nel mio progetto open source più popolare mi disturbava davvero. Ogni volta che mettevo mano a un middleware lo vedevo: o si usava la coppia di metodi del routing (OnBeforeRouting e OnAfterRouting) lasciando vuoti gli altri due, oppure si implementava la coppia dell’endpoint (OnBeforeControllerAction e OnAfterControllerAction), che conosce già controller e action perché gira dopo il routing. Era evidente che quella componente andava rifattorizzata. Ora è arrivato il momento.
La violazione è quella del principio di singola responsabilità (SRP). L’ho risolta con un meccanismo che esiste in molti framework: una funzione che riceve la richiesta e un next da chiamare per passare la mano a chi sta dopo. Lo usano Express (next()) e Koa (await next()) in Node.js, ASP.NET Core (await next(context)) in C#, Laravel ($next($request)) in PHP, Starlette e FastAPI (call_next) in Python, Gin (c.Next()) in Go e axum (next.run(request)) in Rust.
È la Chain of Responsibility nella variante detta Intercepting Filter: ogni anello può agire prima, passare la mano con next e agire dopo, oppure fermarsi. In DelphiMVCFramework quel next si chiama ANext. Questo articolo riguarda la prima metà del lavoro: i filtri che girano prima del routing.
Come funziona un filtro HTTP
Un filtro HTTP è questa procedura:

Copia il codice
procedure(const AContext: TWebContext; const ANext: TMVCHTTPFilterNext)
Avvolge l’intera richiesta: i middleware classici, il routing e l’action. Quello che scrivi prima di ANext() gira prima di tutto, quello che scrivi dopo gira quando la risposta è completa, cioè dopo l’action. Per fermare la richiesta non chiami ANext(), e nessuna action viene eseguita.
Si chiama HTTP perché lavora a livello di protocollo: vede metodo, header e body della richiesta e della risposta. Funziona anche con HTTPS, perché la cifratura TLS sta più in basso e il filtro riceve la richiesta già decifrata.
| Middleware classico | Filtro HTTP | |
|---|---|---|
| Interfaccia | IMVCMiddleware, quattro metodi | una procedura con AContext e ANext |
| Si registra con | AEngine.AddMiddleware(...) | AEngine.UseHTTPFilter(...) |
| Quando gira | ai quattro punti del ciclo, nell’ordine di registrazione | intorno all’intera richiesta: middleware classici, routing e action |
| Prima e dopo | due metodi separati | stessa procedura: prima e dopo ANext() |
| Per fermare la richiesta | Handled := True | non chiami ANext() |
| Controller | sì | sì |
UseHTTPFilter è un metodo di TMVCEngine e restituisce TMVCEngine, quindi puoi concatenare le chiamate. I filtri pronti stanno in MVCFramework.Filters.
Il filtro HTTP funziona con i controller e con la Minimal API allo stesso modo, ed è il motivo per cui il wizard può usare la stessa catena nei due casi. I filtri sono trattati per esteso nel capitolo 12 della guida ufficiale, seconda edizione.
Dove stanno i quattro hook?
OnBeforeRoutingdiventa il codice prima diANext().OnAfterRoutingdiventa il codice dopoANext(). Il nome inganna: scatta alla fine di tutto, non subito dopo il routing. Con untry..finallyattorno aANext()quel codice gira anche quando la action solleva un’eccezione. Nel middleware classico in quel casoOnAfterRoutingviene saltato.OnBeforeControllerActioneOnAfterControllerActionnon hanno un equivalente in un filtro HTTP. Ricevono il nome del controller e quello della action, che si conoscono solo dopo il routing, mentre il filtro HTTP avvolge tutta la richiesta senza conoscerli. Li ha il filtro che gira dopo il routing,UseEndpointFilter: è la seconda responsabilità, quella di cui parlo in fondo.
Un filtro scritto a mano
Per scrivere un filtro non serve una classe: passi a UseHTTPFilter un metodo anonimo con la firma che hai appena visto. Questo aggiunge due header di sicurezza a ogni risposta:

Copia il codice
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);
Gli header li imposti prima di ANext(), così una action che ne vuole un altro valore lo sovrascrive. Il filtro pronto SecurityHeaders() mette X-XSS-Protection: 0 e X-Content-Type-Options: nosniff. Questo è fatto a mano per mostrare la forma, e aggiunge X-Frame-Options.
Un filtro può anche fermare la richiesta. Questo risponde 400 ai POST, PUT e PATCH con un body vuoto o fatto di soli spazi, prima che il routing la guardi:

Copia il codice
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() sta nel ramo else: quando il filtro rifiuta la richiesta, ANext() non viene chiamato e nessun controller la vede.
I diciassette middleware deprecati
Diciassette middleware sono marcati deprecated: quattordici classi TMVC...Middleware e tre funzioni Use...SessionMiddleware, che creano i middleware di sessione. Ciascuno ha il suo filtro in MVCFramework.Filters:
| Middleware deprecato | Filtro | Come lo registri |
|---|---|---|
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])), dopo ActiveRecordFilter |
ActiveRecordFilter e DatabaseSessionFilter non stanno in MVCFramework.Filters ma in MVCFramework.Filters.ActiveRecord, Swagger in MVCFramework.Filters.Swagger e OpenAPI in MVCFramework.Filters.OpenAPI. Lo strato di base non dipende così da FireDAC, da SwagDoc e dall’emettitore OpenAPI. Chi usa quei filtri aggiunge la unit alle uses, le firme non cambiano.
WaitForShutdownOrConsoleReturn, che nel middleware Shutdown era un metodo di classe, ora è una procedura di MVCFramework.Filters. StaticFiles ha anche un overload che prende un TMVCStaticFilesOptions, con le opzioni del middleware classico: charset, fallback per le single page application, personalizzazione dei tipi MIME, regole per richiesta.
Gli ultimi cinque sono filtri nuovi che i controller possono usare anche così:
- I filtri di sessione mettono la factory della sessione su ogni richiesta, e
Context.Sessionsi usa come sempre.DatabaseSessionFilterva registrato dopoActiveRecordFilter. ActiveRecordFilterprende la connessione prima del dispatch e la rilascia in unfinally, quindi anche quando la action solleva un’eccezione. Le connessioni aggiuntive valgono per ogni istanza del filtro: il middleware le attivava solo per la prima del processo.RateLimitconTMVCRateLimitOptionsha le stesse opzioni e la stessa risposta del middleware: chiave per IP, utente, API key o personalizzata, prefissi di percorso esclusi, prefisso degli headerX-RateLimit-*, una callback quando una richiesta viene rifiutata. La risposta è 429 conRetry-Aftere un body JSON.RateLimit(10, 60)resta com’era.
Se il tuo codice usa ancora uno di questi middleware, il compilatore ti dice questo:
[dcc32 Warning] W1000 Symbol 'TMVCStaticFilesMiddleware' is deprecated: 'Use the StaticFiles HTTP filter (MVCFramework.Filters): AEngine.UseHTTPFilter(StaticFiles(...))'
È un avviso, non un errore. Il codice continua a compilare e a funzionare.
Cosa genera il wizard
Di conseguenza il wizard dei progetti nuovi, quello del post sulla RC7, genera i filtri al posto di quei middleware. Per un progetto a controller con file statici, CORS, ETag, compressione, trace, rate limit, ActiveRecord e sessioni, EngineConfigU contiene una catena sola:

Copia il codice
// 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))));
Il blocco è un esempio: i nomi della connessione dipendono dal progetto generato, e lRateLimitOptions è preparato poco sopra con TMVCRateLimitOptions.Default.
Il primo registrato è il più esterno. L’ordine che usa il wizard è Redirect, StaticFiles, Analytics, Trace, CORS, ETag, Compression, RateLimit, la documentazione API, ActiveRecordFilter e i filtri di sessione. Sessioni e connessione al database stanno nel punto più interno, così una richiesta rifiutata con 429 o servita come file statico non apre una connessione.
ETag sta prima di Compression perché l’hash lo calcola dopo che ANext() è tornato. Se ETag è più esterno, la compressione ha già riscritto il body, e l’ETag corrisponde ai byte che il client riceve.
Rispetto ad AddMiddleware l’ordine di registrazione non conta: i filtri avvolgono comunque i middleware classici, anche quelli che rispondono da soli, come i file statici o il redirect.
Per OpenAPI e Swagger il wizard usa UseHTTPFilter(Swagger(...)) per l’endpoint del documento e UseHTTPFilter(StaticFiles(lSwaggerUIOptions)) per la Swagger UI sotto /swagger. Lo stesso vale per i progetti WebBroker e per quelli Minimal API, che la catena la usavano già.
Nelle uses il wizard non scrive più MVCFramework.Middleware.Redirect, .StaticFiles, .Analytics, .Trace, .CORS, .ETag, .Compression, .Swagger, .RateLimit, .Session e .ActiveRecord. Aggiunge MVCFramework.Filters e, quando servono, MVCFramework.Filters.ActiveRecord, MVCFramework.Filters.Swagger e MVCFramework.Filters.OpenAPI, più MVCFramework.MinimalAPI nei progetti Minimal API.
Il wizard installato nell’IDE genera dai template che ha dentro il package. Per vedere la catena nuova serve il package della RC9.
Se migri un progetto
Con la 4.0 i middleware non ci saranno più, quindi per i middleware che hanno già un filtro il passaggio va fatto prima di aggiornare. Per i tre più comuni è questo (nelle uses aggiungi MVCFramework.Filters). StaticFiles in questa forma non è identico al middleware: il fallback per le single page application è spento, ne parlo più sotto:

Copia il codice
// 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('*'));
I valori predefiniti di alcuni filtri non coincidono con quelli del middleware. Ho confrontato il sorgente dei due, filtro per filtro, per questi.
CORS
TMVCCORSMiddleware | CORS | |
|---|---|---|
| Credenziali | attive | spente |
| Header consentiti | Content-Type, Accept, jwtusername, jwtpassword, authentication, authorization | Content-Type,Authorization |
| Metodi | POST,GET,OPTIONS,PUT,DELETE,PATCH | GET,POST,PUT,DELETE,PATCH,OPTIONS |
| Max-age | 60 secondi | 1728000 secondi |
Se un client dipende dai valori vecchi, li passi come parametri:

Copia il codice
AEngine.UseHTTPFilter(CORS('*', True, '',
'Content-Type, Accept, jwtusername, jwtpassword, authentication, authorization',
'POST,GET,OPTIONS,PUT,DELETE,PATCH', 60));
Il filtro risponde al preflight prima del routing e aggiunge gli header anche alle risposte d’errore: un 404 porta Access-Control-Allow-Origin.
ETag
Il filtro scrive l’ETag tra virgolette, come chiede la RFC 7232, accetta il prefisso W/ in If-None-Match e agisce solo sulle risposte 2xx. Il middleware classico scrive il digest SHA-1 senza virgolette e lo confronta così com’è con If-None-Match. Se la action ha già impostato un ETag, il classico lo lascia, il filtro lo sostituisce con il suo. In tutti e due i casi l’ETag lo ricevono solo le risposte che hanno un ContentStream: una risposta a stringa non lo riceve.
Redirect
Il filtro risponde 302 (301 con rdkPermanent, il terzo parametro) e conserva la query string (APreserveQuery, vero per default). Confronta il percorso senza distinguere maiuscole e minuscole; il middleware confronta il testo esatto.
Analytics
Il file CSV ha le stesse colonne. La quinta, che nel middleware contiene controller e action, nel filtro contiene il PathInfo: un filtro HTTP non ha un controller da nominare.
Swagger
Il filtro Swagger, che sta in MVCFramework.Filters.Swagger, avvolge lo stesso TMVCSwaggerMiddleware; OpenAPI sta in MVCFramework.Filters.OpenAPI. Il middleware JWT viene cercato quando il documento è generato, a richiesta ricevuta, e l’ordine di registrazione non conta.
Sessioni
I due Boolean dei middleware di sessione, HttpOnly e Secure, nei filtri diventano un set, TMVCSessionCookieFlags, che per default vale [scfHttpOnly]. Non basta quindi cambiare il nome della funzione:
![UseFileSessionMiddleware(10, True, ‘sessions’, True) diventa FileSessionFilter(10, ‘sessions’, [scfHttpOnly, scfSecure]): i due Boolean diventano un set di flag](/images/dmvcframework-middleware-to-http-filters/file-session-before-after.png)
Copia il codice
// before
AEngine.AddMiddleware(UseFileSessionMiddleware(10, True, 'sessions', True));
// after
AEngine.UseHTTPFilter(FileSessionFilter(10, 'sessions', [scfHttpOnly, scfSecure]));
Se copi i parametri uno a uno il compilatore protesta, perché dove c’era un Boolean ora c’è un set. Per costruire il set da un Boolean c’è SessionCookieFlags: ha sempre HttpOnly, e Secure solo se il parametro è vero. È quello che usa il codice generato dal wizard.
Per una session factory qualsiasi c’è SessionFilter(factory), e Session(factory) per i gruppi Minimal API: il filtro ne diventa proprietario. MemorySessionFilter, FileSessionFilter e DatabaseSessionFilter sono costruiti sopra, e quest’ultimo sta in MVCFramework.Filters.ActiveRecord.
Boolean e enum
Nelle firme dei filtri ho evitato i Boolean quando il True non si spiega da solo, la boolean trap. In UseFileSessionMiddleware(10, True, 'sessions', True) chi legge non sa quale True sia HttpOnly e quale Secure, e deve aprire la dichiarazione. I filtri di sessione prendono invece il set TMVCSessionCookieFlags, che si legge da solo: [scfHttpOnly, scfSecure]. Lo stesso vale per Redirect, che distingue 302 e 301 con TMVCRedirectKind (rdkTemporary, rdkPermanent) invece di un Boolean. Non li ho convertiti tutti: restano Boolean, per esempio, le credenziali di CORS e APreserveQuery di Redirect.
Rate limit
RateLimit(10, 60) resta com’era. Per le opzioni si prepara un TMVCRateLimitOptions, come fa il codice generato dal wizard:

Copia il codice
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 forma semplice, StaticFiles('/static', lWwwPath), e quella con le opzioni, StaticFiles(TMVCStaticFilesOptions), sono la stessa implementazione. La differenza principale rispetto a TMVCStaticFilesMiddleware è il fallback per le single page application: nel middleware era attivo per default, nel filtro è spento. Nella forma semplice si accende con sfmSPA:

Copia il codice
AEngine.UseHTTPFilter(StaticFiles('/', 'www/app', sfmSPA));
Con sfmSPA il filtro lascia passare la richiesta al router, e risponde con l’index.html della cartella esistente più vicina solo a una GET o HEAD per cui l’applicazione non ha nessuna rotta. Le rotte dell’API non vengono mai nascoste, quindi la SPA può stare a / con l’API accanto. Con le opzioni si imposta SPAWebAppSupport := True, che in TMVCStaticFilesOptions.Default vale False. Il terzo parametro della forma semplice è la modalità (sfmFiles o sfmSPA), il documento di indice, index.html per default, è il quarto.
Due limiti. Serve AllowUnhandledAction = false, che è il valore predefinito. E il router continua a scrivere nel log “ROUTE NOT FOUND” per ogni rotta lato client.
Il resto è come nel middleware: il prefisso non distingue maiuscole e minuscole, con o senza / finale, i file escono con il tipo MIME e con Last-Modified (e 304 su If-Modified-Since), una cartella senza / finale risponde 301 e poi serve il suo index.html. Restano due differenze. Il middleware scriveva nel log una riga per ogni file servito, con metodo, stato, percorso, IP e durata, e i filtri no. Il filtro serve con il tipo giusto .webp, .woff, .woff2 e .ttf, che il middleware non conosceva. Le altre opzioni del middleware, il charset, i tipi MIME personalizzati e le regole per richiesta, si hanno con TMVCStaticFilesOptions.
Compression
La soglia di 1024 byte, il livello di compressione e la scelta della codifica (la prima fra gzip e deflate nell’ordine dell’header Accept-Encoding) sono quelli del middleware. Cambia una cosa: il filtro non comprime una risposta che ha già un Content-Encoding. RangeMedia ne imposta uno identity sulle risposte parziali, e comprimerle romperebbe Content-Range.
Gli altri
Trace si comporta come il middleware. Ogni riga di log porta l’id di correlazione [rid:...], e lo stesso id torna nell’header di risposta X-Request-ID: è quello del chiamante se c’è, altrimenti ne genera uno di sei cifre esadecimali. Authorization compare con lo schema e tre asterischi, Bearer ***, e gli header di risposta Authorization, Proxy-Authorization, Cookie e Set-Cookie sono oscurati. Il body si registra fino a 1024 byte. La firma ha un parametro in più, l’ultimo: Trace(AMaxBodySize, ALogTag, ALogAuthorization, AHideNonTextBody, ACorrelationHeaderName), e il nome dell’header è X-Request-ID per default. Con ALogAuthorization = True l’Authorization della richiesta si scrive in chiaro.
SecurityHeaders scrive gli stessi due header del middleware, X-XSS-Protection: 0 e X-Content-Type-Options: nosniff, e non ha parametri. Shutdown risponde solo ai POST su /api/shutdown, come prima. RangeMedia e OpenAPI hanno gli stessi valori del middleware, e OpenAPI serve il documento su /openapi.json. ActiveRecordFilter legge FDConnectionDefs.ini come il middleware, ma è scritto nativo e rilascia la connessione in un finally: con il middleware, se la action sollevava un’eccezione, OnAfterRouting non girava e la connessione non veniva rilasciata.
Ho confrontato i valori predefiniti leggendo il sorgente di middleware e filtro. Se trovi una differenza che non ho elencato, apri una issue.
E JWT e gli altri?
Anche i middleware di autenticazione hanno ora i loro filtri e sono deprecati: JWT (header e cookie), blacklist, refresh token, autenticazione Basic, personalizzata e RoleBased, OIDC e CSRF. Non li ho messi fra i diciassette perché i loro filtri sono fatti in un altro modo.
È la seconda responsabilità di cui parlavo all’inizio. L’autorizzazione dei controller decide sul nome del controller e su quello della action, e li ricevono OnRequest e OnAuthorization. Quei nomi si conoscono dopo il routing, e un filtro HTTP avvolge la richiesta senza conoscerli. Per questo le parti che non hanno bisogno di una rotta, cioè login, logout, callback e blacklist, sono filtri HTTP, mentre il controllo della richiesta è un filtro di endpoint (EndpointFilter), che gira dopo il routing e si registra con UseEndpointFilter. Ogni filtro esegue il codice del suo middleware, quindi controlla e risponde allo stesso modo.
Come i diciassette, i middleware di autenticazione continueranno a funzionare fino alla 4.0 esclusa, con un avviso del compilatore che nomina il filtro da usare. In un altro post analizzerò questo altro tipo di filtri.
Domande frequenti
Perché DelphiMVCFramework affianca i filtri HTTP ai middleware?
Il middleware classico aveva due responsabilità: influenzare il routing e controllare come le action vengono eseguite. Il filtro HTTP separa la prima con il meccanismo del next, lo stesso di Express, Koa, ASP.NET Core, Laravel, Starlette, FastAPI, Gin e axum: una procedura che riceve il contesto e ANext, e decide se e quando passare la richiesta a ciò che sta dopo.
Quali middleware di DelphiMVCFramework sono deprecati?
Diciassette middleware, quattordici classi e tre funzioni di creazione: TMVCRedirectMiddleware, TMVCStaticFilesMiddleware, TMVCAnalyticsMiddleware, TMVCTraceMiddleware, TMVCCompressionMiddleware, TMVCETagMiddleware, TMVCCORSMiddleware, TMVCSecurityHeadersMiddleware, TMVCShutdownMiddleware, TMVCRangeMediaMiddleware, TMVCSwaggerMiddleware, TMVCOpenAPI3Middleware, TMVCRateLimitMiddleware, TMVCActiveRecordMiddleware e le funzioni UseMemorySessionMiddleware, UseFileSessionMiddleware, UseDatabaseSessionMiddleware. Ciascuno ha un filtro HTTP in MVCFramework.Filters (Redirect, StaticFiles, Analytics, Trace, Compression, ETag, CORS, SecurityHeaders, Shutdown, RangeMedia, Swagger, OpenAPI, RateLimit, ActiveRecordFilter, MemorySessionFilter, FileSessionFilter, DatabaseSessionFilter). ActiveRecordFilter e DatabaseSessionFilter stanno in MVCFramework.Filters.ActiveRecord, Swagger in MVCFramework.Filters.Swagger e OpenAPI in MVCFramework.Filters.OpenAPI: chi li usa aggiunge la unit alle uses. Sono deprecati anche i middleware di autenticazione (JWT in header e in cookie, blacklist, refresh token, Basic, autenticazione personalizzata, RoleBased, OIDC, CSRF), che hanno i loro filtri, in parte HTTP e in parte di endpoint.
Il mio server esistente smette di funzionare? No, non nella linea 3.5. Nulla è stato rimosso. Il compilatore mostra l’avviso W1000 per ogni middleware deprecato che usi, e il codice compila e gira come prima.
Cosa succede ai middleware nella 4.0?
Saranno eliminati del tutto. Fino alla 4.0 esclusa continuano a funzionare, con un avviso del compilatore. Tutti i middleware deprecati hanno già il loro filtro e si sostituiscono oggi: i diciassette di questo articolo con un filtro HTTP, quelli di autenticazione con filtri in parte HTTP e in parte di endpoint (EndpointFilter).
Posso usare sessioni, ActiveRecord e rate limit come filtri nei controller?
Sì. MemorySessionFilter, FileSessionFilter, DatabaseSessionFilter, ActiveRecordFilter e RateLimit con TMVCRateLimitOptions sono filtri HTTP, quindi valgono per i controller. DatabaseSessionFilter va registrato dopo ActiveRecordFilter. Il wizard li genera al posto dei middleware classici.
E i middleware di JWT, Basic, OIDC e CSRF?
Hanno anche loro i filtri e sono deprecati. Le parti che non hanno bisogno di una rotta, cioè login, logout, callback e blacklist, sono filtri HTTP. Il controllo della richiesta è un filtro di endpoint (EndpointFilter), che gira dopo il routing: l’autorizzazione dei controller si basa sul nome del controller e della action, che prima del routing non si conoscono. I middleware continuano a funzionare fino alla 4.0 esclusa.
In che ordine registro filtri e middleware?
Rispetto ad AddMiddleware l’ordine non conta: i filtri avvolgono comunque i middleware classici. Fra i filtri il primo registrato è il più esterno, ETag va prima di Compression, e ActiveRecordFilter e i filtri di sessione stanno nel punto più interno.
Cosa cambia nei valori predefiniti di CORS passando al filtro CORS?
Il filtro parte con le credenziali spente, gli header Content-Type,Authorization, i metodi GET,POST,PUT,DELETE,PATCH,OPTIONS e un max-age di 1728000 secondi. Il middleware classico partiva con le credenziali attive, un elenco di header più lungo e gli stessi metodi in un altro ordine. Chi dipende dai valori vecchi li passa come parametri a CORS.
Cosa cambia, oltre a CORS, nei valori predefiniti dei filtri rispetto ai middleware?
Il fallback per le single page application di StaticFiles è spento per default, mentre nel middleware era attivo: si accende con StaticFiles('/', cartella, sfmSPA) o con SPAWebAppSupport nelle opzioni di TMVCStaticFilesOptions, e il router decide, quindi le rotte dell’API non vengono nascoste e la SPA può stare anche su /. Il filtro non scrive la riga di log per ogni file servito che scriveva il middleware, e Compression non comprime una risposta che ha già un Content-Encoding. Trace, SecurityHeaders, Shutdown, OpenAPI, RangeMedia e ActiveRecordFilter hanno gli stessi valori predefiniti dei middleware.
Risorse
- La guida ufficiale, seconda edizione - i filtri nel capitolo 12, la Minimal API nel capitolo 29, su Leanpub
- DelphiMVCFramework 3.5 RC7 entra nel Project Manager di Delphi e parla con gli agenti AI - il wizard, il menù del Project Manager, OpenAPI 3
- Delphi Minimal API - le rotte con metodi anonimi, senza classi controller
- Repository GitHub - codice sorgente e sample, anche per aprire una issue
– Daniele Teti
Fatti chiave
- Argomento: la differenza fra middleware classici (IMVCMiddleware, AddMiddleware) e filtri HTTP (TMVCHTTPFilter, UseHTTPFilter) in DelphiMVCFramework, il framework REST e web per Delphi, e come si migra. Linea 3.5.0-silicon; sarà nella RC9 (3.5.0-silicon-rc9). Il lavoro descritto non è ancora in una release. L'articolo riguarda i filtri che girano prima del routing.
- Non è un breaking change: nella 3.5.0-silicon i filtri sono una funzionalità aggiuntiva e i server esistenti continuano a funzionare allo stesso modo, con un avviso del compilatore (W1000) per ogni middleware deprecato. Il breaking change è previsto nella 4.0, dove i middleware saranno eliminati del tutto. La transizione è graduale perché DelphiMVCFramework è usato in ambienti critici (ospedali, banche, fabbriche, videogiochi, ERP).
- Perché: il middleware classico aveva due responsabilità, influenzare il routing (OnBeforeRouting, OnAfterRouting) e controllare come le action vengono eseguite (OnBeforeControllerAction, OnAfterControllerAction). Il filtro HTTP separa la prima con il meccanismo del next, usato anche da Express, Koa, ASP.NET Core, Laravel, Starlette, FastAPI, Gin e axum: una procedura (AContext, ANext) che gira prima del routing e avvolge l'intero dispatch; vale per controller e Minimal API. OnBeforeRouting corrisponde al codice prima di ANext(); OnAfterRouting, che scatta alla fine di tutto, al codice dopo ANext().
- I filtri HTTP prendono il posto dei middleware dove ne esiste uno equivalente. Diciassette middleware sono marcati deprecated (avviso W1000, non errore), quattordici classi e tre funzioni: TMVCRedirectMiddleware, TMVCStaticFilesMiddleware, TMVCAnalyticsMiddleware, TMVCTraceMiddleware, TMVCCompressionMiddleware, TMVCETagMiddleware, TMVCCORSMiddleware, TMVCSecurityHeadersMiddleware, TMVCShutdownMiddleware, TMVCRangeMediaMiddleware, TMVCSwaggerMiddleware, TMVCOpenAPI3Middleware, TMVCRateLimitMiddleware, TMVCActiveRecordMiddleware, UseMemorySessionMiddleware, UseFileSessionMiddleware, UseDatabaseSessionMiddleware. Filtri: Redirect, StaticFiles, Analytics, Trace, Compression, ETag, CORS, SecurityHeaders, Shutdown, RangeMedia, Swagger, OpenAPI, RateLimit (con TMVCRateLimitOptions), ActiveRecordFilter, MemorySessionFilter, FileSessionFilter, DatabaseSessionFilter (unit MVCFramework.Filters, tranne ActiveRecordFilter e DatabaseSessionFilter in MVCFramework.Filters.ActiveRecord, Swagger in MVCFramework.Filters.Swagger e OpenAPI in MVCFramework.Filters.OpenAPI, da aggiungere alle uses; UseHTTPFilter è un metodo di TMVCEngine). Nulla è stato rimosso. Di conseguenza il wizard dei progetti nuovi genera i filtri al posto di quei middleware.
- Anche i middleware di autenticazione hanno i loro filtri e sono deprecati: JWT (header e cookie), blacklist, refresh token, Basic, autenticazione personalizzata, RoleBased, OIDC, CSRF. Login, logout, callback e blacklist sono filtri HTTP; il controllo della richiesta è un filtro di endpoint (EndpointFilter, registrato con UseEndpointFilter), che gira dopo il routing perché l'autorizzazione dei controller decide sul nome di controller e action. Tutti i middleware deprecati funzionano fino alla 4.0 esclusa; nella 4.0 saranno eliminati del tutto.
- Ordine: il primo filtro registrato è il più esterno e i filtri avvolgono i middleware classici qualunque sia l'ordine rispetto ad AddMiddleware; ETag prima di Compression; ActiveRecordFilter e sessioni nel punto più interno, così una richiesta rifiutata (429) o servita come file statico non apre una connessione.
- Differenze per chi migra: CORS parte con credenziali spente, header Content-Type,Authorization, max-age 1728000 (il middleware: credenziali attive, altri header, max-age 60); ETag tra virgolette con supporto di W/ (il middleware: digest senza virgolette); Redirect confronta il percorso senza distinguere maiuscole; Analytics scrive il PathInfo al posto di controller e action; i filtri di sessione prendono un set TMVCSessionCookieFlags ([scfHttpOnly, scfSecure], predefinito [scfHttpOnly]) al posto dei due Boolean HttpOnly e Secure dei vecchi Use*SessionMiddleware, con l'helper SessionCookieFlags(ASecure) per costruirlo da un Boolean; Redirect distingue 302 e 301 con TMVCRedirectKind (rdkTemporary, predefinito, o rdkPermanent) invece di un Boolean: nelle firme dei filtri i Boolean che non si spiegano da soli sono stati sostituiti da tipi che si leggono da soli (la boolean trap). StaticFiles ha il fallback per le single page application spento per default (nel middleware è attivo): si accende con StaticFiles('/', cartella, sfmSPA) o con SPAWebAppSupport := True nelle opzioni (TMVCStaticFilesOptions): il filtro lascia decidere il router e risponde index.html solo a una GET o HEAD per cui l'applicazione non ha nessuna rotta, quindi può stare anche su / con l'API accanto (serve AllowUnhandledAction = false, il predefinito, e il router scrive nel log ROUTE NOT FOUND per ogni rotta lato client); il terzo parametro della forma semplice è la modalità (TMVCStaticFilesMode), il documento di indice è il quarto; i filtri non scrivono la riga di log per ogni file servito che scriveva il middleware, e StaticFiles serve .webp, .woff, .woff2 e .ttf con il tipo giusto. Compression non ricomprime una risposta che ha già un Content-Encoding. SecurityHeaders, Shutdown, OpenAPI, RangeMedia e Trace hanno gli stessi valori predefiniti dei middleware (Trace restituisce l'id di correlazione X-Request-ID, oscura Authorization e registra il body fino a 1024 byte, con un parametro in più, ACorrelationHeaderName); ActiveRecordFilter rilascia la connessione anche se la action solleva un'eccezione e attiva le connessioni aggiuntive per ogni istanza. RateLimit con opzioni: stesse opzioni e stessa risposta del middleware (429, Retry-After, corpo JSON).
- Autore: Daniele Teti, creatore di DelphiMVCFramework.
Comments