fastapi / fastapi

FastAPI framework, high performance, easy to learn, fast to code, ready for production
https://fastapi.tiangolo.com/
MIT License
75.78k stars 6.4k forks source link

Middlewares breaking mounted app (socket.io app) #4123

Closed maslyankov closed 1 year ago

maslyankov commented 2 years ago

First Check

Commit to Help

Example Code

import fastapi
import fastapi.middleware.cors
import fastapi.middleware.httpsredirect
import fastapi.middleware.trustedhost

app_ret: fastapi.FastAPI = fastapi.FastAPI()

# HTTPS Everything
app_ret.add_middleware(
    fastapi.middleware.httpsredirect.HTTPSRedirectMiddleware
)

# Trusted hosts
app_ret.add_middleware(
    fastapi.middleware.trustedhost.TrustedHostMiddleware,
    allowed_hosts=_app_settings.allowed_hosts
)

# CORS
app_ret.add_middleware(
    fastapi.middleware.cors.CORSMiddleware,
    allow_origins=_app_settings.allowed_origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"]
)

import typing

import fastapi.logger
import socketio

class SocketManager:
    """
       Integrates SocketIO with FastAPI app.
       Adds `sio` property to FastAPI object (app).
       Default mount location for SocketIO app is at `/ws`
       and defautl SocketIO path is `socket.io`.
       (e.g. full path: `ws://www.example.com/ws/socket.io/)
       SocketManager exposes basic underlying SocketIO functionality
       e.g. emit, on, send, call, etc.
       """

    def __init__(
            self, app: fastapi.FastAPI = None, mount_location: str = "/sockets", socketio_path: str = "",
            cors_allowed_origins: typing.Union[str, list[str]] = "*", async_mode: str = "asgi"
    ) -> None:
        fastapi.logger.logger.debug(f"SocketIO server allowed origins: {cors_allowed_origins}")

        self._sio: socketio.AsyncServer = socketio.AsyncServer(
            async_mode=async_mode,
            cors_allowed_origins=cors_allowed_origins
        )
        self._app = socketio.ASGIApp(
            socketio_server=self._sio,
            socketio_path=socketio_path
        )

        if app:
            fastapi.logger.logger.debug(f"Mounting socketio to app ({mount_location})...")
            app.mount(mount_location, self._app, "SocketIO Server")

            app.sio = self._sio

    def get_socket_manager(self) -> socketio.AsyncServer:
        return self._sio

    def get_socket_app(self) -> socketio.ASGIApp:
        return self._app

socket_manager: socketio.AsyncServer = None
SettingsRef = typing.ForwardRef('Settings')
background_task_started = False

def init_sockets(app_obj: fastapi.FastAPI = None, config: SettingsRef = None):
    global socket_manager

    fastapi.logger.logger.debug("Initializing SocketsIO...")

    socket_manager_instance: SocketManager = SocketManager(app=app_obj, cors_allowed_origins="*")
    socket_manager = socket_manager_instance.get_socket_manager()

    @socket_manager.on('connect')
    async def connect(sid, environ, auth):
        global background_task_started

        if not background_task_started:
            socket_manager.start_background_task(background_task)
            background_task_started = True

        fastapi.logger.logger.debug(f'socket connected -> {sid}')

    @socket_manager.on('join')
    async def join(sid, message):
        socket_manager.enter_room(sid, message['room'])
        fastapi.logger.logger.info(f"{sid} joined room {message['room']}")
        await socket_manager.emit('my_response', {'data': 'Entered room: ' + message['room']},
                                  room=sid)

    async def background_task():
        """Example of how to send server generated events to clients."""
        count = 0
        while True:
            await socket_manager.sleep(10)
            count += 1
            fastapi.logger.logger.debug(f"Sending the periodic event via socket...")
            await socket_manager.emit('my_response', {'data': f'Server generated event No {count}'})

    @socket_manager.on('my_event')
    async def test_message(sid, message):
        await socket_manager.emit('my_response', {'data': message['data']}, room=sid)

    @socket_manager.on('my_broadcast_event')
    async def test_broadcast_message(sid, message):
        await socket_manager.emit('my_response', {'data': message['data']})

    @socket_manager.on('leave')
    async def leave(sid, message):
        socket_manager.leave_room(sid, message['room'])
        await socket_manager.emit('my_response', {'data': 'Left room: ' + message['room']},
                                  room=sid)

    @socket_manager.on('close room')
    async def close(sid, message):
        await socket_manager.emit('my_response',
                                  {'data': 'Room ' + message['room'] + ' is closing.'},
                                  room=message['room'])
        await socket_manager.close_room(message['room'])

    @socket_manager.on('my_room_event')
    async def send_room_message(sid, message):
        await socket_manager.emit('my_response', {'data': message['data']},
                                  room=message['room'])

    @socket_manager.on('disconnect request')
    async def disconnect_request(sid):
        await socket_manager.disconnect(sid)

    @socket_manager.on('disconnect')
    def test_disconnect(sid):
        print('Client disconnected')

    return socket_manager_instance.get_socket_app()

app.sockets.init_sockets(app_ret, _app_settings)

Description

Using the above code, if I comment out the middlewares, the sockets work as expected and can connect. If I leave the middlewares, the client cannot connect and I constantly get errors as such:

ERROR:uvicorn.error:Exception in ASGI application
Traceback (most recent call last):
  File "C:\Users\user\AppData\Local\pypoetry\Cache\virtualenvs\server-AW5Yy82W-py3.9\lib\site-packages\uvicorn\protocols\websockets\websockets_impl.py", line 203, in run_asgi
    result = await self.app(self.scope, self.asgi_receive, self.asgi_send)
  File "C:\Users\user\AppData\Local\pypoetry\Cache\virtualenvs\server-AW5Yy82W-py3.9\lib\site-packages\uvicorn\middleware\proxy_headers.py", line 75, in __call__
    return await self.app(scope, receive, send)
  File "C:\Users\user\AppData\Local\pypoetry\Cache\virtualenvs\server-AW5Yy82W-py3.9\lib\site-packages\fastapi\applications.py", line 208, in __call__
    await super().__call__(scope, receive, send)
  File "C:\Users\user\AppData\Local\pypoetry\Cache\virtualenvs\server-AW5Yy82W-py3.9\lib\site-packages\starlette\applications.py", line 112, in __call__
    await self.middleware_stack(scope, receive, send)
  File "C:\Users\user\AppData\Local\pypoetry\Cache\virtualenvs\server-AW5Yy82W-py3.9\lib\site-packages\starlette\middleware\errors.py", line 146, in __call__
    await self.app(scope, receive, send)
  File "C:\Users\user\AppData\Local\pypoetry\Cache\virtualenvs\server-AW5Yy82W-py3.9\lib\site-packages\starlette\middleware\cors.py", line 76, in __call__
    await self.app(scope, receive, send)
  File "C:\Users\user\AppData\Local\pypoetry\Cache\virtualenvs\server-AW5Yy82W-py3.9\lib\site-packages\starlette\middleware\trustedhost.py", line 60, in __call__
    await response(scope, receive, send)
  File "C:\Users\user\AppData\Local\pypoetry\Cache\virtualenvs\server-AW5Yy82W-py3.9\lib\site-packages\starlette\responses.py", line 132, in __call__
    await send(
  File "C:\Users\user\AppData\Local\pypoetry\Cache\virtualenvs\server-AW5Yy82W-py3.9\lib\site-packages\uvicorn\protocols\websockets\websockets_impl.py", line 255, in asgi_send
    raise RuntimeError(msg % message_type)
RuntimeError: Expected ASGI message 'websocket.accept' or 'websocket.close', but got 'http.response.start'.
←[32mINFO←[0m:     connection open
INFO:uvicorn.error:connection open
←[32mINFO←[0m:     connection closed
INFO:uvicorn.error:connection closed

Operating System

Windows

Operating System Details

No response

FastAPI Version

0.70.0

Python Version

3.9.6

Additional Context

Starting the server with:

uvicorn main:app_ret --port 5000
maslyankov commented 2 years ago

Client page for test:

<!DOCTYPE HTML>
<html>
<head>
    <title>python-socketio test</title>
    <script type="text/javascript" src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/3.0.3/socket.io.min.js"></script>
    <script type="text/javascript" charset="utf-8">
        $(document).ready(function(){
            var socket = io.connect("ws://127.0.0.1:5000", {
                path: "/sockets/socket.io"
            });

            console.log(`Socket obj is:`);
            console.dir(socket);

            socket.on('connect', function() {
                socket.emit('my_event', {data: 'I\'m connected!'});
            });
            socket.on('disconnect', function() {
                $('#log').append('<br>Disconnected');
            });
            socket.on('my_response', function(msg) {
                console.log(`Received: ${msg.data}`);
                $('#log').append('<br>Received: ' + msg.data);
            });

            // event handler for server sent data
            // the data is displayed in the "Received" section of the page
            // handlers for the different forms in the page
            // these send data to the server in a variety of ways
            $('form#emit').submit(function(event) {
                socket.emit('my_event', {data: $('#emit_data').val()});
                return false;
            });
            $('form#broadcast').submit(function(event) {
                socket.emit('my_broadcast_event', {data: $('#broadcast_data').val()});
                return false;
            });
            $('form#join').submit(function(event) {
                socket.emit('join', {room: $('#join_room').val()});
                return false;
            });
            $('form#leave').submit(function(event) {
                socket.emit('leave', {room: $('#leave_room').val()});
                return false;
            });
            $('form#send_room').submit(function(event) {
                socket.emit('my_room_event', {room: $('#room_name').val(), data: $('#room_data').val()});
                return false;
            });
            $('form#close').submit(function(event) {
                socket.emit('close room', {room: $('#close_room').val()});
                return false;
            });
            $('form#disconnect').submit(function(event) {
                socket.emit('disconnect request');
                return false;
            });
        });
    </script>
</head>
<body>
    <h1>python-socketio test</h1>
    <h2>Send:</h2>
    <form id="emit" method="POST" action='#'>
        <input type="text" name="emit_data" id="emit_data" placeholder="Message">
        <input type="submit" value="Echo">
    </form>
    <form id="broadcast" method="POST" action='#'>
        <input type="text" name="broadcast_data" id="broadcast_data" placeholder="Message">
        <input type="submit" value="Broadcast">
    </form>
    <form id="join" method="POST" action='#'>
        <input type="text" name="join_room" id="join_room" placeholder="Room Name">
        <input type="submit" value="Join Room">
    </form>
    <form id="leave" method="POST" action='#'>
        <input type="text" name="leave_room" id="leave_room" placeholder="Room Name">
        <input type="submit" value="Leave Room">
    </form>
    <form id="send_room" method="POST" action='#'>
        <input type="text" name="room_name" id="room_name" placeholder="Room Name">
        <input type="text" name="room_data" id="room_data" placeholder="Message">
        <input type="submit" value="Send to Room">
    </form>
    <form id="close" method="POST" action="#">
        <input type="text" name="close_room" id="close_room" placeholder="Room Name">
        <input type="submit" value="Close Room">
    </form>
    <form id="disconnect" method="POST" action="#">
        <input type="submit" value="Disconnect">
    </form>
    <h2>Received:</h2>
    <div><p id="log"></p></div>
</body>
</html>