pythonwebsocketdaphnefalconframeworkasgi

Websockets with Falcon & Daphne behind a K8s Ingress


My Falcon based ASGI app is executed via Daphne and works fine when it is run locally and accessed via localhost. The app is packaged in a container and run in a K8s cluster behind an Ingress. On K8s the app is not running at the root of the domain, but in a /sub/folder. This breaks the routing of Falcon. I could not find an existing middleware for this, so I implemented one:

class StripRootPath:
    async def process_request(self, req, resp):
        root_path = req.root_path
        if root_path:
            req.path = req.path[len(root_path) :]

This works fine if I call Daphne like this

daphne --root-path /sub/path ...

and as long as I'm only using "normal" HTTP requests. Working with websockets fails. The error messages suggested that paths could not be found, so I assumed that my stripping of the root path did not work. I figured out that I was right. There is a special

async def process_request_ws(self, req, ws):

for websocket connections. I tried to implement this method exactly as process_request but req.root_path is empty. I did not find any value in req that would allow me to strip the prefix.

Now I wonder if I'm doing something completely wrong or if this is a bug and root_path is supposed to be set!?


Solution

  • This seems to be a bug in Daphne, the request scope for WebSocket does not contain the root_path key/value.

    process_request_ws worked fine to redirect internally by modifying req.path, however, I simply hardcoded the prefix in my proof-of-concept test.py:

    import falcon
    import falcon.asgi
    
    
    class InternalRedirect:
        async def process_request(self, req, resp):
            print(f'req.path={req.path}; req.root_path={req.root_path}')
            if req.path.startswith('/root/'):
                req.path = req.path.split('/root', 1)[1]
    
        async def process_request_ws(self, req, ws):
            print(f'req.path={req.path}; req.root_path={req.root_path}')
            if req.path.startswith('/root/'):
                req.path = req.path.split('/root', 1)[1]
    
    
    class HelloResource:
        async def on_get(self, req, resp, name):
            resp.media = {'message': f'Hello, {name}!'}
    
        async def on_websocket(self, req, ws, name):
            try:
                await ws.accept()
                await ws.send_media({'message': f'Hello, {name}'})
    
                while True:
                    payload = await ws.receive_text()
                    print(f'Received: [{payload}])')
            except falcon.WebSocketDisconnected:
                pass
    
    
    app = falcon.asgi.App(middleware=[InternalRedirect()])
    app.add_route('/hello/{name}', HelloResource())
    

    I tried running the same app in Uvicorn, and root_path is present both for ordinary GET requests, and when upgraded to a WebSocket:

    $ uvicorn --root-path /root test:app
    
    ...
    
    INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
    req.path=/root/hello/robot; req.root_path=/root
    INFO:     ('127.0.0.1', 33776) - "WebSocket /root/root/hello/robot" [accepted]
    INFO:     connection open
    INFO:     connection closed
    

    OTOH, even Uvicorn's log illustrates (NB the double /root/root) the confusion around root_path in ASGI; contrary to WSGI's SCRIPT_NAME, it is unclear whether ASGI path should include it or not. See also:

    Despite the confusion, Uvicorn should solve your problem at hand. Moreover, it's one the most popular and performant ASGI app servers out there, so my suggested solution is to simply try swapping out Daphne to Uvicorn.