Middleware y el Pipeline HTTP en ASP.NET Core: Cómo fluye realmente una petición

Cuando una petición HTTP llega a tu aplicación ASP.NET Core, no va directo al controlador ni salta mágicamente a tu endpoint de Minimal API.

Antes tiene que atravesar algo que casi todos usamos a diario, pero que mucha gente no termina de entender del todo: el pipeline de middleware.

Cada pieza de middleware que está en esa cadena tiene la oportunidad de:

  • inspeccionar la petición
  • modificar el HttpContext
  • decidir cortar el flujo ahí mismo
  • o simplemente dejar que la petición siga su camino hacia el siguiente componente

Entender bien cómo funciona este pipeline te va a salvar de muchos dolores de cabeza. Es clave para:

  • implementar autenticación y autorización de forma correcta
  • registrar logs útiles (y sin duplicados raros)
  • capturar y manejar errores de manera global
  • medir rendimiento real
  • mantener el código limpio y predecible

Vamos a ver paso a paso cómo funciona realmente.

¿Qué es (y cómo se ve) el pipeline HTTP?

En ASP.NET Core el pipeline no es más que una cadena de middlewares por la que pasa la petición.

Cada middleware recibe dos cosas importantes:

  • El HttpContext (con toda la info de la request y response)
  • Una referencia al siguiente middleware (normalmente llamada next)

El flujo se ve más o menos así:

Request
   ↓
Middleware 1
   ↓
Middleware 2
   ↓
Middleware 3
   ↓
Endpoint (Controller / Minimal API)
   ↑
Middleware 3
   ↑
Middleware 2
   ↑
Middleware 1
   ↑
Response

Fíjate en algo súper importante: la respuesta vuelve en sentido inverso.

Eso significa que un middleware puede ejecutar código antes de llamar al siguiente… y también después de que todos los siguientes hayan terminado.

Ese doble momento (before + after) es lo que hace tan poderoso al sistema de middlewares.

Un middleware sencillo para empezar

Mira este ejemplo clásico de logging:

public class RequestLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestLoggingMiddleware> _logger;

    public RequestLoggingMiddleware(
        RequestDelegate next,
        ILogger<RequestLoggingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        _logger.LogInformation(
            "Iniciando request {Method} {Path}", 
            context.Request.Method, 
            context.Request.Path);

        await _next(context);

        _logger.LogInformation(
            "Finalizando request {Method} {Path} → {StatusCode}", 
            context.Request.Method, 
            context.Request.Path, 
            context.Response.StatusCode);
    }
}

¿Qué pasa aquí?

  1. Recibe la petición –> escribe log “entrando”
  2. Llama a await _next(context) –> le da el control al siguiente
  3. Cuando todo lo demás termina (incluso el endpoint), escribe el log “saliendo”

Ese patrón antes –> next –> después es literalmente la base de casi todo lo que ves en el pipeline.

Cómo (y dónde) se registran los middlewares

Todo se configura en Program.cs (o en el viejo Startup.cs si estás en versiones más antiguas).

La forma directa:

app.UseMiddleware<RequestLoggingMiddleware>();

Pero lo normal es usar los métodos de extensión que ya trae el framework:

app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseStaticFiles();           // si sirve archivos estáticos
app.MapControllers();           // o MapRazorPages, MapMinimalApis, etc.

El orden en el que los escribes es crítico.

El orden del pipeline: donde la mayoría se equivoca

Un error muy frecuente es subestimar el orden.

Ejemplo clásico de configuración incorrecta:

app.UseAuthorization();     // ¡mal!
app.UseAuthentication();

¿Por qué está mal? Porque UseAuthorization necesita saber quién es el usuario (es decir, ya debe haber pasado por autenticación).

El orden correcto más común sería algo así:

app.UseExceptionHandler("/error");     // lo más temprano posible (captura excepciones abajo)
app.UseHsts();                         // seguridad
app.UseHttpsRedirection();             // redirección https

app.UseStaticFiles();                  // archivos estáticos primero (rápido y barato)

app.UseRouting();

app.UseCors();                         // si usas CORS
app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();                  // o MapRazorPages, endpoints de Minimal API, etc.

Si pones UseAuthentication después de UseAuthorization, mucha gente se encontrará con 401/403 fantasma que cuestan horas de debug.

Las tres formas principales de agregar middleware

1. Use –> el más usado: Permite ejecutar código antes y después.

app.Use(async (context, next) =>
{
    // código antes
    await next();
    // código después
});

2. Run –> termina el pipeline

No llama a nadie más. Se usa para respuestas terminales (por ejemplo, un 404 custom o un “Hello World” de prueba).

app.Run(async context =>
{
    await context.Response.WriteAsync("¡Hola desde el final del pipeline!");
});

3. Map (y MapWhen) → sub-pipelines condicionales

Útil para aplicar middlewares solo a ciertas rutas.

app.Map("/api", api =>
{
    api.UseMiddleware<ApiSpecificLoggingMiddleware>();
    api.UseMiddleware<RateLimitingMiddleware>();
});

Dependency Injection dentro de los middlewares

Una de las cosas más bonitas de ASP.NET Core es que puedes inyectar servicios directamente en el constructor del middleware:

public class RequestLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestLoggingMiddleware> _logger;
    private readonly ICurrentUserService _userService;

    public RequestLoggingMiddleware(
        RequestDelegate next,
        ILogger<RequestLoggingMiddleware> logger,
        ICurrentUserService userService)
    {
        _next = next;
        _logger = logger;
        _userService = userService;
    }

    // ...
}

El contenedor de DI resuelve todo automáticamente. Eso te permite meter logging, métricas, servicios de negocio, etc., sin tener que hacer malabares.

Errores típicos que veo una y otra vez

  • Olvidarte de llamar a await next() –> la petición se queda colgada ahí
  • Poner consultas pesadas o lecturas de archivos grandes dentro de un middleware –> se ejecuta en cada request
  • Resolver servicios manualmente con context.RequestServices.GetService<…>() –> mejor siempre constructor injection
  • Armar mal el orden (sobre todo con routing/auth/authorization)

¿Cuándo vale la pena crear un middleware personalizado?

No todo tiene que ser middleware, pero brillan cuando quieres resolver cross-cutting concerns que afectan muchas (o todas) las peticiones:

  • Logging estructurado de requests/responses
  • Manejo global de excepciones
  • Métricas y Prometheus/OpenTelemetry
  • Auditoría de acciones sensibles
  • Rate limiting / throttling
  • Agregar headers de seguridad (CSP, Permissions-Policy, etc.)
  • Correlation IDs para tracing distribuido

Si la lógica debe ejecutarse “siempre” o “casi siempre”, es un gran candidato a middleware.

Para terminar: ¿por qué deberías prestarle atención al pipeline?

El pipeline de middleware es el mecanismo central con el que ASP.NET Core procesa peticiones HTTP.

Cuando lo entiendes bien:

  • vitas comportamientos raros que cuestan días de debug
  • Escribes código más limpio y predecible
  • Aprovechas al máximo la inyección de dependencias y la extensibilidad del framework
  • Puedes crear soluciones reutilizables que luego usas en varios proyectos

Tómate un rato para revisar el orden de tu Program.cs actual. Muchas veces con solo reordenar 3–4 líneas se soluciona un problema que parecía imposible.

Y si alguna vez te preguntan “¿por qué mi autenticación no funciona?” o “¿dónde rayos se está colgando esta petición?”, ya sabes por dónde empezar: el pipeline.

Sigamos codificando.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.