Logging request/response bodies without hanging (safe patterns)
Want to log request bodies for debugging API issues? Here’s how to do it without breaking your app.
The Problem: Why Body Logging Breaks
Reading request bodies in middleware seems straightforward—but it has hidden pitfalls that cause hangs, broken routes, and streaming failures.
Don’t Do This
from fastapi import FastAPI
app = FastAPI()
@app.middleware("http")
async def log_body(request, call_next):
# This will break your app
body = await request.body()
print(f"Request body: {body}")
response = await call_next(request)
return response
This pattern causes several problems:
Body consumed twice —
request.body()reads the stream. Your route handler tries to read it again and gets empty bytes or hangs waiting for data that already arrived.Memory exhaustion — Large file uploads or payloads get loaded entirely into memory before your route even runs.
Event loop blocking — Synchronous
print()blocks the event loop. With high traffic, your app becomes unresponsive.
Common Symptoms
If you’ve added body logging and see these issues, this is likely the cause:
Routes hang indefinitely
422 Unprocessable Entityon valid JSONEmpty
request.json()in route handlersStreaming uploads fail or timeout
Memory usage spikes under load
The Solution: Cache the Body
The key is reading the body once and making it available to both your middleware and route handler. Starlette provides receive caching for this.
Safe Body Logging Middleware
from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware
from fapilog.fastapi import FastAPIBuilder
app = FastAPI(
lifespan=FastAPIBuilder()
.with_preset("fastapi")
.build()
)
class BodyLoggingMiddleware(BaseHTTPMiddleware):
"""Safely log request bodies without consuming them."""
MAX_BODY_LOG = 10_000 # Truncate bodies larger than 10KB
async def dispatch(self, request: Request, call_next):
# Cache the body so it can be read multiple times
body = await request.body()
# Access the logger from app state
logger = request.app.state.fapilog_logger
# Log truncated body
body_preview = body[: self.MAX_BODY_LOG]
if len(body) > self.MAX_BODY_LOG:
body_preview = body_preview + b"...[truncated]"
await logger.debug(
"request_body",
path=request.url.path,
method=request.method,
body=body_preview.decode("utf-8", errors="replace"),
body_size=len(body),
truncated=len(body) > self.MAX_BODY_LOG,
)
response = await call_next(request)
return response
# Add after FastAPIBuilder configures the app
app.add_middleware(BodyLoggingMiddleware)
This works because:
BaseHTTPMiddlewareautomatically caches the body when you callrequest.body()Subsequent calls (including in your route) return the cached bytes
We truncate before logging to avoid memory issues
fapilog’s async logger doesn’t block the event loop
Response Body Logging
Logging response bodies is trickier—you need to intercept the streaming response. Here’s a safe pattern:
from starlette.responses import Response
from starlette.types import Message
class ResponseBodyMiddleware(BaseHTTPMiddleware):
"""Log response bodies with size limits."""
MAX_RESPONSE_LOG = 10_000
async def dispatch(self, request: Request, call_next):
response = await call_next(request)
# Only log for JSON responses under size limit
content_type = response.headers.get("content-type", "")
if "application/json" not in content_type:
return response
# Collect response body chunks
body_chunks = []
total_size = 0
async def receive_body(message: Message):
nonlocal total_size
if message["type"] == "http.response.body":
chunk = message.get("body", b"")
if total_size < self.MAX_RESPONSE_LOG:
body_chunks.append(chunk)
total_size += len(chunk)
# This requires a custom response wrapper—see full example below
# For simplicity, log from route handlers instead
return response
For most use cases, logging response bodies from route handlers is simpler and safer than middleware interception.
Truncation for Large Bodies
Always limit how much body data you log. The examples above use a fixed byte limit, but fapilog’s size_guard processor provides automatic truncation for all log payloads:
from fapilog import LoggerBuilder
logger = await (
LoggerBuilder()
.with_size_guard(max_bytes="100 KB", action="truncate")
.build_async()
)
With size_guard enabled:
Payloads exceeding
max_bytesare automatically truncatedA
_truncated: truefield marks truncated entriesCritical fields like
messageare preserved
What Truncated Output Looks Like
{
"message": "request_body",
"body": "{ \"user\": \"alice\", \"data\": \"...[truncated]",
"body_size": 150000,
"_truncated": true,
"path": "/api/upload"
}
Adjusting Truncation Limits
# More aggressive truncation for high-volume endpoints
logger = await (
LoggerBuilder()
.with_size_guard(
max_bytes="10 KB",
action="truncate",
preserve_fields=["correlation_id", "path", "method"],
)
.build_async()
)
Bodies Are Redacted Too
Request and response bodies pass through the same redaction pipeline as all log fields. Sensitive data in JSON bodies is automatically masked.
Redacted Body Example
With field-based redaction enabled:
from fapilog import LoggerBuilder
logger = await (
LoggerBuilder()
.with_field_mask(fields=["password", "credit_card", "ssn"])
.build_async()
)
# Request body: {"username": "alice", "password": "hunter2"}
await logger.info(
"login_request",
body={"username": "alice", "password": "hunter2"},
)
# Log output: password is masked
# {"message": "login_request", "body": {"username": "alice", "password": "***"}}
JSON Body Redaction
When logging parsed JSON bodies, pass them as dictionaries rather than strings to enable deep redaction:
# Good: Dict enables field-level redaction
body_dict = await request.json()
await logger.debug("request_body", body=body_dict)
# Less effective: String only gets pattern matching
body_str = (await request.body()).decode()
await logger.debug("request_body", body=body_str)
Summary
Problem |
Solution |
|---|---|
Body consumed twice |
Use |
Memory exhaustion |
Truncate before logging |
Event loop blocking |
Use fapilog’s async logger |
Sensitive data in bodies |
Enable redaction, pass dicts not strings |
Going Deeper
Redacting Secrets and PII - Complete redaction configuration
Non-blocking Async Logging - Backpressure and queue management
Why Fapilog? - How fapilog compares to other logging libraries