Become a member!

DelphiMVCFramework 3.5.0-silicon: de los middleware a los filtros, ¿qué cambiará en tu servidor?

🌐
Este artículo también está disponible en otros idiomas:
🇮🇹 Italiano  •  🇬🇧 English  •  🇩🇪 Deutsch  •  🇫🇷 Français  •  🇧🇷 Português

DelphiMVCFramework, el framework open source REST y web para Delphi

Un middleware clásico de DelphiMVCFramework tiene cuatro métodos. Un filtro HTTP tiene un solo procedimiento. ¿Por qué he cambiado un mecanismo que funcionaba desde hacía años, adónde van a parar los cuatro hooks, en qué orden registras los filtros y qué pasa con el servidor que ya tienes en producción?

Durante años, en DelphiMVCFramework la forma de enganchar código al ciclo de una petición han sido los middleware, con AddMiddleware. Los filtros HTTP, con UseHTTPFilter, llegaron con la Minimal API de la 3.5, la del post sobre la RC6. Ahora los filtros ocupan el lugar de los middleware allí donde existe uno equivalente: diecisiete middleware están marcados como deprecated, cada uno con su filtro. Estará en la RC9 de la 3.5.0-silicon.

✅ No es un breaking change. Los servidores actuales siguen funcionando igual, sin cambios y sin problemas.

En la 3.5.0-silicon los filtros son una funcionalidad adicional: los middleware se quedan donde están, y el compilador muestra un aviso por cada middleware marcado como deprecated. El breaking change llegará en la 4.0, donde los middleware se eliminarán por completo.

DelphiMVCFramework es el framework más popular de su tipo para Delphi en GitHub, con más de 50 contributors, y se usa en entornos críticos: hospitales, bancos, fábricas, videojuegos, ERP con cientos de usuarios. Por eso he decidido una transición suave, sin traumas.

En este artículo hablo de los filtros HTTP, los que se ejecutan antes del enrutamiento.


Dos responsabilidades en una clase

Hasta ahora un middleware hacía dos cosas. Con OnBeforeRouting y OnAfterRouting influía en el enrutamiento: podía responder antes de que el router mirara la petición, para servir un archivo estático, hacer un redirect o contestar a un preflight de CORS, y al final de la petición observar o modificar la respuesta, como hacen ETag y analytics. Con OnBeforeControllerAction y OnAfterControllerAction controlaba cómo se ejecutaban las acciones. Cuatro métodos en una sola clase, para dos tareas distintas.

Quien me conoce, o ha seguido alguno de mis cursos, sabe cuánta importancia doy a los principios SOLID. Encontrar una clase con dos tareas en mi proyecto open source más popular me molestaba de verdad. Cada vez que tocaba un middleware lo veía: o se usaba el par de métodos del enrutamiento (OnBeforeRouting y OnAfterRouting) dejando vacíos los otros dos, o se implementaba el par del endpoint (OnBeforeControllerAction y OnAfterControllerAction), que ya conoce controlador y acción porque se ejecuta después del enrutamiento. Era evidente que ese componente había que refactorizarlo. Ahora ha llegado el momento.

La violación es la del principio de responsabilidad única (SRP). La he resuelto con un mecanismo que existe en muchos frameworks: una función que recibe la petición y un next al que llamar para pasar el testigo a quien viene después. Lo usan Express (next()) y Koa (await next()) en Node.js, ASP.NET Core (await next(context)) en C#, Laravel ($next($request)) en PHP, Starlette y FastAPI (call_next) en Python, Gin (c.Next()) en Go y axum (next.run(request)) en Rust.

Es la Chain of Responsibility en la variante llamada Intercepting Filter: cada eslabón puede actuar antes, pasar el testigo con next y actuar después, o detenerse. En DelphiMVCFramework ese next se llama ANext. Este artículo trata de la primera mitad del trabajo: los filtros que se ejecutan antes del enrutamiento.


Cómo funciona un filtro HTTP

Un filtro HTTP es este procedimiento:

Firma de un filtro HTTP: un procedimiento con AContext de tipo TWebContext y ANext de tipo TMVCHTTPFilterNext

Copiar el código
procedure(const AContext: TWebContext; const ANext: TMVCHTTPFilterNext)

Envuelve toda la petición: los middleware clásicos, el enrutamiento y la acción. Lo que escribes antes de ANext() se ejecuta antes que todo, lo que escribes después se ejecuta cuando la respuesta está completa, es decir, después de la acción. Para detener la petición no llamas a ANext(), y no se ejecuta ninguna acción.

Se llama HTTP porque trabaja a nivel de protocolo: ve el método, las cabeceras y el cuerpo de la petición y de la respuesta. Funciona también con HTTPS, porque el cifrado TLS está más abajo y el filtro recibe la petición ya descifrada.

Middleware clásicoFiltro HTTP
InterfazIMVCMiddleware, cuatro métodosun procedimiento con AContext y ANext
Se registra conAEngine.AddMiddleware(...)AEngine.UseHTTPFilter(...)
Cuándo se ejecutaen los cuatro puntos del ciclo, en el orden de registroalrededor de toda la petición: middleware clásicos, enrutamiento y acción
Antes y despuésdos métodos separadosel mismo procedimiento: antes y después de ANext()
Para detener la peticiónHandled := Trueno llamas a ANext()
Controladoressísí

UseHTTPFilter es un método de TMVCEngine y devuelve TMVCEngine, así que puedes encadenar las llamadas. Los filtros listos para usar están en MVCFramework.Filters.

El filtro HTTP funciona con los controladores y con la Minimal API de la misma manera, y por eso el asistente puede usar la misma cadena en los dos casos. Los filtros se tratan a fondo en el capítulo 12 de la guía oficial, segunda edición.


¿Dónde están los cuatro hooks?

  • OnBeforeRouting se convierte en el código antes de ANext().
  • OnAfterRouting se convierte en el código después de ANext(). El nombre engaña: se dispara al final de todo, no justo después del enrutamiento. Con un try..finally alrededor de ANext(), ese código se ejecuta también cuando la acción lanza una excepción. En el middleware clásico, en ese caso OnAfterRouting se salta.
  • OnBeforeControllerAction y OnAfterControllerAction no tienen equivalente en un filtro HTTP. Reciben el nombre del controlador y el de la acción, que solo se conocen después del enrutamiento, mientras que el filtro HTTP envuelve toda la petición sin conocerlos. Los tiene el filtro que se ejecuta después del enrutamiento, UseEndpointFilter: es la segunda responsabilidad, de la que hablo al final.

Un filtro escrito a mano

Para escribir un filtro no hace falta una clase: le pasas a UseHTTPFilter un método anónimo con la firma que acabas de ver. Este añade dos cabeceras de seguridad a cada respuesta:

Filtro HTTP escrito a mano que establece las cabeceras X-Content-Type-Options y X-Frame-Options y luego llama a ANext

Copiar el 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);

Las cabeceras las estableces antes de ANext(): así, si una acción quiere otro valor, lo sobrescribe. El filtro incluido SecurityHeaders() pone X-XSS-Protection: 0 y X-Content-Type-Options: nosniff. Este está hecho a mano para mostrar la forma, y añade X-Frame-Options.

Un filtro también puede detener la petición. Este responde 400 a los POST, PUT y PATCH con un cuerpo vacío o formado solo por espacios, antes de que el enrutamiento la mire:

Filtro HTTP que responde 400 a los POST, PUT y PATCH con un cuerpo vacío o de solo espacios y llama a ANext únicamente en la rama else

Copiar el 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() está en la rama else: cuando el filtro rechaza la petición, no se llama a ANext() y ningún controlador la ve.


Los diecisiete middleware deprecados

Diecisiete middleware están marcados como deprecated: catorce clases TMVC...Middleware y tres funciones Use...SessionMiddleware, que crean los middleware de sesión. Cada uno tiene su filtro en MVCFramework.Filters:

Middleware deprecadoFiltroCómo lo registras
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])), después de ActiveRecordFilter

ActiveRecordFilter y DatabaseSessionFilter no están en MVCFramework.Filters sino en MVCFramework.Filters.ActiveRecord, Swagger en MVCFramework.Filters.Swagger y OpenAPI en MVCFramework.Filters.OpenAPI. Así la capa base no depende de FireDAC, de SwagDoc ni del emisor de OpenAPI. Quien usa esos filtros añade la unit a la uses. Las firmas no cambian.

WaitForShutdownOrConsoleReturn, que en el middleware Shutdown era un método de clase, ahora es un procedimiento de MVCFramework.Filters. StaticFiles tiene además una sobrecarga que toma un TMVCStaticFilesOptions, con las opciones del middleware clásico: charset, fallback para las single page application, personalización de los tipos MIME, reglas por petición.

Los últimos cinco son filtros nuevos que los controladores pueden usar también:

  • Los filtros de sesión ponen la factory de la sesión en cada petición, y Context.Session se usa como siempre. DatabaseSessionFilter se registra después de ActiveRecordFilter.
  • ActiveRecordFilter toma la conexión antes del dispatch y la libera en un finally, así que lo hace también cuando la acción lanza una excepción. Las conexiones adicionales valen para cada instancia del filtro: el middleware las activaba solo para la primera del proceso.
  • RateLimit con TMVCRateLimitOptions tiene las mismas opciones y la misma respuesta que el middleware: clave por IP, usuario, API key o personalizada, prefijos de ruta excluidos, prefijo de las cabeceras X-RateLimit-*, un callback cuando se rechaza una petición. La respuesta es 429 con Retry-After y un cuerpo JSON. RateLimit(10, 60) se queda como estaba.

Si tu código todavía usa uno de estos middleware, el compilador te dice esto:

[dcc32 Warning] W1000 Symbol 'TMVCStaticFilesMiddleware' is deprecated: 'Use the StaticFiles HTTP filter (MVCFramework.Filters): AEngine.UseHTTPFilter(StaticFiles(...))'

Es un aviso, no un error. El código sigue compilando y funcionando.


Qué genera el asistente

En consecuencia, el asistente de los proyectos nuevos, el del post sobre la RC7, genera los filtros en lugar de esos middleware. Para un proyecto de controladores con archivos estáticos, CORS, ETag, compresión, trace, rate limit, ActiveRecord y sesiones, EngineConfigU contiene una sola cadena:

Cadena de filtros HTTP generada por el asistente en EngineConfigU: Redirect, StaticFiles, Trace, CORS, ETag, Compression, RateLimit, ActiveRecordFilter y MemorySessionFilter

Copiar el 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))));

El bloque es un ejemplo: los nombres de la conexión dependen del proyecto generado, y lRateLimitOptions se prepara un poco más arriba con TMVCRateLimitOptions.Default.

El primero que se registra es el más externo. El orden que usa el asistente es Redirect, StaticFiles, Analytics, Trace, CORS, ETag, Compression, RateLimit, la documentación de la API, ActiveRecordFilter y los filtros de sesión. Las sesiones y la conexión a la base de datos están en el punto más interno, así una petición rechazada con 429 o servida como archivo estático no abre una conexión.

ETag va antes que Compression porque el hash lo calcula después de que ANext() haya vuelto. Si ETag es más externo, la compresión ya ha reescrito el cuerpo, y el ETag corresponde a los bytes que recibe el cliente.

Respecto a AddMiddleware, el orden de registro no cuenta: los filtros envuelven de todos modos los middleware clásicos, incluso los que responden por sí solos, como los archivos estáticos o el redirect.

Para OpenAPI y Swagger el asistente usa UseHTTPFilter(Swagger(...)) para el endpoint del documento y UseHTTPFilter(StaticFiles(lSwaggerUIOptions)) para la Swagger UI bajo /swagger. Lo mismo vale para los proyectos WebBroker y para los de Minimal API, que ya usaban la cadena.

En las uses el asistente ya no escribe MVCFramework.Middleware.Redirect, .StaticFiles, .Analytics, .Trace, .CORS, .ETag, .Compression, .Swagger, .RateLimit, .Session y .ActiveRecord. Añade MVCFramework.Filters y, cuando hacen falta, MVCFramework.Filters.ActiveRecord, MVCFramework.Filters.Swagger y MVCFramework.Filters.OpenAPI, más MVCFramework.MinimalAPI en los proyectos Minimal API.

El asistente instalado en el IDE genera a partir de las plantillas incluidas en el package. Para ver la cadena nueva hace falta el package de la RC9.


Si migras un proyecto

Con la 4.0 los middleware ya no existirán, así que para los middleware que ya tienen un filtro el cambio hay que hacerlo antes de actualizar. Para los tres más comunes es este (en las uses añade MVCFramework.Filters). StaticFiles en esta forma no es idéntico al middleware: el fallback para las single page application está apagado, y de eso hablo más abajo:

Antes y después: AddMiddleware con TMVCRedirectMiddleware, TMVCStaticFilesMiddleware y TMVCCORSMiddleware se convierte en UseHTTPFilter con Redirect, StaticFiles y CORS

Copiar el 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('*'));

Los valores por defecto de algunos filtros no coinciden con los del middleware. He comparado el código fuente de los dos, filtro por filtro, para estos.

CORS

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

Si un cliente depende de los valores antiguos, los pasas como parámetros:

Filtro CORS con los parámetros que reproducen los valores del middleware clásico: credenciales activas, cabeceras y métodos de la lista antigua, max-age 60

Copiar el código
AEngine.UseHTTPFilter(CORS('*', True, '',
  'Content-Type, Accept, jwtusername, jwtpassword, authentication, authorization',
  'POST,GET,OPTIONS,PUT,DELETE,PATCH', 60));

El filtro responde al preflight antes del enrutamiento y añade las cabeceras también a las respuestas de error: un 404 lleva Access-Control-Allow-Origin.

ETag

El filtro escribe el ETag entre comillas, como pide la RFC 7232, acepta el prefijo W/ en If-None-Match y actúa solo sobre las respuestas 2xx. El middleware clásico escribe el digest SHA-1 sin comillas y lo compara tal cual con If-None-Match. Si la acción ya ha establecido un ETag, el clásico lo deja y el filtro lo sustituye por el suyo. En los dos casos solo reciben el ETag las respuestas que tienen un ContentStream: una respuesta de tipo cadena no lo recibe.

Redirect

El filtro responde 302 (301 con rdkPermanent, el tercer parámetro) y conserva la query string (APreserveQuery, verdadero por defecto). Compara la ruta sin distinguir mayúsculas de minúsculas; el middleware compara el texto exacto.

Analytics

El archivo CSV tiene las mismas columnas. La quinta, que en el middleware contiene controlador y acción, en el filtro contiene el PathInfo: un filtro HTTP no tiene un controlador que nombrar.

Swagger

El filtro Swagger, que está en MVCFramework.Filters.Swagger, envuelve el mismo TMVCSwaggerMiddleware; OpenAPI está en MVCFramework.Filters.OpenAPI. El middleware JWT se busca cuando se genera el documento, una vez recibida la petición, y el orden de registro no cuenta.

Sesiones

Los dos Boolean de los middleware de sesión, HttpOnly y Secure, en los filtros se convierten en un set, TMVCSessionCookieFlags, que por defecto vale [scfHttpOnly]. Así que no basta con cambiar el nombre de la función:

UseFileSessionMiddleware(10, True, ‘sessions’, True) se convierte en FileSessionFilter(10, ‘sessions’, [scfHttpOnly, scfSecure]): los dos Boolean pasan a ser un set de flags

Copiar el código
// before
AEngine.AddMiddleware(UseFileSessionMiddleware(10, True, 'sessions', True));

// after
AEngine.UseHTTPFilter(FileSessionFilter(10, 'sessions', [scfHttpOnly, scfSecure]));

Si copias los parámetros uno a uno, el compilador protesta, porque donde había un Boolean ahora hay un set. Para construir el set a partir de un Boolean existe SessionCookieFlags: pone siempre HttpOnly, y Secure solo si el parámetro es verdadero. Es lo que usa el código generado por el asistente.

Para una session factory cualquiera existe SessionFilter(factory), y Session(factory) para los grupos de Minimal API: el filtro se convierte en su propietario. MemorySessionFilter, FileSessionFilter y DatabaseSessionFilter están construidos encima, y este último está en MVCFramework.Filters.ActiveRecord.

Boolean y enum

En las firmas de los filtros he evitado los Boolean cuando el True no se explica solo, la boolean trap. En UseFileSessionMiddleware(10, True, 'sessions', True) quien lee no sabe qué True es HttpOnly y cuál Secure, y tiene que abrir la declaración. Los filtros de sesión, en cambio, toman el set TMVCSessionCookieFlags, que se lee solo: [scfHttpOnly, scfSecure]. Lo mismo vale para Redirect, que distingue 302 y 301 con TMVCRedirectKind (rdkTemporary, rdkPermanent) en lugar de un Boolean. No los he convertido todos: siguen siendo Boolean, por ejemplo, las credenciales de CORS y APreserveQuery de Redirect.

Rate limit

RateLimit(10, 60) se queda como estaba. Para las opciones se prepara un TMVCRateLimitOptions, como hace el código generado por el asistente:

RateLimit con TMVCRateLimitOptions: 10 peticiones en 60 segundos, ruta /health excluida, callback OnExceeded que escribe en el log

Copiar el 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

La forma simple, StaticFiles('/static', lWwwPath), y la que lleva opciones, StaticFiles(TMVCStaticFilesOptions), son la misma implementación. La diferencia principal respecto a TMVCStaticFilesMiddleware es el fallback para las single page application: en el middleware estaba activo por defecto, en el filtro está apagado. En la forma simple se enciende con sfmSPA:

StaticFiles en / con sfmSPA, para una single page application con la API al lado

Copiar el código
AEngine.UseHTTPFilter(StaticFiles('/', 'www/app', sfmSPA));

Con sfmSPA el filtro deja pasar la petición al router, y responde con el index.html de la carpeta existente más cercana solo a un GET o HEAD para el que la aplicación no tiene ninguna ruta. Las rutas de la API no se ocultan nunca, así que la SPA puede estar en / con la API al lado. Con las opciones se establece SPAWebAppSupport := True, que en TMVCStaticFilesOptions.Default vale False. El tercer parámetro de la forma simple es el modo (sfmFiles o sfmSPA), el documento de índice, index.html por defecto, es el cuarto.

Dos límites. Hace falta AllowUnhandledAction = false, que es el valor por defecto. Y el router sigue escribiendo en el log “ROUTE NOT FOUND” por cada ruta del lado del cliente.

El resto es como en el middleware: el prefijo no distingue mayúsculas de minúsculas, con o sin / final; los archivos se sirven con su tipo MIME y con Last-Modified (y 304 con If-Modified-Since); una carpeta sin / final responde 301 y luego sirve su index.html. Quedan dos diferencias. El middleware escribía en el log una línea por cada archivo servido, con método, estado, ruta, IP y duración, y los filtros no. El filtro sirve con el tipo correcto .webp, .woff, .woff2 y .ttf, que el middleware no conocía. Las demás opciones del middleware, el charset, los tipos MIME personalizados y las reglas por petición, se obtienen con TMVCStaticFilesOptions.

Compression

El umbral de 1024 bytes, el nivel de compresión y la elección de la codificación (la primera entre gzip y deflate en el orden de la cabecera Accept-Encoding) son los del middleware. Cambia una cosa: el filtro no comprime una respuesta que ya tiene un Content-Encoding. RangeMedia establece uno identity en las respuestas parciales, y comprimirlas rompería Content-Range.

Los demás

Trace se comporta como el middleware. Cada línea de log lleva el id de correlación [rid:...], y el mismo id vuelve en la cabecera de respuesta X-Request-ID: es el del cliente que llama, si lo hay; si no, genera uno de seis dígitos hexadecimales. Authorization aparece con el esquema y tres asteriscos, Bearer ***, y las cabeceras de respuesta Authorization, Proxy-Authorization, Cookie y Set-Cookie se ocultan. El cuerpo se registra hasta 1024 bytes. La firma tiene un parámetro más, el último: Trace(AMaxBodySize, ALogTag, ALogAuthorization, AHideNonTextBody, ACorrelationHeaderName), y el nombre de la cabecera es X-Request-ID por defecto. Con ALogAuthorization = True el Authorization de la petición se escribe en claro.

SecurityHeaders escribe las mismas dos cabeceras que el middleware, X-XSS-Protection: 0 y X-Content-Type-Options: nosniff, y no tiene parámetros. Shutdown responde solo a los POST en /api/shutdown, como antes. RangeMedia y OpenAPI tienen los mismos valores que el middleware, y OpenAPI sirve el documento en /openapi.json. ActiveRecordFilter lee FDConnectionDefs.ini como el middleware, pero está escrito de forma nativa y libera la conexión en un finally: con el middleware, si la acción lanzaba una excepción, OnAfterRouting no se ejecutaba y la conexión no se liberaba.

He comparado los valores por defecto leyendo el código fuente de middleware y filtro. Si encuentras una diferencia que no he enumerado, abre una issue.


¿Y JWT y los demás?

También los middleware de autenticación tienen ahora sus filtros y están deprecados: JWT (cabecera y cookie), blacklist, refresh token, autenticación Basic, personalizada y RoleBased, OIDC y CSRF. No los he puesto entre los diecisiete porque sus filtros están hechos de otra manera.

Es la segunda responsabilidad de la que hablaba al principio. La autorización de los controladores decide sobre el nombre del controlador y el de la acción, y los reciben OnRequest y OnAuthorization. Esos nombres se conocen después del enrutamiento, y un filtro HTTP envuelve la petición sin conocerlos. Por eso las partes que no necesitan una ruta, es decir login, logout, callback y blacklist, son filtros HTTP, mientras que el control de la petición es un filtro de endpoint (EndpointFilter), que se ejecuta después del enrutamiento y se registra con UseEndpointFilter. Cada filtro ejecuta el código de su middleware, así que controla y responde de la misma manera.

Como los diecisiete, los middleware de autenticación seguirán funcionando hasta la 4.0, excluida, con un aviso del compilador que nombra el filtro que hay que usar. En otro post analizaré este otro tipo de filtros.


Preguntas frecuentes

¿Por qué DelphiMVCFramework incorpora los filtros HTTP junto a los middleware? El middleware clásico tenía dos responsabilidades: influir en el enrutamiento y controlar cómo se ejecutan las acciones. El filtro HTTP separa la primera con el mecanismo del next, el mismo de Express, Koa, ASP.NET Core, Laravel, Starlette, FastAPI, Gin y axum: un procedimiento que recibe el contexto y ANext, y decide si y cuándo pasar la petición a lo que viene después.

¿Qué middleware de DelphiMVCFramework están deprecados? Diecisiete middleware, catorce clases y tres funciones de creación: TMVCRedirectMiddleware, TMVCStaticFilesMiddleware, TMVCAnalyticsMiddleware, TMVCTraceMiddleware, TMVCCompressionMiddleware, TMVCETagMiddleware, TMVCCORSMiddleware, TMVCSecurityHeadersMiddleware, TMVCShutdownMiddleware, TMVCRangeMediaMiddleware, TMVCSwaggerMiddleware, TMVCOpenAPI3Middleware, TMVCRateLimitMiddleware, TMVCActiveRecordMiddleware y las funciones UseMemorySessionMiddleware, UseFileSessionMiddleware, UseDatabaseSessionMiddleware. Cada uno tiene un filtro HTTP en MVCFramework.Filters (Redirect, StaticFiles, Analytics, Trace, Compression, ETag, CORS, SecurityHeaders, Shutdown, RangeMedia, Swagger, OpenAPI, RateLimit, ActiveRecordFilter, MemorySessionFilter, FileSessionFilter, DatabaseSessionFilter). ActiveRecordFilter y DatabaseSessionFilter están en MVCFramework.Filters.ActiveRecord, Swagger en MVCFramework.Filters.Swagger y OpenAPI en MVCFramework.Filters.OpenAPI: quien los usa añade la unit a la uses. También están deprecados los middleware de autenticación (JWT en cabecera y en cookie, blacklist, refresh token, Basic, autenticación personalizada, RoleBased, OIDC, CSRF), que tienen sus propios filtros, en parte HTTP y en parte de endpoint.

¿Mi servidor existente deja de funcionar? No, no en la línea 3.5. No se ha eliminado nada. El compilador muestra el aviso W1000 por cada middleware deprecado que uses, y el código compila y funciona como antes.

¿Qué pasa con los middleware en la 4.0? Se eliminarán por completo. Hasta la 4.0, excluida, siguen funcionando, con un aviso del compilador. Todos los middleware deprecados ya tienen su filtro y se sustituyen hoy: los diecisiete de este artículo con un filtro HTTP, los de autenticación con filtros en parte HTTP y en parte de endpoint (EndpointFilter).

¿Puedo usar sesiones, ActiveRecord y rate limit como filtros en los controladores? Sí. MemorySessionFilter, FileSessionFilter, DatabaseSessionFilter, ActiveRecordFilter y RateLimit con TMVCRateLimitOptions son filtros HTTP, así que valen para los controladores. DatabaseSessionFilter se registra después de ActiveRecordFilter. El asistente los genera en lugar de los middleware clásicos.

¿Y los middleware de JWT, Basic, OIDC y CSRF? También tienen sus filtros y están deprecados. Las partes que no necesitan una ruta, es decir login, logout, callback y blacklist, son filtros HTTP. El control de la petición es un filtro de endpoint (EndpointFilter), que se ejecuta después del enrutamiento: la autorización de los controladores se basa en el nombre del controlador y de la acción, que antes del enrutamiento no se conocen. Los middleware siguen funcionando hasta la 4.0, excluida.

¿En qué orden registro filtros y middleware? Respecto a AddMiddleware el orden no cuenta: los filtros envuelven de todos modos los middleware clásicos. Entre los filtros, el primero registrado es el más externo, ETag va antes que Compression, y ActiveRecordFilter y los filtros de sesión están en el punto más interno.

¿Qué cambia en los valores por defecto de CORS al pasar al filtro CORS? El filtro arranca con las credenciales apagadas, las cabeceras Content-Type,Authorization, los métodos GET,POST,PUT,DELETE,PATCH,OPTIONS y un max-age de 1728000 segundos. El middleware clásico arrancaba con las credenciales activas, una lista de cabeceras más larga y los mismos métodos en otro orden. Quien depende de los valores antiguos los pasa como parámetros a CORS.

¿Qué cambia, además de CORS, en los valores por defecto de los filtros respecto a los middleware? El fallback para las single page application de StaticFiles está apagado por defecto, mientras que en el middleware estaba activo: se enciende con StaticFiles('/', folder, sfmSPA) o con SPAWebAppSupport en las opciones de TMVCStaticFilesOptions, y decide el router, así que las rutas de la API no se ocultan y la SPA puede estar también en /. El filtro no escribe la línea de log por cada archivo servido que escribía el middleware, y Compression no comprime una respuesta que ya tiene un Content-Encoding. Trace, SecurityHeaders, Shutdown, OpenAPI, RangeMedia y ActiveRecordFilter tienen los mismos valores por defecto que los middleware.


Recursos

– Daniele Teti

Datos clave
  • Tema: la diferencia entre los middleware clásicos (IMVCMiddleware, AddMiddleware) y los filtros HTTP (TMVCHTTPFilter, UseHTTPFilter) en DelphiMVCFramework, el framework REST y web para Delphi, y cómo se migra. Línea 3.5.0-silicon; estará en la RC9 (3.5.0-silicon-rc9). El trabajo descrito todavía no está en una release. El artículo trata de los filtros que se ejecutan antes del enrutamiento.
  • No es un breaking change: en la 3.5.0-silicon los filtros son una funcionalidad adicional y los servidores existentes siguen funcionando igual, con un aviso del compilador (W1000) por cada middleware deprecado. El breaking change está previsto en la 4.0, donde los middleware se eliminarán por completo. La transición es gradual porque DelphiMVCFramework se usa en entornos críticos (hospitales, bancos, fábricas, videojuegos, ERP).
  • Por qué: el middleware clásico tenía dos responsabilidades, influir en el enrutamiento (OnBeforeRouting, OnAfterRouting) y controlar cómo se ejecutan las acciones (OnBeforeControllerAction, OnAfterControllerAction). El filtro HTTP separa la primera con el mecanismo del next, que usan también Express, Koa, ASP.NET Core, Laravel, Starlette, FastAPI, Gin y axum: un procedimiento (AContext, ANext) que se ejecuta antes del enrutamiento y envuelve todo el dispatch; vale para controladores y Minimal API. OnBeforeRouting corresponde al código antes de ANext(); OnAfterRouting, que se dispara al final de todo, al código después de ANext().
  • Los filtros HTTP ocupan el lugar de los middleware donde existe uno equivalente. Diecisiete middleware están marcados como deprecated (aviso W1000, no error), catorce clases y tres funciones: 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 (con TMVCRateLimitOptions), ActiveRecordFilter, MemorySessionFilter, FileSessionFilter, DatabaseSessionFilter (unit MVCFramework.Filters, salvo ActiveRecordFilter y DatabaseSessionFilter en MVCFramework.Filters.ActiveRecord, Swagger en MVCFramework.Filters.Swagger y OpenAPI en MVCFramework.Filters.OpenAPI, que hay que añadir a la uses; UseHTTPFilter es un método de TMVCEngine). No se ha eliminado nada. En consecuencia, el asistente de los proyectos nuevos genera los filtros en lugar de esos middleware.
  • También los middleware de autenticación tienen sus filtros y están deprecados: JWT (cabecera y cookie), blacklist, refresh token, Basic, autenticación personalizada, RoleBased, OIDC, CSRF. Login, logout, callback y blacklist son filtros HTTP; el control de la petición es un filtro de endpoint (EndpointFilter, registrado con UseEndpointFilter), que se ejecuta después del enrutamiento porque la autorización de los controladores decide sobre el nombre del controlador y de la acción. Todos los middleware deprecados siguen funcionando hasta la 4.0, excluida; en la 4.0 se eliminarán por completo.
  • Orden: el primer filtro registrado es el más externo y los filtros envuelven los middleware clásicos sea cual sea el orden respecto a AddMiddleware; ETag antes de Compression; ActiveRecordFilter y las sesiones en el punto más interno, así una petición rechazada (429) o servida como archivo estático no abre una conexión.
  • Diferencias para quien migra: CORS arranca con las credenciales apagadas, cabeceras Content-Type,Authorization, max-age 1728000 (el middleware: credenciales activas, otras cabeceras, max-age 60); ETag entre comillas con soporte de W/ (el middleware: digest sin comillas); Redirect compara la ruta sin distinguir mayúsculas de minúsculas; Analytics escribe el PathInfo en lugar de controlador y acción; los filtros de sesión toman un set TMVCSessionCookieFlags ([scfHttpOnly, scfSecure], por defecto [scfHttpOnly]) en lugar de los dos Boolean HttpOnly y Secure de los viejos Use*SessionMiddleware, con el helper SessionCookieFlags(ASecure) para construirlo a partir de un Boolean; Redirect distingue 302 y 301 con TMVCRedirectKind (rdkTemporary, por defecto, o rdkPermanent) en lugar de un Boolean: en las firmas de los filtros los Boolean que no se explican solos se han sustituido por tipos que se leen solos (la boolean trap). StaticFiles tiene el fallback para las single page application apagado por defecto (en el middleware está activo): se enciende con StaticFiles('/', folder, sfmSPA) o con SPAWebAppSupport := True en las opciones (TMVCStaticFilesOptions): el filtro deja decidir al router y responde index.html solo a un GET o HEAD para el que la aplicación no tiene ninguna ruta, así que puede estar también en / con la API al lado (hace falta AllowUnhandledAction = false, el valor por defecto, y el router escribe en el log ROUTE NOT FOUND por cada ruta del lado del cliente); el tercer parámetro de la forma simple es el modo (TMVCStaticFilesMode), el documento de índice es el cuarto; los filtros no escriben la línea de log por cada archivo servido que escribía el middleware, y StaticFiles sirve .webp, .woff, .woff2 y .ttf con el tipo correcto. Compression no vuelve a comprimir una respuesta que ya tiene un Content-Encoding. SecurityHeaders, Shutdown, OpenAPI, RangeMedia y Trace tienen los mismos valores por defecto que los middleware (Trace devuelve el id de correlación X-Request-ID, oculta Authorization y registra el cuerpo hasta 1024 bytes, con un parámetro más, ACorrelationHeaderName); ActiveRecordFilter libera la conexión aunque la acción lance una excepción y activa las conexiones adicionales para cada instancia. RateLimit con opciones: mismas opciones y misma respuesta que el middleware (429, Retry-After, cuerpo JSON).
  • Autor: Daniele Teti, creador de DelphiMVCFramework.

Comments