I'm not an expert on ASGI and HTTP requests, but here's a way to make it work.
To create a Request
object with a request body, you can pass in a receive
argument:
# starlette/requests.py
class Request(HTTPConnection):
def __init__(
self, scope: Scope, receive: Receive = empty_receive, send: Send = empty_send
):
where receive
is of type Receive
:
# starlette/types.py
Message = typing.MutableMapping[str, typing.Any]
Receive = typing.Callable[[], typing.Awaitable[Message]]
which is a sort of an await
able function that returns a Message
, which is a sort of mutable mapping (such as a dict
). The reason receive
is a function is because it's part of the "receive" channel that is expected to "receive" the request body as a stream of messages:
Most of the information about the incoming request is stored in the “scope”, and presented at the point the ASGI app is instantiated. However, for the request body, that’s not possible.
In order to access the request body, we have to get a stream of messages from the “receive” channel.
You can see how it's used from Starlette's code for requests:
self._stream_consumed = True
while True:
message = await self._receive() # <------------- this.
if message["type"] == "http.request":
body = message.get("body", b"")
if body:
yield body
if not message.get("more_body", False):
break
elif message["type"] == "http.disconnect":
self._is_disconnected = True
raise ClientDisconnect()
yield b""
Since you probably already have your request body ready, you can just fake-stream it in one chunk, taking into account the expected keys for "type"
, "body"
(the actual body/payload), and a "more_body"
key set to False
to break out of the stream.
import json
from starlette.requests import Message
async def create_body() -> Message:
body = {'abc': 123, 'def': {'ghi': 456}}
return {
'type': 'http.request',
'body': json.dumps(body).encode('utf-8'),
'more_body': False,
}
You can then pass that to Request
's receive=
parameter to call your function:
from starlette.requests import Request
@router.put('/tab/{tab_name}')
async def insert_tab(request: Request, tab_name: str):
tab = await request.json()
return {tab_name: tab}
@router.put('/call-insert-tab')
async def some_other_function():
req = Request(
scope={
'type': 'http',
'scheme': 'http',
'method': 'PUT',
'path': '/tab/abc',
'raw_path': b'/tab/abc',
'query_string': b'',
'headers': {}
},
receive=create_body, # <-------- Pass it here
)
return await insert_tab(req, 'abc')
While that could work, I would say it isn't great. The purpose of using a library like Starlette is to not bother with how these HTTP Request objects are actually created and handled. I didn't test the above code beyond the example, but I feel like it could break at some point (especially with FastAPI's dependency injections).
Since your use-case is simply to
send a json containing the new tab data as the body of my request which later will be converted to a python dict using await request.json()
You actually don't need to use Request
directly for this. What I suggest is to instead replace your request: Request
parameter with a tab: Tab
model as explained in the FastAPI tutorial on passing in a request body: https://fastapi.tiangolo.com/tutorial/body/
from pydantic import BaseModel
class Tab(BaseModel):
abc: int
xyz: int
@app.put('/tab/{tab_name}')
async def insert_tab(tab: Tab, tab_name: str):
print(tab.abc)
print(tab.xyz)
return {tab_name: tab}
FastAPI will automatically get the body of the underlying Request
object and convert it from JSON into your model. (It auto-assumes that parameters not in the path are part of the body/payload.). And it should work just the same as request.json()
:
$ cat data.json
{
"abc": 123,
"xyz": 456
}
$ curl -s -XPUT 'http://localhost:5050/tab/tab1' --header 'Content-Type: application/json' --data @data.json | jq
{
"tab1": {
"abc": 123,
"xyz": 456
}
}
That would also make it easier to call your route functions that require a request body: you just need to pass in the data directly without bothering with Requests
:
@app.put('/tab/{tab_name}')
async def insert_tab(tab: Tab, tab_name: str):
return {tab_name: tab}
@app.put('/call-insert-tab')
async def some_other_function():
tab = Tab(abc=123, xyz=456)
return await insert_tab(tab, 'tab1')
See the FastAPI tutorial on Request Body for more info and examples. If you are trying to avoid Pydantic or don't want to create classes for the body for whatever reason, there is also a Body
type parameter: https://fastapi.tiangolo.com/tutorial/body-multiple-params/#singular-values-in-body.