Golang无法升级连接至WebSocket:Connection头无'upgrade'令牌
解决WebSocket连接升级错误:'upgrade' token not found in 'Connection' header
问题核心原因
你的代码存在三个关键问题导致协议升级失败:
- 路由逻辑混淆:同一个
/chat/路由同时处理HTTP页面渲染和WebSocket连接请求,当用户通过浏览器访问页面时,普通HTTP请求会触发WebSocket升级逻辑,而这类请求不包含WebSocket握手所需的Connection: Upgrade头,直接导致错误。 - 非法响应写入:在调用
upgrader.Upgrade完成协议切换后,你又执行了Tmpl.ExecuteTemplate,这会试图往已经完成WebSocket升级的响应中写入HTTP内容,违反了WebSocket协议规范。 - 路径不匹配:前端WebSocket连接路径为
/chat,但后端路由是/chat/(末尾带斜杠),导致请求可能无法正确路由到处理函数。
修复方案
1. 拆分路由职责
将页面渲染和WebSocket连接处理拆分为两个独立路由:
- 用
/chat路由负责渲染聊天页面 - 用
/ws路由专门处理WebSocket连接
2. 移除WebSocket升级后的非法响应写入
完成upgrader.Upgrade后,禁止再向ResponseWriter写入任何HTTP内容。
3. 统一前后端路径
调整前端WebSocket连接路径,与后端WebSocket路由保持一致。
4. 配置WebSocket升级器的跨域检查
开发环境下可临时允许所有Origin,生产环境需根据实际域名配置。
修改后的代码示例
后端代码
import ( "fmt" "net/http" "sync" "text/template" "github.com/gorilla/websocket" ) // 配置WebSocket升级器 var upgrader = websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true // 开发环境允许所有来源,生产环境替换为具体的Origin校验逻辑 }, } var ( websocketConn = make(map[*websocket.Conn]bool) websocketMutex sync.Mutex Tmpl *template.Template ) // 渲染聊天页面的路由处理函数 func chatPage(w http.ResponseWriter, r *http.Request) { funcName := logger.GetFuncName() logger.InfoHttp(r.URL.Path, r.Method, funcName) token, err := auth.GetToken(r) if err != nil { http.Redirect(w, r, "/", 302) return } userId, err := auth.ValidateToken(token) if err != nil { logger.Error("Couldn't validate token", err, funcName) http.Redirect(w, r, "/", 302) return } user, err := userController.GetUserById(userId) if err != nil { logger.Error(fmt.Sprintf("Couldn't get user by id(%v)", userId), err, funcName) http.Redirect(w, r, "/", 302) return } // 仅在HTTP页面请求时渲染模板 Tmpl.ExecuteTemplate(w, "index.html", user) } // 处理WebSocket连接的路由处理函数 func chatWebSocket(w http.ResponseWriter, r *http.Request) { funcName := logger.GetFuncName() logger.InfoHttp(r.URL.Path, r.Method, funcName) // 验证用户token,确保WebSocket连接来自已登录用户 token, err := auth.GetToken(r) if err != nil { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } _, err = auth.ValidateToken(token) if err != nil { logger.Error("Couldn't validate token", err, funcName) http.Error(w, "Unauthorized", http.StatusUnauthorized) return } // 执行WebSocket升级 conn, err := upgrader.Upgrade(w, r, nil) if err != nil { logger.Error("Upgrade failed", err, funcName) http.Error(w, err.Error(), http.StatusInternalServerError) return } defer func() { conn.Close() websocketMutex.Lock() delete(websocketConn, conn) websocketMutex.Unlock() }() // 将连接加入全局管理 websocketMutex.Lock() websocketConn[conn] = true websocketMutex.Unlock() // 消息循环 for { mt, message, err := conn.ReadMessage() if err != nil || mt == websocket.CloseMessage { logger.Error("Couldn't read message", err, funcName) break } fmt.Println(string(message)) // 广播消息到所有连接 websocketMutex.Lock() for connection := range websocketConn { err = connection.WriteMessage(websocket.TextMessage, message) if err != nil { connection.Close() logger.Error("Couldn't write message to connection", err, funcName) delete(websocketConn, connection) } } websocketMutex.Unlock() } } func StartServer() { var err error Tmpl, err = template.ParseGlob("templates/*.html") if err != nil { funcName := logger.GetFuncName() logger.Error("Couldn't parse template", err, funcName) } http.HandleFunc("/chat", chatPage) http.HandleFunc("/ws", chatWebSocket) err = http.ListenAndServe(":8080", nil) if err != nil { funcName := logger.GetFuncName() logger.Error("Couldn't start the server", err, funcName) } }
前端代码
<body> <form> <input type="text" id="message" placeholder="Type your message here" /> <button type="submit">Send</button> </form> <div id="chat"></div> <!-- 补充缺失的chat元素 --> <script> // 连接到专门的WebSocket路由 const websocket = new WebSocket('ws://localhost:8080/ws'); const chat = document.getElementById('chat'); const form = document.querySelector('form'); const input = document.getElementById('message'); websocket.onopen = (event) => { console.log('WebSocket connected'); }; websocket.onmessage = (event) => { try { const message = JSON.parse(event.data); chat.innerHTML += `<p>${message.username}: ${message.text}</p>`; } catch (e) { console.error('Failed to parse message:', e); } }; websocket.onerror = (error) => { console.error('WebSocket error:', error); }; websocket.onclose = (event) => { console.log('WebSocket closed:', event.code, event.reason); }; form.addEventListener('submit', (event) => { event.preventDefault(); const message = input.value.trim(); if (!message) return; const username = 'Alice'; // 替换为登录用户的用户名 const data = JSON.stringify({ username, text: message }); websocket.send(data); input.value = ''; }); </script> </body>
额外注意事项
- 生产环境中,
CheckOrigin必须配置严格的Origin校验,避免跨域安全问题。 - 全局连接管理的锁要正确使用,避免并发读写冲突。
- 前端要处理WebSocket的错误和关闭事件,提升用户体验。
- 确保用户认证逻辑在WebSocket连接时也执行,防止未授权连接。
内容的提问来源于stack exchange,提问作者Folium
相关产品推荐
相关产品推荐

