Adding middleware¶
ShieldMiddleware is the enforcement layer. It is a standard ASGI middleware that intercepts every HTTP request, calls engine.check(), and returns the appropriate error response when a route is blocked. Without it, decorators register state but nothing enforces it.
The examples below use FastAPI, but ShieldMiddleware works with any Starlette-compatible ASGI framework.
Basic setup¶
from fastapi import FastAPI
from shield.core.engine import ShieldEngine
from shield.fastapi.middleware import ShieldMiddleware
engine = ShieldEngine() # uses MemoryBackend by default
app = FastAPI()
app.add_middleware(ShieldMiddleware, engine=engine)
Important
Add ShieldMiddleware before including any routers. Middleware is applied in reverse registration order in ASGI frameworks built on Starlette, so adding it first ensures it wraps all routes.
What the middleware does¶
Incoming HTTP request
│
▼
ShieldMiddleware.dispatch()
│
├─ /docs, /redoc, /openapi.json ──────────────────────→ pass through
│
├─ Lazy-scan app routes for __shield_meta__ (once only)
│
├─ @force_active route? ──────────────────────────────→ pass through
│
├─ engine.check(path, method)
│ │
│ ├─ Global maintenance ON? → 503
│ ├─ MAINTENANCE → 503 + Retry-After header
│ ├─ DISABLED → 503
│ ├─ ENV_GATED → 403 JSON
│ ├─ DEPRECATED → pass through + inject response headers
│ ├─ ACTIVE → pass through ✓
│ │
│ └─ Rate limit check (if policy registered for route)
│ ├─ Exempt IP or role? → pass through ✓
│ ├─ Under limit? → pass through + X-RateLimit-* headers ✓
│ └─ Limit exceeded? → 429 + Retry-After header
│
└─ call_next(request)
Route registration¶
The middleware auto-registers routes on first startup by scanning for __shield_meta__ on route handlers. This works with any router type: plain APIRouter, ShieldRouter, or routes added directly to the app.
If a route already has persisted state in the backend (for example, written by a previous CLI command), the decorator default is ignored and the persisted state wins. This means runtime changes survive restarts.
Paths excluded from checks¶
The following paths always pass through regardless of shield state:
/docs,/redoc,/openapi.json: API documentation/shield/: admin dashboard prefix
You can exclude additional paths by using @force_active on those routes.
Global maintenance mode¶
The middleware also enforces global maintenance mode, a single switch that blocks every route at once:
# Block everything immediately
await engine.enable_global_maintenance(
reason="Emergency patch — back in 15 minutes",
exempt_paths=["/health", "GET:/admin/status"],
)
# All non-exempt routes now return 503
# Restore normal operation
await engine.disable_global_maintenance()
See Reference: ShieldEngine for the full global maintenance API.
Response format¶
All error responses from the middleware use a consistent JSON structure:
{
"error": {
"code": "MAINTENANCE_MODE",
"message": "This endpoint is temporarily unavailable",
"reason": "Database migration in progress",
"path": "/api/payments",
"retry_after": "2025-06-01T04:00:00Z"
}
}
| Scenario | Status | code |
|---|---|---|
| Maintenance mode | 503 | MAINTENANCE_MODE |
| Route disabled | 503 | ROUTE_DISABLED |
| Env-gated (wrong env) | 403 | ENV_GATED |
| Global maintenance | 503 | MAINTENANCE_MODE |
| Rate limit exceeded | 429 | RATE_LIMIT_EXCEEDED |
OpenAPI integration (FastAPI only)¶
FastAPI exposes a live OpenAPI schema at /openapi.json. Shield can filter it to hide disabled and env-gated routes and annotate maintained or deprecated ones:
from shield.fastapi.openapi import apply_shield_to_openapi
apply_shield_to_openapi(app, engine) # call after add_middleware
For enhanced docs UI with maintenance banners:
from shield.fastapi.openapi import apply_shield_to_openapi, setup_shield_docs
apply_shield_to_openapi(app, engine) # must come first
setup_shield_docs(app, engine) # inject banners into /docs and /redoc
See Reference: Middleware for all parameters.