Course outline

FastAPI / Advanced: lifecycle and reliable service design / Lesson 15

Middleware, CORS, and observability

Add bounded cross-cutting behavior while preserving response, error, browser-origin, and security boundaries.

About 5 min readContent reviewed
On this page

Lesson goals

  • Explain the assumptions and boundaries of middleware, cors, and observability.
  • Interpret both complete examples and justify their observed outputs.
  • Diagnose the listed failures and apply the worked solutions while keeping identity, state, and lifecycle ownership explicit.

Middleware surrounds an application boundary

Middleware can observe or alter request and response processing across many operations. It is useful for approved correlation, selected headers, and other consistent policy, but its position changes which paths and errors it sees. Do not put every business rule into a global layer simply because it runs everywhere. The first example validates a small public request identifier and echoes a safe correlation value. That identifier is diagnostic data, not authentication. Avoid serializing the complete request or environment in middleware, since it can expose credentials and make a small operation consume memory proportional to an unbounded body.

python Complete bounded-correlation check
import re
from fastapi import FastAPI, Request
from fastapi.testclient import TestClient
app = FastAPI()
@app.middleware("http")
async def correlation(request: Request, call_next):
    candidate = request.headers.get("X-Request-ID", "")
    request.state.request_id = candidate if re.fullmatch(r"[a-z0-9-]{1,40}", candidate) else "lab-generated"
    response = await call_next(request)
    response.headers["X-Request-ID"] = request.state.request_id
    return response
@app.get("/health")
def health(): return {"ok": True}
with TestClient(app) as client:
    print(client.get("/health", headers={"X-Request-ID": "lab-a"}).headers["X-Request-ID"])
    print(client.get("/health", headers={"X-Request-ID": "x" * 80}).headers["X-Request-ID"])
Expected output
lab-a
lab-generated

An allowed short identifier is echoed and an oversized value becomes a fixed synthetic fallback. Production generation needs an appropriate policy; correlation never grants identity or permission.

Interpret response timing honestly

Time around call_next can describe part of request processing but may not cover complete body streaming, background work, or all external effects. Status counts also do not establish business correctness. Choose metric names, units, windows, and route templates deliberately. Unique user IDs, request IDs, or full URLs can create high cardinality and sensitive correlation in telemetry. Use bounded labels and access-controlled detail where needed. Exceptions, early responses, validation failures, and streamed bodies deserve explicit tests because a middleware that works on a simple 200 path can miss the most important diagnostic branch.

CORS is a browser response-access policy

Cross-origin resource sharing controls when browsers allow scripts from another origin to access responses. It is not authentication, server-side authorization, a firewall, or a guarantee that nonbrowser callers cannot send a request. The second example allows one synthetic origin and rejects a disallowed preflight, then shows that a direct application GET can still return 200 without the browser-allow header. Configure origins, methods, headers, and credential behavior explicitly according to the client contract. Do not assume a wildcard origin is a safe substitute for review, particularly when credentialed browser behavior is involved.

python Complete CORS boundary check
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.testclient import TestClient
app = FastAPI()
app.add_middleware(CORSMiddleware, allow_origins=["https://client.example.invalid"], allow_credentials=True, allow_methods=["GET"], allow_headers=["X-Lab"])
@app.get("/resource")
def resource(): return {"public": True}
with TestClient(app) as client:
    allowed = client.options("/resource", headers={"Origin": "https://client.example.invalid", "Access-Control-Request-Method": "GET"})
    denied = client.options("/resource", headers={"Origin": "https://other.example.invalid", "Access-Control-Request-Method": "GET"})
    direct = client.get("/resource", headers={"Origin": "https://other.example.invalid"})
    print(allowed.status_code, allowed.headers["access-control-allow-origin"])
    print(denied.status_code)
    print(direct.status_code, direct.headers.get("access-control-allow-origin"))
Expected output
200 https://client.example.invalid
400
200 None

Allowed preflight receives the matching origin; denied preflight is 400. The direct GET still returns 200 without an allow-origin header, showing that CORS is not server authorization.

Keep error and proxy behavior under review

Middleware ordering determines whether a custom header or CORS policy appears on an error response, so test the chosen stack rather than assuming every response is identical. Host and proxy trust are separate deployment questions. Never trust arbitrary forwarded identity or client IP headers without an approved proxy boundary. Body inspection and streaming require careful ASGI ownership and resource limits; logging code must not consume or buffer data in a way that breaks the handler. If context propagation requirements exceed a convenience middlewareโ€™s documented behavior, choose an appropriate lower-level design and verify it under the pinned dependencies.

Verify useful observability without external export

Use synthetic headers and in-process requests to test correlation validation, allowed and denied origins, missing headers, and error paths. Avoid a real logging backend, trace exporter, browser account, or network service in examples. Neither script contacts the named origin; it is only an HTTP header value. A production observability design also needs retention, access, redaction, failure, and overhead budgets. The goal is enough trustworthy evidence to investigate the actual user contract, not an impressive stream of every byte or a CORS setting presented as a replacement for application security.

Common errors and fixes

CORS is used as the authentication mechanism

Why it happens
Browser response access was confused with server authority.
How to fix it
Implement independent identity and resource authorization.
Prevent it next time
Test nonbrowser and denied-user behavior separately.

Request logging consumes unlimited sensitive data

Why it happens
Cross-cutting convenience ignored body and privacy budgets.
How to fix it
Use bounded allowlisted metadata and correct body ownership.
Prevent it next time
Test redaction, streaming, and oversized input.

Middleware timing is called total business completion

Why it happens
Streaming and background boundaries were omitted.
How to fix it
Name and measure the actual observed stage.
Prevent it next time
Correlate metrics with user and task outcomes.

Check your understanding

1. A disallowed browser origin sends GET and the server returns 200. Does that alone prove CORS is broken?

Show worked solution

No. The browser policy concerns exposure of the response to that origin, not a universal server-side request denial. Inspect the CORS headers and enforce application authentication and authorization independently. Preflight and direct request behavior are different checks.

2. Why should a request ID be length-limited and excluded from authentication decisions?

Show worked solution

It is client-controlled diagnostic context that can otherwise inflate logs or carry unwanted content. Validate or replace it and keep trusted identity separate. Its presence or format does not prove who sent the request or what they may access.

Key takeaways

  • Middleware can observe or alter request and response processing across many operations.
  • Cross-origin resource sharing controls when browsers allow scripts from another origin to access responses.
  • Use synthetic headers and in-process requests to test correlation validation, allowed and denied origins, missing headers, and error paths.

Official references