Frontend unable to connect to Render backend with HTTP and Websockets

Stack:
Frontend: Next.js
Backend: Express/Node.js

const express = require('express');
const { createClient } = require('@supabase/supabase-js');
const cors = require('cors');
const http = require('http');
const { WebSocketServer } = require('ws');
const WebSocket = require('ws');
global.WebSocket = WebSocket; // so I can use in my http routes

// Create the Express app
const app = express();

// Create an HTTP server from the Express app
const server = http.createServer(app);

// Create WebSocket server
const wss = new WebSocketServer({ noServer: true });

// Database stuff

app.use(cors({
  origin: '*',   // or '*' for all origins
  methods: ['GET', 'POST'],
}));
app.use(express.json());

// Routes and other stuff

// Store connected clients and their rooms
const clients = new Map();
const rooms = new Map();

// WebSocket connection handling
wss.on('connection', (ws, req) => {
  const id = Math.random().toString(36).substring(2, 10);
  console.log('A user connected:', id);
  
  // Store client with ID
  clients.set(ws, { id });
  
  // Send initial connection confirmation
  ws.send(JSON.stringify({
    type: 'connected',
    id
  }));
  
  // Handle messages
  ws.on('message', (message) => {
    try {
      const data = JSON.parse(message.toString());
      console.log(`Received message from ${id}:`, data.type);
      
      // Handle different message types
      switch (data.type) {
        case 'joinDraft':
          const draftId = data.draftId;
          // Add client to room
          if (!rooms.has(draftId)) {
            rooms.set(draftId, new Set());
          }
          rooms.get(draftId).add(ws);
          clients.get(ws).room = draftId;
          console.log(`Client ${id} joined draft room: ${draftId}`);
          
          // Send confirmation
          ws.send(JSON.stringify({
            type: 'joinedDraft',
            draftId,
            clientCount: rooms.get(draftId).size
          }));
          
          // Notify others in the room
          broadcastToRoom(draftId, {
            type: 'userJoined',
            clientId: id,
            clientCount: rooms.get(draftId).size
          }, ws);
          break;
          
        case 'leaveDraft':
          const roomToLeave = clients.get(ws).room;
          if (roomToLeave && rooms.has(roomToLeave)) {
            const clientCount = rooms.get(roomToLeave).size - 1;
            
            // Notify others before removing
            broadcastToRoom(roomToLeave, {
              type: 'userLeft',
              clientId: id,
              clientCount: clientCount
            }, ws);
            
            rooms.get(roomToLeave).delete(ws);
            console.log(`Client ${id} left draft room: ${roomToLeave}`);
            
            // Clean up empty rooms
            if (rooms.get(roomToLeave).size === 0) {
              rooms.delete(roomToLeave);
              console.log(`Room ${roomToLeave} deleted (empty)`);
            }
            
            // Remove room from client info
            delete clients.get(ws).room;
          }
          break;
          
        case 'ping':
          ws.send(JSON.stringify({
            type: 'pong',
            timestamp: Date.now()
          }));
          break;
          
        default:
          // Broadcast to room if client is in a room
          const room = clients.get(ws).room;
          if (room && rooms.has(room)) {
            broadcastToRoom(room, data, ws);
          }
      }
    } catch (error) {
      console.error('Error processing message:', error);
      ws.send(JSON.stringify({
        type: 'error',
        message: 'Failed to process message'
      }));
    }
  });
  
  // Handle disconnection
  ws.on('close', () => {
    const clientInfo = clients.get(ws);
    console.log('User disconnected:', clientInfo?.id);
    
    // Remove from rooms and notify others
    if (clientInfo?.room && rooms.has(clientInfo.room)) {
      const roomId = clientInfo.room;
      const clientCount = rooms.get(roomId).size - 1;
      
      // Notify others in the room
      broadcastToRoom(roomId, {
        type: 'userDisconnected',
        clientId: clientInfo.id,
        clientCount: clientCount
      }, ws);
      
      rooms.get(roomId).delete(ws);
      
      // Clean up empty rooms
      if (rooms.get(roomId).size === 0) {
        rooms.delete(roomId);
        console.log(`Room ${roomId} deleted (empty after disconnect)`);
      }
    }
    
    // Remove client
    clients.delete(ws);
  });
  
  // Handle errors
  ws.on('error', (error) => {
    console.error(`WebSocket error for client ${id}:`, error);
  });
});

// Helper function to broadcast to a room (excluding sender if provided)
function broadcastToRoom(roomId, message, excludeClient = null) {
  if (rooms.has(roomId)) {
    const roomClients = rooms.get(roomId);
    
    roomClients.forEach((client) => {
      if (client !== excludeClient && client.readyState === WebSocket.OPEN) {
        try {
          client.send(JSON.stringify(message));
        } catch (error) {
          console.error('Error sending message to client:', error);
        }
      }
    });
    
    return true;
  }
  return false;
}

// Make broadcast function available to routes
app.set('broadcastToRoom', broadcastToRoom);

// Ping clients periodically to keep connections alive
setInterval(() => {
  wss.clients.forEach((client) => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(JSON.stringify({ type: 'heartbeat', timestamp: Date.now() }));
    }
  });
}, 30000); // Every 30 seconds

const port = process.env.PORT || 3000;

server.on('upgrade', (req, socket, head) => {
  wss.handleUpgrade(req, socket, head, (ws) => {
    wss.emit('connection', ws, req)
  })
})

server.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

Frontend:

useEffect(() => {
      
    // Using the correct endpoint with the WebSocket path
    const ws = new WebSocket('wss://*myurlhere*.onrender.com/');
    
    // Add connection timeout handling
    const connectionTimeout = setTimeout(() => {
      if (ws.readyState !== WebSocket.OPEN) {
        console.error('WebSocket connection timed out');
        ws.close();
      }
    }, 10000); // 10 second timeout
    
    ws.onopen = () => {
      console.log('Connected to WebSocket server');
      clearTimeout(connectionTimeout);
      setIsConnected(true);
      setSocket(ws);
      
      // Send a ping to keep the connection alive
      const pingInterval = setInterval(() => {
        if (ws.readyState === WebSocket.OPEN) {
          ws.send(JSON.stringify({ type: 'ping' }));
        }
      }, 30000); // Every 30 seconds
      
      // Clear interval on component unmount
      return () => clearInterval(pingInterval);
    };
  
    ws.onmessage = (event) => {
      try {
        const data = JSON.parse(event.data);
        console.log('Received message:', data);
        // Handle different message types here
      } catch (error) {
        console.error('Error parsing message:', error);
      }
    };
  
    ws.onerror = (error) => {
      console.error('WebSocket error:', error);
      console.log('Connection state:', ws.readyState);
      console.error('Connection failed. Please check if the server is running and accessible.');
      // Try reconnecting with a fallback URL if needed
      // attemptReconnect();
    };
  
    ws.onclose = (event) => {
      console.log(`WebSocket closed: ${event.code} ${event.reason}`);
      setIsConnected(false);
      clearTimeout(connectionTimeout);
    };
  
    return () => {
      clearTimeout(connectionTimeout);
      if (ws.readyState === WebSocket.OPEN) {
        ws.close();
      }
    };
  }, []);

Connecting to the render backend works when I use npx wscat -c wss:.. but as soon as I use my frontend, it can’t connect to the websocket server. HTTP works fine. Anybody know what the issue might be?

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.