Chuyển đến nội dung chính

Lesson 13: Middleware, Events & Lifespan

HTTP Middleware, ASGI Middleware, Starlette middleware stack. Lifespan events (startup/shutdown), Application state. Custom middleware for logging, timing, request ID tracking.

💻 Programming — Lesson 13 Lesson 13: Middleware, Events & Lifespan

Python FastAPI: From Basics to Advanced

Part 4: Advanced Features

xdev.asia

1. Middleware in FastAPI

Middleware is code that runs before each request and after each response. FastAPI supports two types of middleware: HTTP middleware (easy to write) and ASGI middleware (higher performance).

Request → Middleware 1 → Middleware 2 → Route Handler
                                              │
Response ← Middleware 1 ← Middleware 2 ←──────┘

2. HTTP Middleware (BaseHTTPMiddleware)

# app/middleware/logging.py
import time
import logging

from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response

logger = logging.getLogger(__name__)


class LoggingMiddleware(BaseHTTPMiddleware):
    """Log mọi request/response với timing."""

    async def dispatch(self, request: Request, call_next) -> Response:
        # Before request
        start_time = time.perf_counter()
        request_id = getattr(request.state, "request_id", "unknown")

        logger.info(
            "Request started",
            extra={
                "request_id": request_id,
                "method": request.method,
                "path": request.url.path,
                "client": request.client.host if request.client else "unknown",
            },
        )

        # Process request
        try:
            response = await call_next(request)
        except Exception as exc:
            logger.error(f"Request failed: {exc}", exc_info=True)
            raise

        # After request
        process_time = time.perf_counter() - start_time
        response.headers["X-Process-Time"] = f"{process_time:.4f}"

        logger.info(
            "Request completed",
            extra={
                "request_id": request_id,
                "status_code": response.status_code,
                "process_time": f"{process_time:.4f}s",
            },
        )

        return response

3. Pure ASGI Middleware (High Performance)

# app/middleware/timing.py
import time
from starlette.types import ASGIApp, Receive, Scope, Send


class TimingMiddleware:
    """Pure ASGI middleware - hiệu năng cao hơn BaseHTTPMiddleware."""

    def __init__(self, app: ASGIApp):
        self.app = app

    async def __call__(self, scope: Scope, receive: Receive, send: Send):
        if scope["type"] != "http":
            await self.app(scope, receive, send)
            return

        start_time = time.perf_counter()

        async def send_wrapper(message):
            if message["type"] == "http.response.start":
                process_time = time.perf_counter() - start_time
                headers = list(message.get("headers", []))
                headers.append(
                    (b"x-process-time", f"{process_time:.6f}".encode())
                )
                message["headers"] = headers
            await send(message)

        await self.app(scope, receive, send_wrapper)

4. Lifespan Events

# app/main.py
from contextlib import asynccontextmanager

import redis.asyncio as redis
from fastapi import FastAPI

from app.config import settings
from app.core.database import engine, Base


@asynccontextmanager
async def lifespan(app: FastAPI):
    """
    Application lifespan manager.
    Code trước 'yield' chạy khi startup.
    Code sau 'yield' chạy khi shutdown.
    """
    # ===== STARTUP =====
    print("🚀 Starting application...")

    # Initialize database
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)
    print("✅ Database initialized")

    # Initialize Redis
    app.state.redis = redis.from_url(
        settings.redis_url,
        encoding="utf-8",
        decode_responses=True,
    )
    print("✅ Redis connected")

    # Initialize other services
    # app.state.email_service = EmailService()
    # app.state.storage = S3Storage()

    print("✅ Application started successfully")

    yield  # ===== APPLICATION IS RUNNING =====

    # ===== SHUTDOWN =====
    print("🛑 Shutting down application...")

    # Close Redis
    await app.state.redis.close()
    print("✅ Redis disconnected")

    # Close database
    await engine.dispose()
    print("✅ Database connections closed")

    print("✅ Application shut down successfully")


app = FastAPI(
    title="FastAPI Tutorial",
    lifespan=lifespan,
)

5. Application State

# Truy cập app state trong routes
from fastapi import FastAPI, Request

app = FastAPI()

@app.get("/cache/{key}")
async def get_cache(key: str, request: Request):
    """Truy cập Redis thông qua app state."""
    redis = request.app.state.redis
    value = await redis.get(f"cache:{key}")
    return {"key": key, "value": value}


@app.post("/cache/{key}")
async def set_cache(key: str, value: str, request: Request):
    redis = request.app.state.redis
    await redis.setex(f"cache:{key}", 3600, value)
    return {"message": "Cached"}

6. Middleware Order

# app/main.py - Middleware được thêm theo thứ tự NGƯỢC
# Middleware cuối cùng được thêm sẽ chạy ĐẦU TIÊN

from starlette.middleware.cors import CORSMiddleware
from starlette.middleware.trustedhost import TrustedHostMiddleware

from app.middleware.logging import LoggingMiddleware
from app.middleware.security import SecurityHeadersMiddleware
from app.middleware.request_id import RequestIDMiddleware

# Thêm theo thứ tự ngược (last added = first executed)
app.add_middleware(LoggingMiddleware)           # 3. Logging
app.add_middleware(SecurityHeadersMiddleware)   # 2. Security headers
app.add_middleware(RequestIDMiddleware)         # 1. Request ID (chạy đầu tiên)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
)

# Execution order:
# Request  → RequestID → Security → Logging → Route
# Response ← RequestID ← Security ← Logging ← Route

Summary

In this article we learned:

  • HTTP Middleware: Easy to write with BaseHTTPMiddleware
  • ASGI Middleware: High performance for production
  • Lifespan: Startup/shutdown events manage resources
  • AppState: Share resources between requests
  • Middleware Order: Order of execution matters

The next article will implement WebSockets and Real-time Communication.