How to use FastAPI Depends for endpoint/route in separate file?
Asked Answered
N

3

6

I have an Websocket endpoint defined in separate file, like:

from starlette.endpoints import WebSocketEndpoint
from connection_service import ConnectionService


class WSEndpoint(WebSocketEndpoint):
    """Handles Websocket connections"""

    async def on_connect(self,
            websocket: WebSocket,
            connectionService: ConnectionService = Depends(ConnectionService)):
        """Handles new connection"""
        self.connectionService = connectionService
        ...

and in the main.py I register endpoint as:

from fastapi import FastAPI
from starlette.routing import WebSocketRoute
from ws_endpoint import WSEndpoint

app = FastAPI(routes=[ WebSocketRoute("/ws", WSEndpoint) ])

But Depends for my endpoint is never resolved. Is there a way to make it work?

Plus, what is even the purpose of this mechanism in FastAPI? Cannot we just use local/global variables?

Nitro answered 11/11, 2020 at 7:13 Comment(0)
T
5

TL;DR

The documents seem to hint that you can only use Depends for request functions.

Explanation

I found a related issue #2057 in the FastAPI repo and it seems the Depends(...) only works with the requests and not anything else.

I confirmed this by,

from fastapi import Depends, FastAPI

app = FastAPI()


async def foo_func():
    return "This is from foo"


async def test_depends(foo: str = Depends(foo_func)):
    return foo


@app.get("/")
async def read_items():
    depends_result = await test_depends()
    return depends_result

In this case, the dependency didn't get resolved.


Coming to your case, you can resolve the dependency something like this,

from starlette.endpoints import WebSocketEndpoint
from connection_service import ConnectionService


class WSEndpoint(WebSocketEndpoint):
    async def on_connect(
            self,
            websocket: WebSocket,
            connectionService=None
    ):
        if connectionService is None:
            connectionService = ConnectionService()  # calling the depend function

        self.connectionService = connectionService
Theft answered 11/11, 2020 at 8:27 Comment(6)
Ooh, thank you, so it is mentioned in the documentation? I didn't pay attention to that. So, yes, you're right, the only place where Dependencies are resolved is in FastAPI add_api_route and add_api_websocket_route, or if used their decorator analogs. And you can define a function in one file and then register it in main and it will work fine. __ Although, I don't understand the purpose of this "Dependency Injection" mechanism in scope of FastAPI then, what does it solve?Nitro
I don't think they mentioned using Depends() only with requests, but, I couldn't find any example that shows the opposite. So, I reached the above conclusion. Also, the example I have tried made me believe so.Theft
They don't mention it, but this is the only way it works now as far as I saw. I can see it source code in fastapi.applications.py and if to put a breakpoint in fastpi.dependencies.utils.get_dependantNitro
Actually, your code doesn't make much sense because there will be separate WSEndpoint instance per connection, so you always will hit is None check. Plus I wanted to use ConnectionService as singleton for all connections, but of course I can implement it separately with decorator or other methods.Nitro
"...I wanted to use ConnectionService as singleton for all connections" If you want to make it a singleton, can't it possible to define it out of the class?Theft
Also, I was focusing on saying that the Depends won't work as you intended.Theft
N
9

After hours learning playing around with Dependency Injection and routes/endpoints in FastAPI here is what I found.

Route vs Endpoint

First of all want to point out that Endpoint is a concept that exists in Starlette and no in FastAPI. In my question I show code where I use WebSocketEndpoint class and Dependency Injection will not work in FastAPI. Read further to understand why.

Dependency injection (DI)

DI in FastAPI is not a classic pattern that we know, it is not resolving magically all dependencies everywhere.

Depends is only resolved for FastAPI routes, meaning using methods: add_api_route and add_api_websocket_route, or their decorator analogs: api_route and websocket, which are just wrappers around first two.

Then dependencies are going to be resolved when request comes to the route by FastAPI. This is important to understand that FastAPI is resolving dependencies and not Starlette. FastAPI is build on top of Starlette and you may want to use also some "raw" Starlette features, like: add_route or add_websocket_route, but then you will not have Depends resolution for those.

Also, DI in FastAPI can be used to resolve instances of classes but it's not its main purpose + it makes no sense in Python because you can just use CLOSURE. Where Depends shine is when you need some sort of request validation (what Django accomplishes with decorators). In this usage Depends is great, because it resolves route dependencies and those sub dependencies. Check out my code below and I use auth_check.

Code example

As a bonus I want to have websocket route as a class in separate file with separated methods for connect, disconnect and receive. Also, I want to have authentication check in separate file to be able to swap it in easily.

# main.py
from fastapi import FastAPI
from ws_route import WSRoute

app = FastAPI()
app.add_api_websocket_route("/ws", WSRoute)
# auth.py
from fastapi import WebSocket

def auth_check(websocket: WebSocket):
    # `websocket` instance is resolved automatically
    # and other `Depends` as well. They are what's called sub dependencies.
    # Implement your authentication logic here:
    # Parse Headers or query parameters (which is usually a way for websockets)
    # and perform verification
    return True
# ws_route.py
import typing

import starlette.status as status
from fastapi import WebSocket, WebSocketDisconnect, Depends

from auth import auth_check

class WSRoute:

    def __init__(self,
            websocket: WebSocket,
            is_authenticated: bool = Depends(auth_check)):
        self._websocket = websocket

    def __await__(self) -> typing.Generator:
        return self.dispatch().__await__()

    async def dispatch(self) -> None:
        # Websocket lifecycle
        await self._on_connect()

        close_code: int = status.WS_1000_NORMAL_CLOSURE
        try:
            while True:
                data = await self._websocket.receive_text()
                await self._on_receive(data)
        except WebSocketDisconnect:
            # Handle client normal disconnect here
            pass
        except Exception as exc:
            # Handle other types of errors here
            close_code = status.WS_1011_INTERNAL_ERROR
            raise exc from None
        finally:
            await self._on_disconnect(close_code)

    async def _on_connect(self):
        # Handle your new connection here
        await self._websocket.accept()
        pass

    async def _on_disconnect(self, close_code: int):
        # Handle client disconnect here
        pass

    async def _on_receive(self, msg: typing.Any):
        # Handle client messaging here
        pass
Nitro answered 12/11, 2020 at 19:9 Comment(1)
Thanks for the explanation. I was confused at first with FastAPI bc when I hear dependency injection, I think of containers and dependencies being injected/resolved anywhere but as you explained the dependency injection only works when starting at a FastAPI path operation, on a per-request basis. Being this is a server, it's not really a limitation as I first thought, it's just a pattern I am not used to.Cuddy
T
5

TL;DR

The documents seem to hint that you can only use Depends for request functions.

Explanation

I found a related issue #2057 in the FastAPI repo and it seems the Depends(...) only works with the requests and not anything else.

I confirmed this by,

from fastapi import Depends, FastAPI

app = FastAPI()


async def foo_func():
    return "This is from foo"


async def test_depends(foo: str = Depends(foo_func)):
    return foo


@app.get("/")
async def read_items():
    depends_result = await test_depends()
    return depends_result

In this case, the dependency didn't get resolved.


Coming to your case, you can resolve the dependency something like this,

from starlette.endpoints import WebSocketEndpoint
from connection_service import ConnectionService


class WSEndpoint(WebSocketEndpoint):
    async def on_connect(
            self,
            websocket: WebSocket,
            connectionService=None
    ):
        if connectionService is None:
            connectionService = ConnectionService()  # calling the depend function

        self.connectionService = connectionService
Theft answered 11/11, 2020 at 8:27 Comment(6)
Ooh, thank you, so it is mentioned in the documentation? I didn't pay attention to that. So, yes, you're right, the only place where Dependencies are resolved is in FastAPI add_api_route and add_api_websocket_route, or if used their decorator analogs. And you can define a function in one file and then register it in main and it will work fine. __ Although, I don't understand the purpose of this "Dependency Injection" mechanism in scope of FastAPI then, what does it solve?Nitro
I don't think they mentioned using Depends() only with requests, but, I couldn't find any example that shows the opposite. So, I reached the above conclusion. Also, the example I have tried made me believe so.Theft
They don't mention it, but this is the only way it works now as far as I saw. I can see it source code in fastapi.applications.py and if to put a breakpoint in fastpi.dependencies.utils.get_dependantNitro
Actually, your code doesn't make much sense because there will be separate WSEndpoint instance per connection, so you always will hit is None check. Plus I wanted to use ConnectionService as singleton for all connections, but of course I can implement it separately with decorator or other methods.Nitro
"...I wanted to use ConnectionService as singleton for all connections" If you want to make it a singleton, can't it possible to define it out of the class?Theft
Also, I was focusing on saying that the Depends won't work as you intended.Theft
C
0

I was facing the same issue. Depends/Query was not working. I stopped using WebSocketEndpoint and tried things like this

socket.py

client_id will from frontend as a token query string

# @app.websocket("/ws/hello/token")
async def websocket_hello_endpoint_with_token(websocket: WebSocket, client_id: str = Query(..., alias="token")):
    #on_connect
    await websocket.accept()
    try:
        while True:
            data = await websocket.receive_text()
             #on_receive
            await websocket.send_text(f"Token: {client_id}  & Message text was: {data}")
    except WebSocketDisconnect:
        #on_disconnect
        pass

main.py

using websocket_hello_endpoint_with_token

app = FastAPI()
app.add_api_websocket_route("/ws/hello/token", socket.websocket_hello_endpoint_with_token)

client

<!DOCTYPE html>
<html>
    <head>
        <title>Chat</title>
    </head>
    <body>
        <h1>WebSocket Chat</h1>
        <form action="" onsubmit="sendMessage(event)">
            <label>Token: <input type="text" id="token" autocomplete="off" value="some-key-token"/></label>
            <button onclick="connect(event)">Connect</button>
            <hr>
            <label>Message: <input type="text" id="messageText" autocomplete="off"/></label>
            <button>Send</button>
        </form>
        <ul id='messages'>
        </ul>
        <script>
        var ws = null;
            function connect(event) {
                var token = document.getElementById("token")
                ws = new WebSocket("ws://localhost:6003/ws/hello/token?token=" + token.value);
                
                ws.onopen = function () {
                  console.log('socket opened'); 
                };
                ws.onmessage = function(event) {
                    var messages = document.getElementById('messages')
                    var message = document.createElement('li')
                    var content = document.createTextNode(event.data)
                    <!-- var data = document.createTextNode(event.data) -->
                    <!-- var content = "message:" + data.message -->
                    message.appendChild(content)
                    messages.appendChild(message)
                };
                
                ws.onclose = function(e) {  
                  console.log('socket closed from server'); 
                }

                ws.onerror = function(err) {
                  console.error(err)
                };
                
                event.preventDefault()
            }
            function sendMessage(event) {
                var input = document.getElementById("messageText")
                ws.send(input.value)
                input.value = ''
                event.preventDefault()
            }
        </script>
    </body>
</html>
Caracal answered 20/2, 2021 at 15:14 Comment(3)
You can find my answer below where I describe how I have managed design solution with working websocket endpoint. At the end it looks quite elegant I think.Nitro
@Nitro WSRoute was auto disconnected, after connectionCaracal
I need to look into your code, we are successfully using WSRoute on our project and it works fine. Maybe there is some syntax error. Try to debug to find what actually causing the disconnect. But if you want to use my version I can assure you it works.Nitro

© 2022 - 2024 — McMap. All rights reserved.