You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

部署到Render的WebSocket通知失效,本地运行正常求助

WebSocket生产环境(Render平台)无法接收通知问题排查与修复

问题描述

本地使用http://localhost:5000时前端通知功能正常,但部署到Render平台后,前端无法通过WebSocket接收通知。

前端代码(原)

import {
  addNotification,
  clearNotifications,
} from '@/features/notification/notificationSlice';
import { FC, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import socketIo from 'socket.io-client';

interface SocketComponentProps {
  children: React.ReactNode;
}

const SocketComponent: FC<SocketComponentProps> = ({ children }) => {
  const dispatch = useDispatch();

  const { userInfo } = useSelector((state: any) => state.user);
  const currentUserId = userInfo?.user?.id;
  const messages = useSelector((state: any) => state.chat.conversations);

  useEffect(() => {
    const socket = socketIo('https://polis-backend-test.onrender.com', {
      transports: ['websocket'],
    });

    socket.on('connect', () => {
      console.log('Connected to WebSocket server:', socket.id);
    });

    socket.emit('joinNotifications', { userId: currentUserId });
    socket.on('notification', (notification) => {
      console.log('Notification received:', notification);
      dispatch(addNotification(notification));
    });

    return () => {
      socket.disconnect();
      socket.off('notification');
    };
  }, [messages, dispatch]);

  return <> {children} </>;
};

export default SocketComponent;

后端代码(原)

const { Server } = require('socket.io');

const app = express();

app.use('../public', express.static('public'));
app.use(bodyParser.json());
app.use(cors());

app.use(
  '/graphql',
  graphqlHTTP(async (req, res) => {
    const user = await authMiddleware(req, res);
    console.log({ user });
    return {
      schema,
      graphiql: true,
      context: {
        User,
        Property,
        currentUser: user,
        io,
      },
    };
  }),
);
const server = http.createServer(app);
const io = new Server(server, {
  cors: {
    origin: '*',
    methods: ['GET', 'POST'],
  },
});

io.on('connection', (socket) => {
  socket.on('joinNotifications', ({ userId }) => {
    socket.join(userId);
  });
});

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

if (!module.parent) {
  server.listen(port, () => {
    logger.info(`server running on port ${port}`);
  });
}

module.exports = server;

// 通知处理函数
const sendNotification = (io, userId, title, body, url = null) => {
  try {
    const notification = {
      title,
      body,
      timestamp: new Date(),
      read: false,
    };

    if (url) {
      notification.url = url;
    }

    io.to(userId).emit('notification', notification);
    console.log({ notification });
  } catch (error) {
    console.error(`Error sending notification to user ${userId}:`, error);
  }
};

module.exports = sendNotification;

// GraphQL突变
requestApproval: {
      type: ApprovalType,
      args: {
        bookingId: { type: GraphQLNonNull(GraphQLID) },
        userId: { type: GraphQLNonNull(GraphQLID) },
        message: { type: GraphQLString },
      },
      resolve: async (parent, args, context) => {
        const { io } = context;

        const approval = new Approval({
          booking: args.bookingId,
          user: args.userId,
          message: args.message,
        });

        const createdApproval = await approval.save();
        const booking = await Booking.findById(args.bookingId).populate('user property');
        const host = await User.findById(booking.property.user);
        const hostEmail = host.email;

        const baseUrl = process.env.NODE_ENV === 'production' ? process.env.PROD_URL : process.env.DEV_URL;
        const manageShortletUrl = `${baseUrl}/manage-shortlets`;
        const bookingDetails = {
          ...booking._doc,
          startDate: moment(booking.startDate).format('DD MMMM YYYY'),
          endDate: moment(booking.endDate).format('DD MMMM YYYY'),
        };
        const hostMail = {
          From: 'admin@polis.ng',
          To: hostEmail,
          Subject: `New Booking Request for ${booking.property.title}`,
          HtmlBody: BOOKING_REQUEST_TEMPLATE(host, bookingDetails, manageShortletUrl),
        };

        const notificationLink = `/review-booking/${booking.property._id.toString()}`;

        try {
          const result = await client.sendEmail(hostMail);

          sendNotification(
            io,
            host._id.toString(),
            'Booking Request',
            `You have a new booking request for your property ${booking.property.title}.`,
            notificationLink,
          );

          console.log('sent');
        } catch (error) {
          console.error('Error sending email to host:', error);
        }

        return createdApproval;
      },
    },

问题分析与修复方案

前端修复

  1. 确保用户ID存在后再建立连接:原代码可能在currentUserId未加载完成时就发送joinNotifications事件,导致无法加入目标房间。
  2. 调整传输方式:Render平台的反向代理推荐先使用polling再升级到websocket,避免纯websocket连接失败。
  3. 优化依赖数组:移除messages依赖,避免聊天消息变化导致频繁重连。
  4. 添加错误监听:便于排查连接失败原因。

修改后的前端代码:

import {
  addNotification,
  clearNotifications,
} from '@/features/notification/notificationSlice';
import { FC, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import socketIo from 'socket.io-client';

interface SocketComponentProps {
  children: React.ReactNode;
}

const SocketComponent: FC<SocketComponentProps> = ({ children }) => {
  const dispatch = useDispatch();
  const { userInfo } = useSelector((state: any) => state.user);
  const currentUserId = userInfo?.user?.id;

  useEffect(() => {
    if (!currentUserId) return;

    const socket = socketIo('https://polis-backend-test.onrender.com', {
      transports: ['polling', 'websocket'],
      withCredentials: true,
    });

    socket.on('connect_error', (error) => {
      console.error('WebSocket连接错误:', error);
    });

    socket.on('connect', () => {
      console.log('Connected to WebSocket server:', socket.id);
      socket.emit('joinNotifications', { userId: currentUserId });
    });

    socket.on('notification', (notification) => {
      console.log('Notification received:', notification);
      dispatch(addNotification(notification));
    });

    return () => {
      socket.disconnect();
      socket.off('notification');
      socket.off('connect');
      socket.off('connect_error');
    };
  }, [currentUserId, dispatch]);

  return <> {children} </>;
};

export default SocketComponent;

后端修复

  1. 完善CORS配置:生产环境禁用通配符origin,指定具体前端域名,避免Socket.io兼容性问题。
  2. 确保Socket.io实例初始化顺序:先创建server和io,再挂载GraphQL路由,避免上下文引用未初始化的io。
  3. 添加连接日志:便于排查用户加入房间的状态。

修改后的后端代码:

const { Server } = require('socket.io');
const http = require('http');
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const { graphqlHTTP } = require('express-graphql');

const app = express();

app.use('/public', express.static('public'));
app.use(bodyParser.json());

// 配置允许的域名
const allowedOrigins = process.env.NODE_ENV === 'production' 
  ? [process.env.FRONTEND_URL]
  : ['http://localhost:3000'];

app.use(cors({
  origin: allowedOrigins,
  methods: ['GET', 'POST'],
  credentials: true
}));

const server = http.createServer(app);
const io = new Server(server, {
  cors: {
    origin: allowedOrigins,
    methods: ['GET', 'POST'],
    credentials: true
  }
});

io.on('connection', (socket) => {
  console.log('新Socket连接:', socket.id);
  
  socket.on('joinNotifications', ({ userId }) => {
    if (!userId) {
      console.warn('加入通知房间缺少用户ID');
      return;
    }
    socket.join(userId);
    console.log(`用户 ${userId} 加入通知房间`);
  });

  socket.on('disconnect', () => {
    console.log('Socket连接断开:', socket.id);
  });
});

app.use(
  '/graphql',
  graphqlHTTP(async (req, res) => {
    const user = await authMiddleware(req, res);
    console.log({ user });
    return {
      schema,
      graphiql: process.env.NODE_ENV !== 'production',
      context: {
        User,
        Property,
        currentUser: user,
        io,
      },
    };
  }),
);

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

if (!module.parent) {
  server.listen(port, () => {
    logger.info(`server running on port ${port}`);
  });
}

module.exports = server;

额外排查步骤

  1. 查看Render控制台日志:确认Socket连接、用户加入房间、通知发送的日志是否正常输出。
  2. 前端Network调试:在浏览器开发者工具中查看WebSocket连接状态,是否有错误响应。
  3. 验证通知逻辑:确认GraphQL突变中host._id.toString()是否正确,sendNotification是否被调用。

内容的提问来源于stack exchange,提问作者Favour Samson

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 03:52:02