WebSocket Connection Fails with “WebSocket is closed before the connection is established” Error

I’m having trouble with establishing a WebSocket connection in my React application. Despite my backend WebSocket server working as expected (verified using Postman), my frontend consistently fails to connect. I’m receiving the following error in the browser console:

SocketClient.ts:36 WebSocket connection to 'ws://localhost:3000/socket.io/?EIO=4&transport=websocket' failed: WebSocket is closed before the connection is established.

Here’s the relevant setup for my frontend and backend.

Frontend Code
1 . src/services/socketClient.ts

import { io, Socket } from "socket.io-client";

class SocketClient {
  private socket: Socket | null = null;

  connect(): Promise<void> {
    return new Promise((resolve, reject) => {
      if (!process.env.REACT_APP_SOCKET_HOST) {
        reject("Socket host is not defined.");
        return;
      }

      this.socket = io(process.env.REACT_APP_SOCKET_HOST as string, {
        transports: ["websocket"],
        reconnection: true,
      });

      this.socket.on("connect", () => resolve());
      this.socket.on("connect_error", (error) => {
        console.error("Socket connection error:", error);
        reject(error);
      });
    });
  }

  disconnect(): Promise<void> {
    return new Promise((resolve, reject) => {
      if (this.socket) {
        this.socket.disconnect();
        this.socket.once("disconnect", () => {
          this.socket = null;
          resolve();
        });
      } else {
        reject("No socket connection.");
      }
    });
  }

  emit(event: string, data: any): Promise<void> {
    return new Promise((resolve, reject) => {
      if (!this.socket) return reject("No socket connection.");

      this.socket.emit(event, data, (response: any) => {
        if (response?.error) {
          return reject(response.error);
        }
        resolve();
      });
    });
  }

  on(event: string, callback: (data: any) => void): void {
    if (!this.socket) throw new Error("No socket connection.");
    this.socket.on(event, callback);
  }
}

export default new SocketClient();

2 .src/redux/slices/socketClient/socketSlice.ts

import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import SocketClient from "../../../services/SocketClient";

const socketClient = SocketClient;

interface SocketState {
  connectionStatus:
    | "idle"
    | "connecting"
    | "connected"
    | "disconnected"
    | "failed"
    | "disconnecting";
}

const initialState: SocketState = {
  connectionStatus: "idle",
};

export const connectToSocket = createAsyncThunk("socket/connect", async () => {
  try {
    await socketClient.connect();
  } catch (error) {
    throw new Error("Failed to connect to the socket server");
  }
});

export const disconnectFromSocket = createAsyncThunk(
  "socket/disconnect",
  async () => {
    try {
      await socketClient.disconnect();
    } catch (error) {
      throw new Error("Failed to disconnect from the socket server");
    }
  }
);

const socketSlice = createSlice({
  name: "socket",
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder.addCase(connectToSocket.pending, (state) => {
      state.connectionStatus = "connecting";
    });
    builder.addCase(connectToSocket.fulfilled, (state) => {
      state.connectionStatus = "connected";
    });
    builder.addCase(connectToSocket.rejected, (state, action) => {
      state.connectionStatus = "failed";
      console.error("Socket connection error:", action.error.message);
    });
    builder.addCase(disconnectFromSocket.pending, (state) => {
      state.connectionStatus = "disconnecting";
    });
    builder.addCase(disconnectFromSocket.fulfilled, (state) => {
      state.connectionStatus = "disconnected";
    });
    builder.addCase(disconnectFromSocket.rejected, (state, action) => {
      state.connectionStatus = "failed";
      console.error("Socket disconnection error:", action.error.message);
    });
  },
});

export default socketSlice.reducer;
  1. src/App.tsx
import { BrowserRouter as Router } from "react-router-dom";
import AppRoutes from "./routes/Routes";
import { useDispatch } from "react-redux";
import { AppDispatch } from "./redux";
import { useEffect } from "react";
import {
  connectToSocket,
  disconnectFromSocket,
} from "./redux/slices/socketClient/socketSlice";

function App() {
  const dispatch = useDispatch<AppDispatch>();

  useEffect(() => {
    const connect = async () => {
      try {
        await dispatch(connectToSocket()).unwrap();
      } catch (error) {
        console.error("Socket connection error:", error);
      }
    };

    connect();

    return () => {
      dispatch(disconnectFromSocket());
    };
  }, [dispatch]);

  return (
    <Router>
      <AppRoutes />
    </Router>
  );
}

export default App;

Backend Code

  1. index.js

const mongoose = require('mongoose');
const app = require('./app');
const config = require('./config/config');
const logger = require('./config/logger');
const setupSocketServer = require('./services/socket.io');
const harvester = require('./services/processedDataServices/saveTodb.service');

let server;
mongoose.connect(config.mongoose.url, config.mongoose.options).then(() => {
  logger.info('Connected to MongoDB');
  server = app.listen(config.port, () => {
    logger.info(`Listening to port ${config.port}`);
    harvester.startOnchangeServices();
  });
  setupSocketServer(server);
});

const exitHandler = () => {
  if (server) {
    server.close(() => {
      logger.info('Server closed');
      process.exit(1);
    });
  } else {
    process.exit(1);
  }
};

const unexpectedErrorHandler = (error) => {
  logger.error(error);
  exitHandler();
};

process.on('uncaughtException', unexpectedErrorHandler);
process.on('unhandledRejection', unexpectedErrorHandler);

process.on('SIGTERM', () => {
  logger.info('SIGTERM received');
  if (server) {
    server.close();
  }
});
  1. services/socket.io.js
const socketIo = require('socket.io');
const logger = require('../config/logger');

module.exports = (server) => {
  const corsOptions = {
    origin: '*',
    methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
    credentials: true,
  };
  const io = socketIo(server, {
    cors: corsOptions,
  });

  io.on('connection', (socket) => {
    logger.info(`User connected: Socket ID - ${socket.id}`);

    // Handle disconnection
    socket.on('disconnect', () => {
      logger.info(`User disconnected: Socket ID - ${socket.id}`);
    });
  });
};

My frontend code is using socket.io-client to connect to the WebSocket server. The backend WebSocket server is working fine (verified using Postman).

What I’ve Tried:
Verified that the WebSocket server is up and running.
Checked the URL and environment variables.
Ensured the server code handles connections and disconnections correctly.

Any guidance on how to resolve this issue or what might be going wrong would be greatly appreciated!