You could use a Middleware. A middleware takes each request that comes to your application, and hence, allows you to handle the request before it is processed by any specific endpoint, as well as the response, before it is returned to the client. To create a middleware, you use the decorator @app.middleware("http") on top of a function, as shown below. As you need to consume the request body from the stream inside the middleware—using either request.body() or request.stream(), as shown in this answer (behind the scenes, the former method actually calls the latter, see here)—then it won't be available when you later pass the request to the corresponding endpoint. Thus, you can follow the approach described in this post to make the request body available down the line (i.e., using the set_body function below). As for the response body, you can use the same approach as described in this answer to consume the body and then return the response to the client. Either option described in the aforementioned linked answer would work; the below, however, uses Option 2, which stores the body in a bytes object and returns a custom Response directly (along with the status_code, headers and media_type of the original response).
To log the data, you could use a BackgroundTask, as described in this answer and this answer. A BackgroundTask will run only once the response has been sent (see Starlette documentation as well); thus, the client won't have to be waiting for the logging to complete before receiving the response (and hence, the response time won't be noticeably impacted).
Note: If you had a streaming request or response with a body that wouldn't fit into your server's RAM (for example, imagine a body of 100GB on a machine running 8GB RAM), it would become problematic, as your are storing the data in the RAM, which wouldn't have enough space available to accommodate the accumulated data. You mentioned that "the body size of both request and response JSON is about 1MB"; hence, that should normally be fine (however, it is always a good practice to consider beforehand matters, such as how many requests your API is expected to be serving concurrently, what other applications might be using the RAM, etc., in order to rule whether this is an issue or not). If you needed to, you could limit the number of requests to your API endpoints using, for example, SlowAPI (as shown in this answer). Also, you could limit the usage of the middleware to specific endpoints by checking the request.url.path against a pre-defined list of routes, as described in this answer (see "Update" section), or by using a Custom APIRoute class in a router instead (with similar logic inside as in the middleware below).
Working example
from fastapi import FastAPI, Request, Response
from starlette.background import BackgroundTask
from starlette.types import Message
import logging
app = FastAPI()
logging.basicConfig(filename='example.log', level=logging.DEBUG)
def log_req_resp(request_body, response_body):
logging.info(request_body.decode())
logging.info(response_body.decode())
async def set_body(request: Request, body: bytes):
async def receive() -> Message:
return {"type": "http.request", "body": body}
request._receive = receive
@app.middleware("http")
async def some_middleware(request: Request, call_next):
request_body = await request.body()
await set_body(request, request_body)
response = await call_next(request)
response_body = b''
async for chunk in response.body_iterator:
response_body += chunk
task = BackgroundTask(log_req_resp, request_body, response_body)
return Response(content=response_body, status_code=response.status_code,
headers=dict(response.headers), media_type=response.media_type, background=task)
In case you would like to perform some validation on the request body—for example, ensruing that the request body size is not exceeding a certain value—instead of using request.body(), you can process the body one chunk at a time using the .stream() method, as shown below (similar to this answer).
@app.middleware("http")
async def some_middleware(request: Request, call_next):
request_body = b''
async for chunk in request.stream():
request_body += chunk
...