Custom Middleware

AgentOS with custom middleware for rate limiting and request logging

Add custom middleware to your AgentOS application. The example below implements two common middleware types: rate limiting and request/response logging. Its counters live in one process and use request.client.host; they are not shared across workers and may identify a proxy rather than the end user.

Code

custom_middleware.py
import time
from collections import defaultdict, deque
from typing import Dict

from agno.agent import Agent
from agno.db.postgres import PostgresDb
from agno.models.openai import OpenAIResponses
from agno.os import AgentOS
from agno.tools.hackernews import HackerNewsTools
from fastapi import Request, Response
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware


# === Rate Limiting Middleware ===
class RateLimitMiddleware(BaseHTTPMiddleware):
    """
    Rate limiting middleware that limits requests per IP address.
    """

    def __init__(self, app, requests_per_minute: int = 60, window_size: int = 60):
        super().__init__(app)
        self.requests_per_minute = requests_per_minute
        self.window_size = window_size
        # Store request timestamps per IP
        self.request_history: Dict[str, deque] = defaultdict(lambda: deque())

    async def dispatch(self, request: Request, call_next) -> Response:
        # Get client IP
        client_ip = request.client.host if request.client else "unknown"
        current_time = time.time()

        # Clean old requests outside the window
        history = self.request_history[client_ip]
        while history and current_time - history[0] > self.window_size:
            history.popleft()

        # Check if rate limit exceeded
        if len(history) >= self.requests_per_minute:
            return JSONResponse(
                status_code=429,
                content={
                    "detail": f"Rate limit exceeded. Max {self.requests_per_minute} requests per minute."
                },
            )

        # Add current request to history
        history.append(current_time)

        # Add rate limit headers
        response = await call_next(request)
        response.headers["X-RateLimit-Limit"] = str(self.requests_per_minute)
        response.headers["X-RateLimit-Remaining"] = str(
            self.requests_per_minute - len(history)
        )
        response.headers["X-RateLimit-Reset"] = str(
            int(current_time + self.window_size)
        )

        return response


# === Request/Response Logging Middleware ===
class RequestLoggingMiddleware(BaseHTTPMiddleware):
    """
    Request/response logging middleware with timing and basic info.
    """

    def __init__(self, app, log_body: bool = False, log_headers: bool = False):
        super().__init__(app)
        self.log_body = log_body
        self.log_headers = log_headers
        self.request_count = 0

    async def dispatch(self, request: Request, call_next) -> Response:
        self.request_count += 1
        start_time = time.time()

        # Basic request info
        client_ip = request.client.host if request.client else "unknown"
        print(
            f"[REQ] Request #{self.request_count}: {request.method} {request.url.path} from {client_ip}"
        )

        # Optional: Log headers
        if self.log_headers:
            print(f"[HEADERS] Headers: {dict(request.headers)}")

        # Optional: Log request body
        if self.log_body and request.method in ["POST", "PUT", "PATCH"]:
            body = await request.body()
            if body:
                print(f"[BODY] Body: {body.decode()}")

        # Process request
        response = await call_next(request)

        # Log response info
        duration = time.time() - start_time
        status_label = "[OK]" if response.status_code < 400 else "[ERROR]"
        print(
            f"{status_label} Response: {response.status_code} in {duration * 1000:.1f}ms"
        )

        # Add request count to response header
        response.headers["X-Request-Count"] = str(self.request_count)

        return response


# === Setup database and agent ===
db = PostgresDb(db_url="postgresql+psycopg://ai:ai@localhost:5532/ai")

agent = Agent(
    id="demo-agent",
    name="Demo Agent",
    model=OpenAIResponses(id="gpt-5.2"),
    db=db,
    tools=[HackerNewsTools()],
    markdown=True,
)

agent_os = AgentOS(
    description="Essential middleware demo with rate limiting and logging",
    agents=[agent],
)

app = agent_os.get_app()

# Add custom middleware
app.add_middleware(
    RateLimitMiddleware,
    requests_per_minute=10,
    window_size=60,
)

app.add_middleware(
    RequestLoggingMiddleware,
    log_body=False,
    log_headers=False,
)

if __name__ == "__main__":
    """
    Run the essential middleware demo using AgentOS serve method.

    Features:
    1. Rate Limiting (10 requests/minute)
    2. Request/Response Logging
    """

    agent_os.serve(
        app="custom_middleware:app",
        reload=True,
    )

Usage

Set up your virtual environment

uv venv --python 3.12
source .venv/bin/activate

Set Environment Variables

export OPENAI_API_KEY=your_openai_api_key

Install dependencies

uv pip install -U agno openai "fastapi[standard]" uvicorn sqlalchemy pgvector "psycopg[binary]"

Setup PostgreSQL Database

# Using Docker
docker run -d \
  --name agno-postgres \
  -e POSTGRES_DB=ai \
  -e POSTGRES_USER=ai \
  -e POSTGRES_PASSWORD=ai \
  -p 5532:5432 \
  pgvector/pgvector:pg17

Run Example

python custom_middleware.py

Test the Middleware

Basic Request (observe console logging):

curl http://localhost:7777/config

Test Rate Limiting (trigger 429 errors after 10 requests):

for i in {1..15}; do curl http://localhost:7777/config; done

Check Rate Limit Headers: Wait more than 60 seconds after the loop so the next request is admitted.

curl -v http://localhost:7777/config

Middleware Features

Illustrates a process-local limit per peer IP:

  • Configurable Limits: Set requests per minute and time window
  • Per-IP Tracking: Each IP address gets its own request count
  • Sliding Window: Uses a sliding time window for accurate limiting
  • Rate Limit Headers: Provides client information about limits

Headers Added to Admitted Responses: Rejected requests return 429 without these headers.

  • X-RateLimit-Limit: Maximum requests allowed
  • X-RateLimit-Remaining: Requests remaining in current window
  • X-RateLimit-Reset: Conservative timestamp: this request time plus the window length. Earlier requests may expire sooner, so this is not the exact next admission time

Customization:

app.add_middleware(
    RateLimitMiddleware,
    requests_per_minute=100,  # Allow 100 requests per minute
    window_size=60,           # 60-second sliding window
)

Developer Resources