Skip to main content

Middleware Documentation

Middleware in AtlasML intercepts every request and response, enabling logging, authentication, error handling, and other cross-cutting concerns. This guide explains how middleware works and how to use it effectively.


What is Middleware?​

Middleware is code that runs before and after every API request. It wraps your endpoint handlers, allowing you to:

  • Log requests and responses
  • Modify requests/responses
  • Add headers
  • Handle errors
  • Measure performance
  • Authenticate users
  • Rate limit requests

Middleware Flow​

Middleware executes twice:

  1. Before the endpoint handler (inbound)
  2. After the endpoint handler (outbound)

RequestLoggingMiddleware​

AtlasML's primary middleware logs all requests and responses for debugging and monitoring.

Location​

File: atlasml/app.py

Implementation​

import time
from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware

class RequestLoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
start_time = time.time()

# --- INBOUND: Before endpoint handler ---
logger.info(f"→ {request.method} {request.url.path}")

# Log POST request bodies
if request.method == "POST":
body = await request.body()
logger.info(f"📦 Request body: {body.decode()[:200]}")

# --- CALL ENDPOINT HANDLER ---
response = await call_next(request)

# --- OUTBOUND: After endpoint handler ---
duration = time.time() - start_time
logger.info(f"← {response.status_code} ({duration:.3f}s)")

return response

What It Logs​

1. Request Method and Path​

INFO → POST /api/v1/competency/suggest

2. Request Body (POST only, first 200 chars)​

INFO 📦 Request body: {"description":"Python programming","course_id":1}

3. Response Status and Duration​

INFO ← 200 (0.234s)

Example Log Output​

INFO → POST /api/v1/competency/suggest
INFO 📦 Request body: {"description":"Understanding of OOP","course_id":1}
INFO ← 200 (0.456s)

INFO → GET /api/v1/health/
INFO ← 200 (0.002s)

INFO → POST /api/v1/competency/save
INFO 📦 Request body: {"competencies":[{"id":100,"title":"ML Basics",...}],"operation_type":"UPDATE"}
INFO ← 200 (1.234s)

How Middleware Works​

Execution Order​

When multiple middleware are registered, they execute in reverse order on the way out:

Example:

app.add_middleware(CORSMiddleware)          # Executes 2nd (in), 2nd (out)
app.add_middleware(RequestLoggingMiddleware) # Executes 1st (in), 3rd (out)

Middleware Registration​

In atlasml/app.py:

# Create FastAPI app
app = FastAPI(title="AtlasML API", lifespan=lifespan)

# Add middleware
app.add_middleware(RequestLoggingMiddleware)
tip

Middleware registered last executes first on the inbound path.


Use Cases​

1. Request Logging (Current)​

Purpose: Debug API activity, monitor performance, audit trails.

What it logs:

  • All HTTP requests (method, path)
  • POST bodies for debugging
  • Response status codes
  • Processing time

When it's useful:

  • Debugging failed requests
  • Identifying slow endpoints
  • Understanding API usage patterns
  • Troubleshooting issues

2. Authentication (Via Dependencies)​

AtlasML uses dependency injection for authentication instead of middleware:

@router.post("/suggest", dependencies=[Depends(TokenValidator)])
async def suggest_competencies(request: SuggestCompetencyRequest):
# Only runs if authenticated

Why not middleware?

  • More flexible (per-endpoint control)
  • Better for optional auth
  • Clearer in code

3. CORS (Cross-Origin Resource Sharing)​

If AtlasML needs to be accessed from browsers:

from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
CORSMiddleware,
allow_origins=["https://frontend.example.com"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

4. Request ID Tracking​

Add unique IDs to trace requests through logs:

import uuid
from starlette.middleware.base import BaseHTTPMiddleware

class RequestIDMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
request_id = str(uuid.uuid4())
request.state.request_id = request_id

response = await call_next(request)
response.headers["X-Request-ID"] = request_id
return response

Modifying Requests​

Middleware can modify the request before it reaches the handler:

class AddCustomHeaderMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# Add custom header
request.headers.__dict__["_list"].append(
(b"x-custom-header", b"custom-value")
)

response = await call_next(request)
return response
warning

Modifying request headers can be tricky. Consider using request.state instead:

request.state.custom_data = "value"

Modifying Responses​

Middleware can modify the response after the handler:

class AddResponseHeaderMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
response = await call_next(request)

# Add custom response header
response.headers["X-Powered-By"] = "AtlasML"
response.headers["X-Version"] = "1.0.0"

return response

Error Handling in Middleware​

Middleware can catch and handle exceptions:

class ErrorHandlingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
try:
response = await call_next(request)
return response
except Exception as e:
logger.error(f"Unhandled exception: {e}")
return JSONResponse(
status_code=500,
content={"detail": "Internal server error"}
)
note

FastAPI has built-in exception handlers that are usually sufficient. Use middleware exception handling only for cross-cutting concerns.


Performance Monitoring​

Use middleware to track performance metrics:

import time
from prometheus_client import Histogram

request_duration = Histogram(
'http_request_duration_seconds',
'HTTP request duration',
['method', 'endpoint', 'status']
)

class MetricsMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
start_time = time.time()

response = await call_next(request)

duration = time.time() - start_time
request_duration.labels(
method=request.method,
endpoint=request.url.path,
status=response.status_code
).observe(duration)

return response

Creating Custom Middleware​

Step 1: Create Middleware Class​

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

class CustomMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# Code before endpoint handler
print(f"Before: {request.url.path}")

# Call the endpoint
response = await call_next(request)

# Code after endpoint handler
print(f"After: {response.status_code}")

return response

Step 2: Register Middleware​

In atlasml/app.py:

from atlasml.middleware import CustomMiddleware

app.add_middleware(CustomMiddleware)

Step 3: Test Middleware​

from fastapi.testclient import TestClient

def test_custom_middleware():
client = TestClient(app)
response = client.get("/api/v1/health/")
assert response.status_code == 200

Middleware Best Practices​

1. Keep Middleware Fast​

Middleware runs on every request. Avoid:

  • Heavy computations
  • Synchronous database calls
  • Blocking operations
# ❌ Bad - Slow
class SlowMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
time.sleep(1) # Blocks all requests!
return await call_next(request)

# ✅ Good - Fast
class FastMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
await some_async_operation() # Non-blocking
return await call_next(request)

2. Order Matters​

Place middleware in the correct order:

# Correct order
app.add_middleware(ErrorHandlingMiddleware) # Catches all errors
app.add_middleware(RequestLoggingMiddleware) # Logs requests/responses
app.add_middleware(AuthenticationMiddleware) # Checks auth

3. Use Dependencies for Per-Endpoint Logic​

For logic that only applies to specific endpoints, use dependencies instead of middleware:

# ❌ Bad - Middleware for specific logic
class CompetencyMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
if "/competency/" in request.url.path:
# Special logic
...
return await call_next(request)

# ✅ Good - Dependency
@router.post("/suggest", dependencies=[Depends(validate_course)])
async def suggest_competencies(...):
...

4. Don't Read Request Body Twice​

If middleware reads the body, it needs to be restored:

class BodyLoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# Read body
body = await request.body()
logger.info(f"Body: {body.decode()}")

# Restore body for endpoint to read
async def receive():
return {"type": "http.request", "body": body}

request._receive = receive

return await call_next(request)
tip

RequestLoggingMiddleware already handles this correctly!

5. Handle Exceptions Gracefully​

class SafeMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
try:
response = await call_next(request)
return response
except Exception as e:
logger.exception("Middleware error")
# Re-raise to let FastAPI handle it
raise

Debugging Middleware​

Enable Debug Logging​

Set log level to DEBUG:

import logging

logging.basicConfig(level=logging.DEBUG)

Add Debug Prints​

class DebugMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
print(f"[DEBUG] Request: {request.method} {request.url}")
print(f"[DEBUG] Headers: {request.headers}")

response = await call_next(request)

print(f"[DEBUG] Response: {response.status_code}")
print(f"[DEBUG] Response headers: {response.headers}")

return response

Check Middleware Order​

# In app.py, print registered middleware
print("Registered middleware:")
for middleware in app.user_middleware:
print(f" - {middleware}")

Disabling Middleware​

For Testing​

Remove middleware in test fixtures:

import pytest
from fastapi.testclient import TestClient
from atlasml.app import app

@pytest.fixture
def client_without_logging():
# Remove logging middleware for clean test output
app.user_middleware = [
m for m in app.user_middleware
if not isinstance(m.cls, RequestLoggingMiddleware)
]
return TestClient(app)

Conditional Registration​

Only add middleware in production:

from atlasml.config import get_settings

if get_settings().env == "production":
app.add_middleware(RequestLoggingMiddleware)

Common Middleware Patterns​

1. Request/Response Logging​

Current implementation - Logs all API activity

2. Authentication​

Use FastAPI dependencies instead - More flexible

3. CORS​

Add if needed for browser access

4. Request ID​

Useful for tracing requests through logs

5. Rate Limiting​

Add for production to prevent abuse

6. Compression​

Compress responses to reduce bandwidth

from fastapi.middleware.gzip import GZipMiddleware

app.add_middleware(GZipMiddleware, minimum_size=1000)

7. Trusted Hosts​

Validate Host header to prevent host header attacks

from fastapi.middleware.trustedhosts import TrustedHostMiddleware

app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=["atlasml.example.com", "*.example.com"]
)

Next Steps​


Resources​