Become a member!

DelphiMVCFramework 3.5.0-silicon: dos middlewares aos filtros, o que vai mudar no seu servidor?

🌐
Este artigo também está disponível em outros idiomas:
🇮🇹 Italiano  •  🇬🇧 English  •  🇪🇸 Español  •  🇩🇪 Deutsch  •  🇫🇷 Français

DelphiMVCFramework, o framework open source REST e web para Delphi

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:

Assinatura de um filtro HTTP: uma procedure com AContext do tipo TWebContext e ANext do tipo TMVCHTTPFilterNext

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ássicoFiltro HTTP
InterfaceIMVCMiddleware, quatro métodosuma procedure com AContext e ANext
Você registra comAEngine.AddMiddleware(...)AEngine.UseHTTPFilter(...)
Quando rodanos quatro pontos do ciclo, na ordem de registroem volta da requisição inteira: middlewares clássicos, roteamento e action
Antes e depoisdois métodos separadosa mesma procedure: antes e depois de ANext()
Para parar a requisiçãoHandled := Truevocê não chama ANext()
Controllersimsim

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?

  • OnBeforeRouting vira o código antes de ANext().
  • OnAfterRouting vira o código depois de ANext(). O nome engana: dispara no fim de tudo, não logo depois do roteamento. Com um try..finally em volta de ANext() esse código roda também quando a action levanta uma exceção. No middleware clássico, nesse caso, OnAfterRouting é pulado.
  • OnBeforeControllerAction e OnAfterControllerAction nã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:

Filtro HTTP escrito à mão que define os headers X-Content-Type-Options e X-Frame-Options e depois chama ANext

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:

Filtro HTTP que responde 400 aos POST, PUT e PATCH com body vazio ou só de espaços e chama ANext apenas no ramo else

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 deprecadoFiltroComo registrar
TMVCRedirectMiddlewareRedirectUseHTTPFilter(Redirect(['/'], '/web'))
TMVCStaticFilesMiddlewareStaticFilesUseHTTPFilter(StaticFiles('/static', lWwwPath))
TMVCAnalyticsMiddlewareAnalyticsUseHTTPFilter(Analytics())
TMVCTraceMiddlewareTraceUseHTTPFilter(Trace())
TMVCCompressionMiddlewareCompressionUseHTTPFilter(Compression(1024))
TMVCETagMiddlewareETagUseHTTPFilter(ETag())
TMVCCORSMiddlewareCORSUseHTTPFilter(CORS('*'))
TMVCSecurityHeadersMiddlewareSecurityHeadersUseHTTPFilter(SecurityHeaders())
TMVCShutdownMiddlewareShutdownUseHTTPFilter(Shutdown('apikey', 'a-strong-secret'))
TMVCRangeMediaMiddlewareRangeMediaUseHTTPFilter(RangeMedia('/static/media', 'static/media'))
TMVCSwaggerMiddlewareSwaggerUseHTTPFilter(Swagger(AEngine, lSwaggerInfo, '/openapi.json'))
TMVCOpenAPI3MiddlewareOpenAPIUseHTTPFilter(OpenAPI(AEngine, lOpenAPIInfo, '/openapi.json'))
TMVCRateLimitMiddlewareRateLimitUseHTTPFilter(RateLimit(lRateLimitOptions))
TMVCActiveRecordMiddlewareActiveRecordFilterUseHTTPFilter(ActiveRecordFilter('conn_def_name', 'FDConnectionDefs.ini'))
UseMemorySessionMiddlewareMemorySessionFilterUseHTTPFilter(MemorySessionFilter(10, [scfHttpOnly, scfSecure]))
UseFileSessionMiddlewareFileSessionFilterUseHTTPFilter(FileSessionFilter(10, 'sessions', [scfHttpOnly, scfSecure]))
UseDatabaseSessionMiddlewareDatabaseSessionFilterUseHTTPFilter(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.Session como sempre. DatabaseSessionFilter deve ser registrado depois de ActiveRecordFilter.
  • ActiveRecordFilter pega a conexão antes do dispatch e a libera em um finally, 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.
  • RateLimit com TMVCRateLimitOptions tem 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 headers X-RateLimit-*, um callback quando uma requisição é rejeitada. A resposta é 429 com Retry-After e 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ó:

Cadeia de filtros HTTP gerada pelo wizard em EngineConfigU: Redirect, StaticFiles, Trace, CORS, ETag, Compression, RateLimit, ActiveRecordFilter e MemorySessionFilter

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):

Antes e depois: AddMiddleware com TMVCRedirectMiddleware, TMVCStaticFilesMiddleware e TMVCCORSMiddleware vira UseHTTPFilter com Redirect, StaticFiles e CORS

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

TMVCCORSMiddlewareCORS
Credenciaisativasdesligadas
Headers permitidosContent-Type, Accept, jwtusername, jwtpassword, authentication, authorizationContent-Type,Authorization
MétodosPOST,GET,OPTIONS,PUT,DELETE,PATCHGET,POST,PUT,DELETE,PATCH,OPTIONS
Max-age60 segundos1728000 segundos

Se um cliente depende dos valores antigos, você os passa como parâmetros:

Filtro CORS com os parâmetros que reproduzem os valores do middleware clássico: credenciais ativas, headers e métodos da lista antiga, max-age 60

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

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:

RateLimit com TMVCRateLimitOptions: 10 requisições em 60 segundos, caminho /health excluído, callback OnExceeded que escreve no log

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:

StaticFiles em / com sfmSPA, para uma single page application com a API ao lado

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

– 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