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

Golang无法升级连接至WebSocket:Connection头无'upgrade'令牌

解决WebSocket连接升级错误:'upgrade' token not found in 'Connection' header

问题核心原因

你的代码存在三个关键问题导致协议升级失败:

  1. 路由逻辑混淆:同一个/chat/路由同时处理HTTP页面渲染和WebSocket连接请求,当用户通过浏览器访问页面时,普通HTTP请求会触发WebSocket升级逻辑,而这类请求不包含WebSocket握手所需的Connection: Upgrade头,直接导致错误。
  2. 非法响应写入:在调用upgrader.Upgrade完成协议切换后,你又执行了Tmpl.ExecuteTemplate,这会试图往已经完成WebSocket升级的响应中写入HTTP内容,违反了WebSocket协议规范。
  3. 路径不匹配:前端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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 22:22:05