Custom Exemptions
While decorators provide a simple way to exempt specific routes from maintenance mode, more complex scenarios often require custom exemption logic. The exempt_handler
parameter of the middleware allows you to implement custom rules that can consider any aspect of the incoming request.
Docs Bypass Maintenance
By default, FastAPI's built-in documentation endpoints (/docs
, /redoc
, /openapi.json
, /docs/oauth2-redirect
) are automatically exempted from maintenance mode to keep API documentation accessible. This built-in behavior works alongside any custom exemption handler you define.
HTTP Error Behavior
The middleware automatically exempts requests that would result in HTTP errors (e.g.,404 Not Found
, 405 Method Not Allowed
, etc.) from maintenance mode. This ensures that when clients make requests to non-existent paths or use wrong HTTP methods, they receive the proper error response instead of a maintenance response.
Basic Usage
Here's a simple example that exempts health check endpoints and requests with an admin key from maintenance mode:
from fastapi import FastAPI, Request
from fastapi_maintenance import MaintenanceModeMiddleware
def is_exempt(request: Request) -> bool:
# Exempt all paths starting with "/health"
if request.url.path.startswith("/health"):
return True
# Exempt requests with special header
if request.headers.get("X-Admin-Key") == "supersecret":
return True
return False
app = FastAPI()
app.add_middleware(
MaintenanceModeMiddleware,
exempt_handler=is_exempt
)
# Result during maintenance:
# ✅ /docs, /redoc, /openapi.json, /docs/oauth2-redirect (automatically exempt)
# ✅ /health/* (custom exemption)
# ✅ Requests with X-Admin-Key header (custom exemption)
# ❌ Other endpoints return 503
Handler Function Requirements
The exempt handler is a function that decides whether a request should bypass maintenance mode. Here's what you need to know:
Function Signature
The handler function must:
- Accept a single Request
parameter from FastAPI
- Return a bool
value
- Be either synchronous or asynchronous (both are supported)
# Synchronous handler
def is_exempt(request: Request) -> bool:
# Logic here
return True/False
# Asynchronous handler
async def is_exempt(request: Request) -> bool:
# Async logic here
return True/False
Return Value
The return value determines how the request is handled:
- True
: The request bypasses maintenance mode and proceeds normally
- False
: The request is subject to maintenance mode rules
Execution Context
The handler runs for every request when maintenance middleware is in place, so:
- Keep it lightweight to avoid performance issues
- Handle all exceptions internally
- Avoid side effects that could impact other requests
- The handler can access any aspect of the Request
object (headers, path, query params, etc.)
Examples
Role-Based Exemptions
def is_exempt(request: Request) -> bool:
# Check for admin role in JWT token
token = request.headers.get("Authorization", "").replace("Bearer ", "")
try:
payload = decode_jwt(token)
if "admin" in payload.get("roles", []):
return True
except Exception:
# Token validation failed, no exemption
pass
return False
IP-Based Exemptions
def is_exempt(request: Request) -> bool:
# Exempt internal networks
client_host = request.client.host if request.client else None
# Exempt localhost and internal network
if client_host in ["127.0.0.1", "::1"] or client_host.startswith("10."):
return True
return False
Combining Multiple Conditions
def is_exempt(request: Request) -> bool:
# Health check endpoints are always accessible
if request.url.path.startswith("/health"):
return True
# Read-only operations during partial maintenance
if request.method in ["GET", "HEAD"] and not is_full_maintenance():
return True
# Admin users can access everything
if is_admin_user(request):
return True
return False
Async Support
The exempt handler can also be asynchronous:
async def is_exempt(request: Request) -> bool:
# Perform async operations like database queries
user = await get_user_from_db(request.headers.get("X-User-ID"))
if user and user.is_admin:
return True
return False
app.add_middleware(
MaintenanceModeMiddleware,
exempt_handler=is_exempt
)