5

I have a project with some APIs using FastAPI in which I need to call one of the API functions inside the project. The API function using FastAPI and Starlette looks like this

@router.put("/tab/{tab_name}", tags=["Tab CRUD"])
async def insert_tab(request: Request, tab_name: str):
    tab = await request.json()
    new_tab_name = tab["name"]
    new_tab_title = tab["title"]
    # Rest of the code

I 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().

Now, I need to call the insert_tab in another function so I need to somehow instantiate the Request object from Starlette. I have done this before but without the JSON body:

from starlette.requests import Request
from starlette.datastructures import Headers

headers = Headers()
scope = {
    'method': 'GET',
    'type': 'http',
    'headers': headers
}
request = Request(scope=scope)

But in this case, I also need to inject the JSON body to the Request object and I'm failing to find a way to do that.

Has anybody done this before or know how should I do this?

Gino Mempin
  • 25,369
  • 29
  • 96
  • 135
Ghasem
  • 14,455
  • 21
  • 138
  • 171
  • 1
    A better design would be to write a function that handles a raw Python dict, then call that function with `request.json()` from the API function, and call it directly from the "other function". In general you don't want to call API functions that rely on things like `Request` from elsewhere in your code, unless you're writing tests for your API (in which case there are ways to create a "mock request" for testing purposes). – Avish Jun 06 '20 at 13:46

3 Answers3

11

If you're trying to programmatically call another endpoint from within your app, it's better to skip the HTTP layer and call the underlying functions directly.

However, if you got to this page while trying to build a mock request for use in unit tests, here is an example:

from starlette.requests import Request
from starlette.datastructures import Headers


def build_request(
    method: str = "GET",
    server: str = "www.example.com",
    path: str = "/",
    headers: dict = None,
    body: str = None,
) -> Request:
    if headers is None:
        headers = {}
    request = Request(
        {
            "type": "http",
            "path": path,
            "headers": Headers(headers).raw,
            "http_version": "1.1",
            "method": method,
            "scheme": "https",
            "client": ("127.0.0.1", 8080),
            "server": (server, 443),
        }
    )
    if body:

        async def request_body():
            return body

        request.body = request_body
    return request

More info on working with Starlette's ASGI request object here:
https://www.encode.io/articles/working-with-http-requests-in-asgi

0

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

Gino Mempin
  • 25,369
  • 29
  • 96
  • 135
0

...I need to call one of the API functions inside the project...I need to somehow instantiate the Request object from Starlette.

If what you would like to achieve is to call another endpoint from within your app, here are a few options on how to do that (no need to instantiate the Request object, even if you are POSTing arbitrary JSON data).

Option 1

With your current implementation, use FastAPI/Starlette's TestClient:

from fastapi.testclient import TestClient

client = TestClient(app)
client.put("/tab/tab1", json={"name": "foo", "title": "bar"})

Option 2

Define an arbitrary dictionary as an expcted body parameter in your endpoint.

@router.put("/tab/{tab_name}", tags=["Tab CRUD"])
def insert_tab(tab: dict, tab_name: str):
    new_tab_name = tab["name"]
    new_tab_title = tab["title"]

and call the endpoint as shown below:

insert_tab({"name": "foo", "title": "bar"}, "tab1")

Option 3

Define a Pydantic model with the expected parameters. This approach has the benefit of data validation that Pydantic offers (however, you need to pre-define the fields you are expecting to send). Note that the model.dict() method below has noe been replaced by model.model_dump(...). You could also use dict(model) instead, or model.model_dump_json(...) as well (see this answer for more details on how FastAPI handles returning data in JSON format).

class MyModel(BaseModel):
    name: str
    title: str

@router.put("/tab/{tab_name}", tags=["Tab CRUD"])
def insert_tab(model: MyModel, tab_name: str):
    new_tab_name = model.name
    new_tab_title = model.title
    return model.dict()

and call the endpoint like this:

insert_tab(MyModel.parse_obj({"name": "foo", "title": "bar"}), "tab1")
Chris
  • 18,724
  • 6
  • 46
  • 80