Skip to main content

Middleware & CORS

Every request to your API passes through the same door. Every response leaves through the same door. Middleware sits at that door—inspecting, modifying, logging, timing.

For agent APIs, middleware solves two critical problems:

  1. CORS — Frontends on different domains need permission to call your API
  2. Observability — You need to know how long requests take and what's happening

What Is Middleware?

Middleware intercepts every request before it reaches your endpoints and every response before it returns to the client:

Client Request

┌─────────────────┐
│ Middleware │ ← Log request, start timer
└────────┬────────┘

┌─────────────────┐
│ Your Route │ ← /tasks, /users/me, etc.
└────────┬────────┘

┌─────────────────┐
│ Middleware │ ← Add headers, log response time
└────────┬────────┘

Client Response

This is powerful because you write the logic once and it applies to ALL endpoints.

Creating Custom Middleware

Use the @app.middleware("http") decorator:

import time
from fastapi import FastAPI, Request

app = FastAPI()


@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
"""Add processing time to every response."""
start_time = time.perf_counter()

response = await call_next(request) # Pass to route

process_time = time.perf_counter() - start_time
response.headers["X-Process-Time"] = str(process_time)
return response

Key components:

ComponentPurpose
request: RequestThe incoming HTTP request
call_nextFunction that passes request to the route (or next middleware)
await call_next(request)Execute the route and get the response
response.headers[...]Modify the response before returning

Test it:

curl -i http://localhost:8000/tasks

Output:

HTTP/1.1 200 OK
x-process-time: 0.0023456
content-type: application/json
...

Every response now includes timing information.

Request Logging Middleware

Log every request for debugging and monitoring:

import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


@app.middleware("http")
async def log_requests(request: Request, call_next):
"""Log all incoming requests."""
logger.info(f"{request.method} {request.url.path}")

response = await call_next(request)

logger.info(f"{request.method} {request.url.path}{response.status_code}")
return response

Output (server logs):

INFO: GET /tasks
INFO: GET /tasks → 200
INFO: POST /tasks
INFO: POST /tasks → 201
INFO: GET /tasks/999
INFO: GET /tasks/999 → 404

Now you can see every request flowing through your API.

Middleware Execution Order

When you have multiple middlewares, order matters. They form a stack:

@app.middleware("http")
async def middleware_a(request: Request, call_next):
print("A: before")
response = await call_next(request)
print("A: after")
return response


@app.middleware("http")
async def middleware_b(request: Request, call_next):
print("B: before")
response = await call_next(request)
print("B: after")
return response

Output:

B: before
A: before
[route executes]
A: after
B: after

The last added middleware is the outermost (first to receive request, last to process response).

CORS: Cross-Origin Resource Sharing

When your frontend (e.g., http://localhost:3000) calls your API (e.g., http://localhost:8000), the browser blocks it by default. Different ports = different origins = blocked.

CORS tells browsers: "Yes, this frontend is allowed to call me."

What Is an Origin?

An origin is: protocol + domain + port

URLOrigin
http://localhost:3000http://localhost:3000
http://localhost:8000http://localhost:8000
https://myapp.comhttps://myapp.com
https://api.myapp.comhttps://api.myapp.com

These are ALL different origins. Cross-origin requests are blocked unless you explicitly allow them.

Configuring CORSMiddleware

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

origins = [
"http://localhost:3000", # React dev server
"http://localhost:5173", # Vite dev server
"https://myapp.com", # Production frontend
]

app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

Now frontends on those origins can call your API.

CORS Parameters

ParameterPurposeCommon Value
allow_originsWhich origins can call the APIList of URLs
allow_methodsWhich HTTP methods are allowed["*"] for all
allow_headersWhich request headers are allowed["*"] for all
allow_credentialsAllow cookies/auth headersTrue
expose_headersWhich response headers browser can access["X-Process-Time"]
max_ageHow long to cache CORS response600 (10 minutes)

Development vs Production

Development (allow everything):

app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Any origin
allow_methods=["*"],
allow_headers=["*"],
)

Production (explicit origins):

from config import get_settings

settings = get_settings()

app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins, # From environment
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
)

Add to .env:

CORS_ORIGINS=["https://myapp.com","https://admin.myapp.com"]

Important CORS Rule

If allow_credentials=True, you cannot use ["*"] for origins. You must list specific origins. This prevents credential leakage to malicious sites.

Complete Middleware Setup

Here's a production-ready middleware configuration:

import time
import logging
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from config import get_settings

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

settings = get_settings()
app = FastAPI(title="Task API")

# CORS - must be added first (outermost)
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=["X-Process-Time"],
)


@app.middleware("http")
async def add_process_time(request: Request, call_next):
"""Add processing time header."""
start = time.perf_counter()
response = await call_next(request)
response.headers["X-Process-Time"] = f"{time.perf_counter() - start:.4f}"
return response


@app.middleware("http")
async def log_requests(request: Request, call_next):
"""Log all requests."""
logger.info(f"→ {request.method} {request.url.path}")
response = await call_next(request)
logger.info(f"← {request.method} {request.url.path} [{response.status_code}]")
return response

Execution order for a request:

  1. log_requests (logs incoming)
  2. add_process_time (starts timer)
  3. CORSMiddleware (adds CORS headers)
  4. Route executes
  5. CORSMiddleware (response)
  6. add_process_time (adds header)
  7. log_requests (logs outgoing)

Hands-On Exercise

Step 1: Add timing middleware to your Task API

Step 2: Add request logging middleware

Step 3: Configure CORS for http://localhost:3000

Step 4: Test with curl:

# Check timing header
curl -i http://localhost:8000/tasks

# Simulate CORS preflight
curl -X OPTIONS http://localhost:8000/tasks \
-H "Origin: http://localhost:3000" \
-H "Access-Control-Request-Method: POST" \
-i

Step 5: Verify CORS headers in response:

Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: *
Access-Control-Allow-Headers: *

Common Mistakes

Mistake 1: Forgetting to await call_next

# Wrong - blocks forever
response = call_next(request)

# Correct
response = await call_next(request)

Mistake 2: Using credentials with wildcard origin

# Wrong - browser rejects this
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True, # Can't use with wildcard!
)

# Correct - explicit origins
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_credentials=True,
)

Mistake 3: Not returning the response

# Wrong - returns None
@app.middleware("http")
async def bad_middleware(request: Request, call_next):
response = await call_next(request)
# Forgot to return!

# Correct
@app.middleware("http")
async def good_middleware(request: Request, call_next):
response = await call_next(request)
return response

Why This Matters for Agents

When you expose agent endpoints:

  1. Frontends need CORS — A React app calling your /agent/chat endpoint
  2. Timing matters — Know if agent responses are slow
  3. Logging helps debug — See what's being sent to agents
  4. Consistent headers — All responses get the same treatment

Middleware ensures every request to your agent API is tracked, timed, and accessible.

Try With AI

Prompt 1: Error Handling Middleware

I want middleware that catches exceptions and returns
consistent JSON error responses instead of HTML error pages.
Show me how to wrap call_next in try/except and format errors.

What you're learning: Middleware can normalize error responses across all endpoints.

Prompt 2: Rate Limit Headers

I'm adding rate limiting to my API. How do I add
X-RateLimit-Remaining and X-RateLimit-Reset headers
via middleware so all endpoints show rate limit status?

What you're learning: Middleware can add information from external systems (like rate limiters) to responses.

Prompt 3: Request ID Tracing

I want to add a unique X-Request-ID header to every request
for distributed tracing. Show me how to generate a UUID
in middleware and add it to both the request (for logging)
and the response (for client correlation).

What you're learning: Request IDs enable tracing requests across microservices and logs.


Reflect on Your Skill

You built a fastapi-agent skill in Lesson 0. Test and improve it based on what you learned.

Test Your Skill

Using my fastapi-agent skill, help me add middleware for timing and CORS.
Does my skill include @app.middleware patterns and CORSMiddleware configuration?

Identify Gaps

Ask yourself:

  • Did my skill include custom middleware with call_next?
  • Did it configure CORS for frontend access?
  • Did it handle middleware execution order?

Improve Your Skill

If you found gaps:

My fastapi-agent skill is missing middleware patterns.
Update it to include timing middleware with @app.middleware,
CORSMiddleware configuration with explicit origins,
and request logging for observability.