Using FastAPI in a sync way, how can I get the raw body of a POST request?
Asked Answered
H

2

22

Using FastAPI in a sync, not async mode, I would like to be able to receive the raw, unchanged body of a POST request.

All examples I can find show async code, when I try it in a normal sync way, the request.body() shows up as a coroutine object.

When I test it by posting some XML to this endpoint, I get a 500 "Internal Server Error".

from fastapi import FastAPI, Response, Request, Body

app = FastAPI()

@app.get("/")
def read_root():
    return {"Hello": "World"}

@app.post("/input")
def input_request(request: Request):
    # how can I access the RAW request body here?  
    body = request.body()

    # do stuff with the body here  

    return Response(content=body, media_type="application/xml")

Is this not possible with FastAPI?

Note: a simplified input request would look like:

POST http://127.0.0.1:1083/input
Content-Type: application/xml

<XML>
    <BODY>TEST</BODY>
</XML>

and I have no control over how input requests are sent, because I need to replace an existing SOAP API.

Hutson answered 10/1, 2022 at 20:52 Comment(0)
G
25

Using async def endpoint

If an object is a co-routine, it needs to be awaited. FastAPI is actually Starlette underneath, and Starlette methods for returning the request body are async methods (see the source code here as well); thus, one needs to await them (inside an async def endpoint). For example:

from fastapi import Request

@app.post("/input")
async def input_request(request: Request):
    return await request.body()

Update 1 - Using def endpoint

Alternatively, if you are confident that the incoming data is a valid JSON, you can define your endpoint with def instead, and use the Body field, as shown below (for more options on how to post JSON data, see this answer):

from fastapi import Body

@app.post("/input")
def input_request(payload: dict = Body(...)):
    return payload

If, however, the incoming data are in XML format, as in the example you provided, one option is to pass them using Files instead, as shown below—as long as you have control over how client data are sent to the server (have a look here as well). Example:

from fastapi import File

@app.post("/input") 
def input_request(contents: bytes = File(...)): 
    return contents

Update 2 - Using def endpoint and async dependency

As described in this post, you can use an async dependency function to pull out the body from the request. You can use async dependencies on non-async (i.e., def) endpoints as well. Hence, if there is some sort of blocking code in this endpoint that prevents you from using async/await—as I am guessing this might be the reason in your case—this is the way to go.

Note: I should also mention that this answer—which explains the difference between def and async def endpoints (that you might be aware of)—also provides solutions when you are required to use async def (as you might need to await for coroutines inside a route), but also have some synchronous expensive CPU-bound operation that might be blocking the server. Please have a look.

Example of the approach described earlier can be found below. You can uncomment the time.sleep() line, if you would like to confirm yourself that a request won't be blocking other requests from going through, as when you declare an endpoint with normal def instead of async def, it is run in an external threadpool (regardless of the async def dependency function).

from fastapi import FastAPI, Depends, Request
import time

app = FastAPI()

async def get_body(request: Request):
    return await request.body()

@app.post("/input")
def input_request(body: bytes = Depends(get_body)):
    print("New request arrived.")
    #time.sleep(5)
    return body
Gardol answered 10/1, 2022 at 21:40 Comment(4)
In sync there's no await, that's why I'm asking. FastAPI supports sync requests after all...Hutson
I'll check and get back to thisHutson
the "File" approach returns "HTTP/1.1 422 Unprocessable Entity". I added an example request to the original question. Note: I can't control how clients requests look like because I'm recreating a legacy SOAP API to be able to re-route the request from the legacy system to a Python service that will replace the old service.Hutson
Thanks for revisiting this. Update #2 works for me (using FastAPI 0.79.0 and Python 3.10.5 on Windows 10) - it also works fine if several requests are made at the same time.Hutson
S
2

For convenience, you can simply use asgiref, this package supports async_to_sync and sync_to_async:

from asgiref.sync import async_to_sync

sync_body_func = async_to_sync(request.body)
print(sync_body_func())

async_to_sync execute an async function in an eventloop, sync_to_async execute a sync function in a threadpool.

Summons answered 1/12, 2022 at 3:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.