DelphiMVCFramework 3.5.0-silicon: dos middlewares aos filtros, o que vai mudar no seu servidor?
🇮🇹 Italiano • 🇬🇧 English • 🇪🇸 Español • 🇩🇪 Deutsch • 🇫🇷 Français

Um middleware clássico do DelphiMVCFramework tem quatro métodos. Um filtro HTTP tem uma procedure só. Por que mudei um mecanismo que funcionava há anos, para onde vão os quatro hooks, em que ordem você registra os filtros, e o que acontece com o servidor que você já tem em produção?
Por anos, no DelphiMVCFramework, para plugar código no ciclo de uma requisição você usava os middlewares, com AddMiddleware. Os filtros HTTP, com UseHTTPFilter, chegaram com a Minimal API da 3.5, a do post sobre a RC6. Agora os filtros tomam o lugar dos middlewares onde existe um equivalente: dezessete middlewares estão marcados como deprecated, cada um com o seu filtro. Isso estará na RC9 da 3.5.0-silicon.
✅ Não é um breaking change. Os servidores atuais continuam funcionando do mesmo jeito, sem alterações e sem problemas.
Na 3.5.0-silicon os filtros são um recurso adicional: os middlewares ficam onde estão, e o compilador mostra um aviso para cada middleware marcado como deprecated. O breaking change virá na 4.0, onde os middlewares serão eliminados de vez.
O DelphiMVCFramework é o framework mais popular do seu tipo no GitHub para Delphi, com mais de 50 contributors, e é usado em ambientes críticos: hospitais, bancos, fábricas, videogames, ERPs com centenas de usuários. Por isso optei por uma transição suave, sem traumas.
Neste artigo falo dos filtros HTTP, os que rodam antes do roteamento.
Duas responsabilidades numa classe
Até agora um middleware fazia duas coisas. Com OnBeforeRouting e OnAfterRouting ele influenciava o roteamento: podia responder antes que o router olhasse a requisição, para servir um arquivo estático, fazer um redirect ou responder a um preflight CORS, e no fim da requisição observar ou modificar a resposta, como fazem ETag e analytics. Com OnBeforeControllerAction e OnAfterControllerAction controlava como as actions eram executadas. Quatro métodos numa classe só, para duas tarefas diferentes.
Quem me conhece, ou já fez algum curso meu, sabe quanta importância dou aos princípios SOLID. Encontrar uma classe com duas tarefas no meu projeto open source mais popular me incomodava de verdade. Toda vez que eu mexia num middleware, eu via: ou você usava o par de métodos do roteamento (OnBeforeRouting e OnAfterRouting) deixando os outros dois vazios, ou implementava o par do endpoint (OnBeforeControllerAction e OnAfterControllerAction), que já conhece controller e action porque roda depois do roteamento. Estava na cara que aquele componente precisava ser refatorado. Agora chegou a hora.
A violação é a do princípio da responsabilidade única (SRP). Resolvi com um mecanismo que existe em muitos frameworks: uma função que recebe a requisição e um next para chamar e passar a vez a quem vem depois. Usam esse mecanismo o Express (next()) e o Koa (await next()) no Node.js, o ASP.NET Core (await next(context)) em C#, o Laravel ($next($request)) em PHP, o Starlette e o FastAPI (call_next) em Python, o Gin (c.Next()) em Go e o axum (next.run(request)) em Rust.
É a Chain of Responsibility na variante chamada Intercepting Filter: cada elo pode agir antes, passar a vez com next e agir depois, ou parar. No DelphiMVCFramework esse next se chama ANext. Este artigo trata da primeira metade do trabalho: os filtros que rodam antes do roteamento.
Como funciona um filtro HTTP
Um filtro HTTP é esta procedure:

Copiar o código
procedure(const AContext: TWebContext; const ANext: TMVCHTTPFilterNext)
Ele envolve a requisição inteira: os middlewares clássicos, o roteamento e a action. O que você escreve antes de ANext() roda antes de tudo, o que você escreve depois roda quando a resposta está completa, ou seja, depois da action. Para parar a requisição você não chama ANext(), e nenhuma action é executada.
O nome é HTTP porque ele trabalha no nível do protocolo: vê método, header e body da requisição e da resposta. Funciona também com HTTPS, porque a criptografia TLS fica numa camada mais baixa e o filtro recebe a requisição já descriptografada.
| Middleware clássico | Filtro HTTP | |
|---|---|---|
| Interface | IMVCMiddleware, quatro métodos | uma procedure com AContext e ANext |
| Você registra com | AEngine.AddMiddleware(...) | AEngine.UseHTTPFilter(...) |
| Quando roda | nos quatro pontos do ciclo, na ordem de registro | em volta da requisição inteira: middlewares clássicos, roteamento e action |
| Antes e depois | dois métodos separados | a mesma procedure: antes e depois de ANext() |
| Para parar a requisição | Handled := True | você não chama ANext() |
| Controller | sim | sim |
UseHTTPFilter é um método de TMVCEngine e retorna TMVCEngine, então você pode encadear as chamadas. Os filtros prontos ficam em MVCFramework.Filters.
O filtro HTTP funciona do mesmo jeito com os controllers e com a Minimal API, e é por isso que o wizard pode usar a mesma cadeia nos dois casos. Os filtros são tratados em detalhe no capítulo 12 do guia oficial, segunda edição.
Para onde vão os quatro hooks?
OnBeforeRoutingvira o código antes deANext().OnAfterRoutingvira o código depois deANext(). O nome engana: dispara no fim de tudo, não logo depois do roteamento. Com umtry..finallyem volta deANext()esse código roda também quando a action levanta uma exceção. No middleware clássico, nesse caso,OnAfterRoutingé pulado.OnBeforeControllerActioneOnAfterControllerActionnão têm equivalente em um filtro HTTP. Recebem o nome do controller e o da action, que só são conhecidos depois do roteamento, enquanto o filtro HTTP envolve a requisição inteira sem conhecê-los. Quem os tem é o filtro que roda depois do roteamento,UseEndpointFilter: é a segunda responsabilidade, a que comento no fim.
Um filtro escrito à mão
Para escrever um filtro não precisa de uma classe: você passa para UseHTTPFilter um método anônimo com a assinatura que acabou de ver. Este adiciona dois headers de segurança a cada resposta:

Copiar o código
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);
Os headers você define antes de ANext(), assim uma action que queira outro valor o sobrescreve. O filtro pronto SecurityHeaders() coloca X-XSS-Protection: 0 e X-Content-Type-Options: nosniff. Este foi feito à mão para mostrar a forma, e adiciona X-Frame-Options.
Um filtro também pode parar a requisição. Este responde 400 aos POST, PUT e PATCH com body vazio ou só de espaços, antes que o roteamento a veja:

Copiar o código
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() fica no ramo else: quando o filtro rejeita a requisição, ANext() não é chamado e nenhum controller a vê.
Os dezessete middlewares deprecados
Dezessete middlewares estão marcados como deprecated: catorze classes TMVC...Middleware e três funções Use...SessionMiddleware, que criam os middlewares de sessão. Cada um tem o seu filtro em MVCFramework.Filters:
| Middleware deprecado | Filtro | Como registrar |
|---|---|---|
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])), depois de ActiveRecordFilter |
ActiveRecordFilter e DatabaseSessionFilter não ficam em MVCFramework.Filters, mas em MVCFramework.Filters.ActiveRecord, Swagger em MVCFramework.Filters.Swagger e OpenAPI em MVCFramework.Filters.OpenAPI. Assim a camada base não depende do FireDAC, do SwagDoc nem do emissor OpenAPI. Quem usa esses filtros adiciona a unit às uses; as assinaturas não mudam.
WaitForShutdownOrConsoleReturn, que no middleware Shutdown era um método de classe, agora é uma procedure de MVCFramework.Filters. StaticFiles tem também um overload que recebe um TMVCStaticFilesOptions, com as opções do middleware clássico: charset, fallback para single page applications, personalização dos tipos MIME, regras por requisição.
Os últimos cinco são filtros novos que os controllers também podem usar assim:
- Os filtros de sessão colocam a factory da sessão em cada requisição, e você usa
Context.Sessioncomo sempre.DatabaseSessionFilterdeve ser registrado depois deActiveRecordFilter. ActiveRecordFilterpega a conexão antes do dispatch e a libera em umfinally, portanto também quando a action levanta uma exceção. As conexões adicionais valem para cada instância do filtro: o middleware as ativava só para a primeira do processo.RateLimitcomTMVCRateLimitOptionstem as mesmas opções e a mesma resposta do middleware: chave por IP, usuário, API key ou personalizada, prefixos de caminho excluídos, prefixo dos headersX-RateLimit-*, um callback quando uma requisição é rejeitada. A resposta é 429 comRetry-Aftere um body JSON.RateLimit(10, 60)continua como era.
Se o seu código ainda usa um desses middlewares, o compilador diz isto:
[dcc32 Warning] W1000 Symbol 'TMVCStaticFilesMiddleware' is deprecated: 'Use the StaticFiles HTTP filter (MVCFramework.Filters): AEngine.UseHTTPFilter(StaticFiles(...))'
É um aviso, não um erro. O código continua compilando e funcionando.
O que o wizard gera
Em consequência disso, o wizard dos projetos novos, o do post sobre a RC7, gera os filtros no lugar desses middlewares. Para um projeto de controllers com arquivos estáticos, CORS, ETag, compressão, trace, rate limit, ActiveRecord e sessões, EngineConfigU contém uma cadeia só:

Copiar o código
// 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))));
O bloco é um exemplo: os nomes da conexão dependem do projeto gerado, e lRateLimitOptions é preparado logo acima com TMVCRateLimitOptions.Default.
O primeiro registrado é o mais externo. A ordem que o wizard usa é Redirect, StaticFiles, Analytics, Trace, CORS, ETag, Compression, RateLimit, a documentação da API, ActiveRecordFilter e os filtros de sessão. Sessões e conexão com o banco ficam no ponto mais interno, assim uma requisição rejeitada com 429 ou servida como arquivo estático não abre uma conexão.
ETag fica antes de Compression porque o hash é calculado depois que ANext() retornou. Se ETag é o mais externo, a compressão já reescreveu o body, e o ETag corresponde aos bytes que o cliente recebe.
Em relação a AddMiddleware, a ordem de registro não importa: os filtros envolvem de qualquer forma os middlewares clássicos, até os que respondem sozinhos, como os arquivos estáticos ou o redirect.
Para OpenAPI e Swagger o wizard usa UseHTTPFilter(Swagger(...)) para o endpoint do documento e UseHTTPFilter(StaticFiles(lSwaggerUIOptions)) para a Swagger UI sob /swagger. O mesmo vale para os projetos WebBroker e para os de Minimal API, que já usavam a cadeia.
Nas uses o wizard não escreve mais MVCFramework.Middleware.Redirect, .StaticFiles, .Analytics, .Trace, .CORS, .ETag, .Compression, .Swagger, .RateLimit, .Session e .ActiveRecord. Adiciona MVCFramework.Filters e, quando necessário, MVCFramework.Filters.ActiveRecord, MVCFramework.Filters.Swagger e MVCFramework.Filters.OpenAPI, mais MVCFramework.MinimalAPI nos projetos Minimal API.
O wizard instalado na IDE gera a partir dos templates que estão dentro do package. Para ver a cadeia nova você precisa do package da RC9.
Se você vai migrar um projeto
Com a 4.0 os middlewares não existirão mais, então, para os que já têm um filtro, a troca precisa ser feita antes de atualizar. Para os três mais comuns é assim (nas uses adicione MVCFramework.Filters). StaticFiles nessa forma não é idêntico ao middleware: o fallback para single page applications está desligado (falo disso mais abaixo):

Copiar o código
// 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('*'));
Os valores padrão de alguns filtros não coincidem com os do middleware. Comparei o código-fonte dos dois, filtro por filtro, para estes.
CORS
TMVCCORSMiddleware | CORS | |
|---|---|---|
| Credenciais | ativas | desligadas |
| Headers permitidos | Content-Type, Accept, jwtusername, jwtpassword, authentication, authorization | Content-Type,Authorization |
| Métodos | POST,GET,OPTIONS,PUT,DELETE,PATCH | GET,POST,PUT,DELETE,PATCH,OPTIONS |
| Max-age | 60 segundos | 1728000 segundos |
Se um cliente depende dos valores antigos, você os passa como parâmetros:

Copiar o código
AEngine.UseHTTPFilter(CORS('*', True, '',
'Content-Type, Accept, jwtusername, jwtpassword, authentication, authorization',
'POST,GET,OPTIONS,PUT,DELETE,PATCH', 60));
O filtro responde ao preflight antes do roteamento e adiciona os headers também às respostas de erro: um 404 traz Access-Control-Allow-Origin.
ETag
O filtro escreve o ETag entre aspas, como pede a RFC 7232, aceita o prefixo W/ em If-None-Match e age só nas respostas 2xx. O middleware clássico escreve o digest SHA-1 sem aspas e o compara, do jeito que está, com If-None-Match. Se a action já definiu um ETag, o clássico o mantém, o filtro o substitui pelo seu. Nos dois casos só as respostas que têm um ContentStream recebem o ETag: uma resposta em string não recebe.
Redirect
O filtro responde 302 (301 com rdkPermanent, o terceiro parâmetro) e preserva a query string (APreserveQuery, verdadeiro por padrão). Compara o caminho sem diferenciar maiúsculas de minúsculas; o middleware compara o texto exato.
Analytics
O arquivo CSV tem as mesmas colunas. A quinta, que no middleware contém controller e action, no filtro contém o PathInfo: um filtro HTTP não tem um controller para nomear.
Swagger
O filtro Swagger, que fica em MVCFramework.Filters.Swagger, envolve o mesmo TMVCSwaggerMiddleware; OpenAPI fica em MVCFramework.Filters.OpenAPI. O middleware JWT é procurado quando o documento é gerado, com a requisição já recebida, e a ordem de registro não importa.
Sessões
Os dois Boolean dos middlewares de sessão, HttpOnly e Secure, nos filtros viram um set, TMVCSessionCookieFlags, que por padrão vale [scfHttpOnly]. Portanto não basta trocar o nome da função:
![UseFileSessionMiddleware(10, True, ‘sessions’, True) vira FileSessionFilter(10, ‘sessions’, [scfHttpOnly, scfSecure]): os dois Boolean viram um set de flags](/images/dmvcframework-middleware-to-http-filters/file-session-before-after.png)
Copiar o código
// before
AEngine.AddMiddleware(UseFileSessionMiddleware(10, True, 'sessions', True));
// after
AEngine.UseHTTPFilter(FileSessionFilter(10, 'sessions', [scfHttpOnly, scfSecure]));
Se você copiar os parâmetros um a um, o compilador reclama, porque onde havia um Boolean agora há um set. Para construir o set a partir de um Boolean existe SessionCookieFlags: tem sempre HttpOnly, e Secure só se o parâmetro for verdadeiro. É o que o código gerado pelo wizard usa.
Para uma session factory qualquer existe SessionFilter(factory), e Session(factory) para os grupos Minimal API: o filtro passa a ser o dono dela. MemorySessionFilter, FileSessionFilter e DatabaseSessionFilter são construídos em cima de SessionFilter, e este último fica em MVCFramework.Filters.ActiveRecord.
Boolean e enum
Nas assinaturas dos filtros evitei os Boolean quando o True não se explica sozinho, a boolean trap. Em UseFileSessionMiddleware(10, True, 'sessions', True) quem lê não sabe qual True é HttpOnly e qual é Secure, e precisa abrir a declaração. Os filtros de sessão recebem, em vez disso, o set TMVCSessionCookieFlags, que se lê sozinho: [scfHttpOnly, scfSecure]. O mesmo vale para Redirect, que distingue 302 e 301 com TMVCRedirectKind (rdkTemporary, rdkPermanent) em vez de um Boolean. Não converti todos: continuam Boolean, por exemplo, as credenciais de CORS e APreserveQuery de Redirect.
Rate limit
RateLimit(10, 60) continua como era. Para as opções você prepara um TMVCRateLimitOptions, como faz o código gerado pelo wizard:

Copiar o código
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
A forma simples, StaticFiles('/static', lWwwPath), e a com as opções, StaticFiles(TMVCStaticFilesOptions), são a mesma implementação. A diferença principal em relação ao TMVCStaticFilesMiddleware é o fallback para single page applications: no middleware estava ativo por padrão, no filtro está desligado. Na forma simples, você liga o fallback com sfmSPA:

Copiar o código
AEngine.UseHTTPFilter(StaticFiles('/', 'www/app', sfmSPA));
Com sfmSPA o filtro deixa a requisição passar para o router, e responde com o index.html da pasta existente mais próxima só a um GET ou HEAD para o qual a aplicação não tem nenhuma rota. As rotas da API nunca são escondidas, então a SPA pode ficar em / com a API ao lado. Com as opções você define SPAWebAppSupport := True, que em TMVCStaticFilesOptions.Default vale False. O terceiro parâmetro da forma simples é o modo (sfmFiles ou sfmSPA), o documento de índice, index.html por padrão, é o quarto.
Dois limites. Você precisa de AllowUnhandledAction = false, que é o valor padrão. E o router continua escrevendo no log “ROUTE NOT FOUND” para cada rota do lado do cliente.
O resto é como no middleware: o prefixo não diferencia maiúsculas de minúsculas, com ou sem / final, os arquivos saem com o tipo MIME e com Last-Modified (e 304 em If-Modified-Since), uma pasta sem / final responde 301 e depois serve o seu index.html. Restam duas diferenças. O middleware escrevia no log uma linha para cada arquivo servido, com método, status, caminho, IP e duração, e os filtros não. O filtro serve .webp, .woff, .woff2 e .ttf com o tipo certo, tipos que o middleware não conhecia. As outras opções do middleware, o charset, os tipos MIME personalizados e as regras por requisição, ficam disponíveis com TMVCStaticFilesOptions.
Compression
O limiar de 1024 bytes, o nível de compressão e a escolha da codificação (a primeira entre gzip e deflate na ordem do header Accept-Encoding) são os do middleware. Muda uma coisa: o filtro não comprime uma resposta que já tem um Content-Encoding. RangeMedia define identity como Content-Encoding das respostas parciais, e comprimi-las quebraria o Content-Range.
Os outros
Trace se comporta como o middleware. Cada linha de log traz o ID de correlação [rid:...], e o mesmo ID volta no header de resposta X-Request-ID: é o do chamador, se houver; caso contrário, o filtro gera um de seis dígitos hexadecimais. Authorization aparece com o esquema e três asteriscos, Bearer ***, e os headers de resposta Authorization, Proxy-Authorization, Cookie e Set-Cookie são ocultados. O body é registrado até 1024 bytes. A assinatura tem um parâmetro a mais, o último: Trace(AMaxBodySize, ALogTag, ALogAuthorization, AHideNonTextBody, ACorrelationHeaderName), e o nome do header é X-Request-ID por padrão. Com ALogAuthorization = True o Authorization da requisição é escrito em texto claro.
SecurityHeaders escreve os mesmos dois headers do middleware, X-XSS-Protection: 0 e X-Content-Type-Options: nosniff, e não tem parâmetros. Shutdown responde só aos POST em /api/shutdown, como antes. RangeMedia e OpenAPI têm os mesmos valores do middleware, e OpenAPI serve o documento em /openapi.json. ActiveRecordFilter lê FDConnectionDefs.ini como o middleware, mas é uma implementação nativa e libera a conexão em um finally: com o middleware, se a action levantava uma exceção, OnAfterRouting não rodava e a conexão não era liberada.
Comparei os valores padrão lendo o código-fonte do middleware e do filtro. Se você encontrar uma diferença que eu não listei, abra uma issue.
E o JWT e os outros?
Os middlewares de autenticação agora também têm os seus filtros e estão deprecados: JWT (header e cookie), blacklist, refresh token, autenticação Basic, personalizada e RoleBased, OIDC e CSRF. Não os coloquei entre os dezessete porque os filtros deles são feitos de outro jeito.
É a segunda responsabilidade de que eu falava no início. A autorização dos controllers decide pelo nome do controller e pelo da action, e quem os recebe são OnRequest e OnAuthorization. Esses nomes são conhecidos depois do roteamento, e um filtro HTTP envolve a requisição sem conhecê-los. Por isso as partes que não precisam de uma rota, ou seja login, logout, callback e blacklist, são filtros HTTP, enquanto o controle da requisição é um filtro de endpoint (EndpointFilter), que roda depois do roteamento e se registra com UseEndpointFilter. Cada filtro executa o código do seu middleware, então controla e responde do mesmo jeito.
Como os dezessete, os middlewares de autenticação vão continuar funcionando até a 4.0, exclusive, com um aviso do compilador que nomeia o filtro a usar. Em outro post vou analisar esse outro tipo de filtros.
Perguntas frequentes
Por que o DelphiMVCFramework coloca os filtros HTTP ao lado dos middlewares?
O middleware clássico tinha duas responsabilidades: influenciar o roteamento e controlar como as actions são executadas. O filtro HTTP separa a primeira com o mecanismo do next, o mesmo de Express, Koa, ASP.NET Core, Laravel, Starlette, FastAPI, Gin e axum: uma procedure que recebe o contexto e ANext, e decide se e quando passar a requisição para o que vem depois.
Quais middlewares do DelphiMVCFramework estão deprecados?
Dezessete middlewares, catorze classes e três funções de criação: TMVCRedirectMiddleware, TMVCStaticFilesMiddleware, TMVCAnalyticsMiddleware, TMVCTraceMiddleware, TMVCCompressionMiddleware, TMVCETagMiddleware, TMVCCORSMiddleware, TMVCSecurityHeadersMiddleware, TMVCShutdownMiddleware, TMVCRangeMediaMiddleware, TMVCSwaggerMiddleware, TMVCOpenAPI3Middleware, TMVCRateLimitMiddleware, TMVCActiveRecordMiddleware e as funções UseMemorySessionMiddleware, UseFileSessionMiddleware, UseDatabaseSessionMiddleware. Cada um tem um filtro HTTP em MVCFramework.Filters (Redirect, StaticFiles, Analytics, Trace, Compression, ETag, CORS, SecurityHeaders, Shutdown, RangeMedia, Swagger, OpenAPI, RateLimit, ActiveRecordFilter, MemorySessionFilter, FileSessionFilter, DatabaseSessionFilter). ActiveRecordFilter e DatabaseSessionFilter ficam em MVCFramework.Filters.ActiveRecord, Swagger em MVCFramework.Filters.Swagger e OpenAPI em MVCFramework.Filters.OpenAPI: quem os usa adiciona a unit às uses. Também estão deprecados os middlewares de autenticação (JWT no header e no cookie, blacklist, refresh token, Basic, autenticação personalizada, RoleBased, OIDC, CSRF), que têm os seus próprios filtros, em parte HTTP e em parte de endpoint.
Meu servidor existente vai parar de funcionar? Não, não na linha 3.5. Nada foi removido. O compilador mostra o aviso W1000 para cada middleware deprecado que você usa, e o código compila e roda como antes.
O que acontece com os middlewares na 4.0?
Serão eliminados de vez. Até a 4.0, exclusive, continuam funcionando, com um aviso do compilador. Todos os middlewares deprecados já têm o seu filtro e podem ser substituídos hoje: os dezessete deste artigo por um filtro HTTP, os de autenticação por filtros em parte HTTP e em parte de endpoint (EndpointFilter).
Posso usar sessões, ActiveRecord e rate limit como filtros nos controllers?
Sim. MemorySessionFilter, FileSessionFilter, DatabaseSessionFilter, ActiveRecordFilter e RateLimit com TMVCRateLimitOptions são filtros HTTP, portanto valem para os controllers. DatabaseSessionFilter deve ser registrado depois de ActiveRecordFilter. O wizard os gera no lugar dos middlewares clássicos.
E os middlewares de JWT, Basic, OIDC e CSRF?
Eles também têm os seus filtros e estão deprecados. As partes que não precisam de uma rota, ou seja login, logout, callback e blacklist, são filtros HTTP. O controle da requisição é um filtro de endpoint (EndpointFilter), que roda depois do roteamento: a autorização dos controllers se baseia no nome do controller e da action, que não são conhecidos antes do roteamento. Os middlewares continuam funcionando até a 4.0, exclusive.
Em que ordem registro filtros e middlewares?
Em relação a AddMiddleware a ordem não importa: os filtros envolvem de qualquer forma os middlewares clássicos. Entre os filtros, o primeiro registrado é o mais externo, ETag vai antes de Compression, e ActiveRecordFilter e os filtros de sessão ficam no ponto mais interno.
O que muda nos valores padrão do CORS ao passar para o filtro CORS?
O filtro parte com as credenciais desligadas, os headers Content-Type,Authorization, os métodos GET,POST,PUT,DELETE,PATCH,OPTIONS e um max-age de 1728000 segundos. O middleware clássico partia com as credenciais ativas, uma lista de headers mais longa e os mesmos métodos em outra ordem. Quem depende dos valores antigos os passa como parâmetros para CORS.
O que muda, além do CORS, nos valores padrão dos filtros em relação aos middlewares?
O fallback para single page applications do StaticFiles vem desligado por padrão, enquanto no middleware estava ativo: liga-se com StaticFiles('/', folder, sfmSPA) ou com SPAWebAppSupport nas opções de TMVCStaticFilesOptions, e o router decide, portanto as rotas da API não são escondidas e a SPA pode ficar também em /. O filtro não escreve a linha de log por arquivo servido que o middleware escrevia, e Compression não comprime uma resposta que já tem um Content-Encoding. Trace, SecurityHeaders, Shutdown, OpenAPI, RangeMedia e ActiveRecordFilter têm os mesmos valores padrão dos middlewares.
Recursos
- O guia oficial, segunda edição - os filtros no capítulo 12, a Minimal API no capítulo 29, na Leanpub
- DelphiMVCFramework 3.5 RC7 entra no Project Manager do Delphi e conversa com os agentes de IA - o wizard, o menu do Project Manager, OpenAPI 3
- Delphi Minimal API - as rotas com métodos anônimos, sem classes controller
- Repositório no GitHub - código-fonte e samples, também para abrir uma issue
– Daniele Teti
Fatos principais
- Tópico: a diferença entre middlewares clássicos (IMVCMiddleware, AddMiddleware) e filtros HTTP (TMVCHTTPFilter, UseHTTPFilter) no DelphiMVCFramework, o framework REST e web para Delphi, e como migrar. Linha 3.5.0-silicon; estará na RC9 (3.5.0-silicon-rc9). O trabalho descrito ainda não está em uma release. O artigo trata dos filtros que rodam antes do roteamento.
- Não é um breaking change: na 3.5.0-silicon os filtros são um recurso adicional e os servidores existentes continuam funcionando do mesmo jeito, com um aviso do compilador (W1000) para cada middleware deprecado. O breaking change está previsto para a 4.0, onde os middlewares serão eliminados de vez. A transição é gradual porque o DelphiMVCFramework é usado em ambientes críticos (hospitais, bancos, fábricas, videogames, ERPs).
- Por quê: o middleware clássico tinha duas responsabilidades, influenciar o roteamento (OnBeforeRouting, OnAfterRouting) e controlar como as actions são executadas (OnBeforeControllerAction, OnAfterControllerAction). O filtro HTTP separa a primeira com o mecanismo do next, usado também por Express, Koa, ASP.NET Core, Laravel, Starlette, FastAPI, Gin e axum: uma procedure (AContext, ANext) que roda antes do roteamento e envolve todo o dispatch; vale para controllers e Minimal API. OnBeforeRouting corresponde ao código antes de ANext(); OnAfterRouting, que dispara no fim de tudo, ao código depois de ANext().
- Os filtros HTTP tomam o lugar dos middlewares onde existe um equivalente. Dezessete middlewares estão marcados como deprecated (aviso W1000, não erro), catorze classes e três funções: TMVCRedirectMiddleware, TMVCStaticFilesMiddleware, TMVCAnalyticsMiddleware, TMVCTraceMiddleware, TMVCCompressionMiddleware, TMVCETagMiddleware, TMVCCORSMiddleware, TMVCSecurityHeadersMiddleware, TMVCShutdownMiddleware, TMVCRangeMediaMiddleware, TMVCSwaggerMiddleware, TMVCOpenAPI3Middleware, TMVCRateLimitMiddleware, TMVCActiveRecordMiddleware, UseMemorySessionMiddleware, UseFileSessionMiddleware, UseDatabaseSessionMiddleware. Filtros: Redirect, StaticFiles, Analytics, Trace, Compression, ETag, CORS, SecurityHeaders, Shutdown, RangeMedia, Swagger, OpenAPI, RateLimit (com TMVCRateLimitOptions), ActiveRecordFilter, MemorySessionFilter, FileSessionFilter, DatabaseSessionFilter (unit MVCFramework.Filters, exceto ActiveRecordFilter e DatabaseSessionFilter em MVCFramework.Filters.ActiveRecord, Swagger em MVCFramework.Filters.Swagger e OpenAPI em MVCFramework.Filters.OpenAPI, a serem adicionadas às uses; UseHTTPFilter é um método de TMVCEngine). Nada foi removido. Em consequência disso, o wizard dos projetos novos gera os filtros no lugar desses middlewares.
- Os middlewares de autenticação também têm os seus filtros e estão deprecados: JWT (header e cookie), blacklist, refresh token, Basic, autenticação personalizada, RoleBased, OIDC, CSRF. Login, logout, callback e blacklist são filtros HTTP; o controle da requisição é um filtro de endpoint (EndpointFilter, registrado com UseEndpointFilter), que roda depois do roteamento porque a autorização dos controllers decide pelo nome do controller e da action. Todos os middlewares deprecados continuam funcionando até a 4.0, exclusive; na 4.0 serão eliminados de vez.
- Ordem: o primeiro filtro registrado é o mais externo e os filtros envolvem os middlewares clássicos qualquer que seja a ordem em relação a AddMiddleware; ETag antes de Compression; ActiveRecordFilter e sessões no ponto mais interno, assim uma requisição rejeitada (429) ou servida como arquivo estático não abre uma conexão.
- Diferenças para quem migra: CORS parte com as credenciais desligadas, header Content-Type,Authorization, max-age 1728000 (o middleware: credenciais ativas, outros headers, max-age 60); ETag entre aspas com suporte a W/ (o middleware: digest sem aspas); Redirect compara o caminho sem diferenciar maiúsculas de minúsculas; Analytics grava o PathInfo no lugar de controller e action; os filtros de sessão recebem um set TMVCSessionCookieFlags ([scfHttpOnly, scfSecure], padrão [scfHttpOnly]) no lugar dos dois Boolean HttpOnly e Secure dos antigos Use*SessionMiddleware, com o helper SessionCookieFlags(ASecure) para construí-lo a partir de um Boolean; Redirect distingue 302 e 301 com TMVCRedirectKind (rdkTemporary, padrão, ou rdkPermanent) em vez de um Boolean: nas assinaturas dos filtros, os Boolean que não se explicam sozinhos foram substituídos por tipos que se leem sozinhos (a boolean trap). StaticFiles tem o fallback para single page applications desligado por padrão (no middleware está ativo): liga-se com StaticFiles('/', folder, sfmSPA) ou com SPAWebAppSupport := True nas opções (TMVCStaticFilesOptions): o filtro deixa o router decidir e responde index.html só a um GET ou HEAD para o qual a aplicação não tem nenhuma rota, portanto pode ficar também em / com a API ao lado (é preciso AllowUnhandledAction = false, o padrão, e o router escreve no log ROUTE NOT FOUND para cada rota do lado do cliente); o terceiro parâmetro da forma simples é o modo (TMVCStaticFilesMode), o documento de índice é o quarto; os filtros não escrevem a linha de log por arquivo servido que o middleware escrevia, e StaticFiles serve .webp, .woff, .woff2 e .ttf com o tipo certo. Compression não recomprime uma resposta que já tem um Content-Encoding. SecurityHeaders, Shutdown, OpenAPI, RangeMedia e Trace têm os mesmos valores padrão dos middlewares (Trace retorna o ID de correlação X-Request-ID, oculta Authorization e registra o body até 1024 bytes, com um parâmetro a mais, ACorrelationHeaderName); ActiveRecordFilter libera a conexão mesmo que a action levante uma exceção e ativa as conexões adicionais para cada instância. RateLimit com opções: mesmas opções e mesma resposta do middleware (429, Retry-After, corpo JSON).
- Autor: Daniele Teti, criador do DelphiMVCFramework.
Comments