python-3.x FASTAPI自定义中间件获取内部请求主体

mspsb9vt  于 2022-12-15  发布在  Python
关注(0)|答案(5)|浏览(246)

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.

@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.

lhcgjxsq

lhcgjxsq1#

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
yqhsw0fo

yqhsw0fo2#

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
f0brbegy

f0brbegy3#

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)])
vfh0ocws

vfh0ocws4#

在FASTAPI的中间件内部尝试request.body()request.json()将挂起。这是Starlette中的已知问题,记录如下,并提供了解决方法:
https://github.com/tiangolo/fastapi/issues/394#issuecomment-883524819

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
inb24sb2

inb24sb25#

Quoting FastAPI Doc about "Details about the Request object":
As FastAPI is actually Starlette underneath, with a layer of several tools on top, you can use Starlette's Request object directly when you need to.
And the starlette doc about the request body object says:
There are a few different interfaces for returning the body of the request:

  • The request body as bytes: await request.body()
  • The request body, parsed as form data or multipart: await request.form()
  • The request body, parsed as JSON: await request.json()
@app.middleware("http")
async def TestCustomMiddleware(request: Request, call_next):
    the_headers = request.headers
    the_body = await request.json()

    print(the_headers)
    print(the_body)

    response = await call_next(request)

    return response

Tested with curl:

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"test": "Also works!"}' \
    http://localhost:8000/foo

相关问题