部署到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; }, },
问题分析与修复方案
前端修复
- 确保用户ID存在后再建立连接:原代码可能在
currentUserId未加载完成时就发送joinNotifications事件,导致无法加入目标房间。 - 调整传输方式:Render平台的反向代理推荐先使用
polling再升级到websocket,避免纯websocket连接失败。 - 优化依赖数组:移除
messages依赖,避免聊天消息变化导致频繁重连。 - 添加错误监听:便于排查连接失败原因。
修改后的前端代码:
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;
后端修复
- 完善CORS配置:生产环境禁用通配符
origin,指定具体前端域名,避免Socket.io兼容性问题。 - 确保Socket.io实例初始化顺序:先创建
server和io,再挂载GraphQL路由,避免上下文引用未初始化的io。 - 添加连接日志:便于排查用户加入房间的状态。
修改后的后端代码:
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;
额外排查步骤
- 查看Render控制台日志:确认Socket连接、用户加入房间、通知发送的日志是否正常输出。
- 前端Network调试:在浏览器开发者工具中查看WebSocket连接状态,是否有错误响应。
- 验证通知逻辑:确认GraphQL突变中
host._id.toString()是否正确,sendNotification是否被调用。
内容的提问来源于stack exchange,提问作者Favour Samson
相关产品推荐
相关产品推荐

