19

Been trying to get the BODY of a request using FASTAPI middleware but it seems i can only get request.headers but not the body. I am in need of the body in order to get a key that I will use to check something on the database. Think of logging or authentication usage of a middleware. This has to be done without customizing APIRouter, needs to be done only at the Middleware level to not affect the rest of the application.

@app.middleware("http")
    async def TestCustomMiddleware(request: Request, call_next):
    print("Middleware works!", request.headers)

    response = await call_next(request)
    resp_body = [section async for section in response.__dict__['body_iterator']]
    print("BODY:", resp_body)
    return response

I am able to get this but there is an error that will break the POST request:

INFO:     Started server process [37160]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
Middleware works! Headers({'content-type': 'application/json', 'user-agent': 'PostmanRuntime/7.26.8', 'accept': '*/*', 'cache-control': 'no-cache', 'postman-token': 'ca6839ec-833d-45c0-9b52-8f904db13966', 'host': 'localhost:8000', 'accept-encoding': 'gzip, deflate, br', 'connection': 'keep-alive', 'content-length': '12'})
BODY: [b'{"test":"1"}']
INFO:     127.0.0.1:60761 - "POST /jctest HTTP/1.1" 200 OK
ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "C:\Python\Python38\lib\site-packages\uvicorn\protocols\http\httptools_impl.py", line 386, in run_asgi
    result = await app(self.scope, self.receive, self.send)
  File "C:\Python\Python38\lib\site-packages\uvicorn\middleware\proxy_headers.py", line 45, in __call__
    return await self.app(scope, receive, send)
  File "C:\Python\Python38\lib\site-packages\fastapi\applications.py", line 181, in __call__
    await super().__call__(scope, receive, send)
  File "C:\Python\Python38\lib\site-packages\starlette\applications.py", line 111, in __call__
    await self.middleware_stack(scope, receive, send)
  File "C:\Python\Python38\lib\site-packages\starlette\middleware\errors.py", line 181, in __call__
    raise exc from None
  File "C:\Python\Python38\lib\site-packages\starlette\middleware\errors.py", line 159, in __call__
    await self.app(scope, receive, _send)
  File "C:\Python\Python38\lib\site-packages\starlette\middleware\base.py", line 26, in __call__
    await response(scope, receive, send)
  File "C:\Python\Python38\lib\site-packages\starlette\responses.py", line 228, in __call__
    await run_until_first_complete(
  File "C:\Python\Python38\lib\site-packages\starlette\concurrency.py", line 18, in run_until_first_complete
    [task.result() for task in done]
  File "C:\Python\Python38\lib\site-packages\starlette\concurrency.py", line 18, in <listcomp>
    [task.result() for task in done]
  File "C:\Python\Python38\lib\site-packages\starlette\responses.py", line 225, in stream_response
    await send({"type": "http.response.body", "body": b"", "more_body": False})
  File "C:\Python\Python38\lib\site-packages\starlette\middleware\errors.py", line 156, in _send
    await send(message)
  File "C:\Python\Python38\lib\site-packages\uvicorn\protocols\http\httptools_impl.py", line 516, in send
    raise RuntimeError("Response content shorter than Content-Length")
RuntimeError: Response content shorter than Content-Length

How can I fix this so I can get the body of the request which is {"test":"1"}?

Trying get the body to find a key that will be used to check a database and either grant access to an API or deny it based on credentials.

0

4 Answers 4

9

I would recommend using a router instead. Reference this github issue

Here is an example

app = FastAPI()
api_router = APIRouter()


async def log_request_info(request: Request):
    request_body = await request.json()

    logger.info(
        f"{request.method} request to {request.url} metadata\n"
        f"\tHeaders: {request.headers}\n"
        f"\tBody: {request_body}\n"
        f"\tPath Params: {request.path_params}\n"
        f"\tQuery Params: {request.query_params}\n"
        f"\tCookies: {request.cookies}\n"
    )


@api_router.get("/", summary="Status")
async def status_get():
    logger.debug('Status requested')
    return {'status': 'OK'}


@api_router.post("/", )
def status_post(urls: Optional[List[str]] = None):
    logger.debug('Status requested')
    return {'status': 'OK'}


app.include_router(api_router, dependencies=[Depends(log_request_info)])

Sign up to request clarification or add additional context in comments.

2 Comments

I just want to say, after trying dozens of different answers for this question across stackoverflow, this is finally the answer that actually worked. Thanks!
add this: if request.method not in ("POST", "PUT", "PATCH"): return - because if request method, for example is GET, we will have error
7

You need to await on the request so the request object is infact ready to be read. This is how I achieved it.

class RequestContextLogMiddleware(BaseHTTPMiddleware):

    async def set_body(self, request: Request):
        receive_ = await request._receive()

        async def receive() -> Message:
            return receive_

        request._receive = receive

    async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
        await self.set_body(request)
        body = await request.body()
        jsonbody = await request.json()
        id_ = jsonbody['external_id']
        response = await call_next(request)    
        return response

3 Comments

when the payload is bigger, the ` response = await call_next(request)` gets stuck indefinitely.
tnx for this! for somebody cold be better to use: @app.middleware("http") async def your_middleware(request: Request, call_next): pass
Please avoid this solution, as this will cause the application to stuck if you have larger request body payload as mentioned by @Irfanuddin. In my case 40KB was enough to get app hang indefinitely. It look me lot of time in debugging and finding the root cause as developer had put above code in the application.
7

Trying request.body() or request.json() inside of the middleware for FASTAPI will hang.

This is a known issue in Starlette documented below: https://github.com/tiangolo/fastapi/issues/394#issuecomment-883524819

Workaround is to put in set_body and get_body and await set_body(request, await request.body()) code. Boilerplate below:

 async def set_body(request: Request, body: bytes):
     async def receive() -> Message:
         return {"type": "http.request", "body": body}
     request._receive = receive
 
 async def get_body(request: Request) -> bytes:
     body = await request.body()
     await set_body(request, body)
     return body
 
 @app.middleware("http")
 async def app_entry(request: Request, call_next):
     
     await set_body(request, await request.body())
 
     print(await get_body(request))
 
     response = await call_next(request)
     return response

1 Comment

This will cause the application to stuck if you have larger request body payload. In my case 40KB request body was enough to get app hang indefinitely. It look me lot of time in debugging and finding the root cause as developer had put above code in the application
2

Solution of getting request body references FastAPI - Custom APIRoute class in a router

class CustomRoute(APIRoute):
    def get_route_handler(self) -> Callable:
        original_route_handler = super().get_route_handler()

        async def custom_route_handler(request: Request) -> Response:
            body = await request.body()
            response: Response = await original_route_handler(request)
            return response

        return custom_route_handler

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.