0

Users may come across some difficulties with the effect that the mount path, socketio path, etc. have in getting a React frontend connected with a FastAPI+SocketIO or solo SocketIO server.

Typically, they will encounter an error along the lines of:

Access to XMLHttpRequest at 'http://http://127.0.0.1:8000{YOU MESS UP HERE}?EIO=4&transport=polling&t=O6ytHpU' 

from origin 'http://localhost:3000' has been blocked by CORS policy: 

No 'Access-Control-Allow-Origin' header is present on the requested resource.

Where port 8000 is typically the port used by a uvicorn-run application and port 3000 is the React server.

The section highlighted as YOU MESS UP HERE corresponds to an incorrect syntax given regarding mount path and/or socketio path.

This question serves to help illuminate the correct syntax required.

1 Answers1

0

Let us consider an example backend setup; most of important notes are in-line:

# projectroot/backend/app.py
import socketio
from fastapi import FastAPI

# Explicitly defined for easy comparison to frontend; normally use a .env file for this
SOCKETIO_MOUNTPOINT = "/bar"  # MUST START WITH A FORWARD SLASH
SOCKETIO_PATH = "foo"
# While some tutorials use "*" as the cors_allowed_origins value, this is not safe practice.
CLIENT_URLS = ["http://localhost:3000", "ws://localhost:3000"]

# Define the socket serverIO and application
sio = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins=CLIENT_URLS)
sio_app = socketio.ASGIApp(socketio_server=sio, socketio_path=SOCKETIO_PATH)

# Define the main fastapi application
app = FastAPI()

# Must mount the socketio application to a mountpoint in other to use socketio paths
# other then the default "socket.io"
app.mount(SOCKETIO_MOUNTPOINT, sio_app)


@sio.event
async def connect(sid, environ, auth):
    print(f"Connected to frontend with socket ID: {sid}")
    await sio.emit("message", f"Backend has connected to using socket ID: {sid}")


@sio.event
def disconnect(sid):
    print(f"Socket with ID {sid} has disconnected")


@sio.event
async def message(_, msg):
    print(f"Recieved the following message from the frontend: {msg}")
    await sio.emit("response", f"Responding from backend. Original message was: {msg}")

Typically if your current working directory is project_root, you'd activate your python environment in a terminal and launch the backend with uvicorn backend.app:app.

Here is an implementation of the frontend:

// projectroot/src/App.tsx
import { useEffect, useState } from "react";
import { io } from "socket.io-client";

const SERVER_PORT = 8000;
const SERVER_URL = `ws://127.0.0.1:${SERVER_PORT}/`;
// Explicitly defined for easy comparison to backend; normally use a .env file for this
const SOCKETIO_MOUNTPOINT = "/bar";
const SOCKETIO_PATH = "foo";

function App() {
  const [socket] = useState(
    io(SERVER_URL, {
      // if using only a socketio app without a mountpoint to a fastapi app,
      // the socketmountpoint variable should either be set to the default 
      // mountpoint "/" or "/" should be prefixed to the socketio path (i.e. below commented out)
      path: `${SOCKETIO_MOUNTPOINT}/${SOCKETIO_PATH}`,
      // path: `/${SOCKETIOPATH}`, // For this socketio-only scenario NOTE THE FORWARD SLASH prefix
      autoConnect: false, // For demo purposes as we manually connect/disconnect
    })
  );
  const [isConnected, setIsConnected] = useState(false); // socket.connected not always accurate; use a useState

  useEffect(() => {
    socket.on("connect", () => setIsConnected(true));
    socket.on("disconnect", () => setIsConnected(false));
    socket.on("response", response => console.log(response));

    // Clean-up
    return () => {
      socket.removeAllListeners("connect");
      socket.removeAllListeners("disconnect");
      socket.removeAllListeners("response");
    };
  }, [socket]);

  return (
    <div className="App">
      <button onClick={() => socket.connect()}>Connect</button>
      <button onClick={() => socket.disconnect()}>Disconnect</button>
      {isConnected && (
        <input placeholder="You can now send messages" onChange={e => socket.emit("message", e.target.value)} />
      )}
    </div>
  );
}

export default App;

So, the crux of the matter is that in the client's SocketIO configuration, the mountpoint and the socketio path must be joined for the path property when defining a socket/manager/etc.

Note that when there is no mounting involved (i.e. just a python socketio server), there still is a mount point. Hence the formula will still remain as mountpoint/socketiopath, with the forward slash still being present as a character before socketiopath.