WebSocket握手阶段返回401时,浏览器因响应缺失WWW-Authenticate头、响应体为空或校验逻辑未在升级前执行,导致onerror/onclose均不触发,readyState卡在0;需服务端在WebSocketServerProtocolHandler前拦截请求,返回含WWW-Authenticate和非空响应体的完整401,并前端配合超时+readyState判断。

浏览器对 WebSocket 握手阶段返回的 401 响应基本“视而不见”——不是你代码没发出去,而是浏览器压根不把它当错误处理。关键得在 HTTP 握手完成前、连接升级前,就让 401 真正生效。
WebSocket 握手时返回 401,为什么前端 onerror/onclose 拿不到原因?
因为 WebSocket 连接建立分两步:先是 HTTP GET 握手请求,成功后才升级为 ws 协议。如果服务器在握手响应中返回 401,但响应体为空、缺少必要 header,浏览器会静默失败,既不触发 socket.onerror,也不进 socket.onclose 的 event.reason ——它连“连接尝试过”都不承认。
常见错误现象:
- 前端
new WebSocket(...)后无任何回调触发,控制台也无报错 - 抓包看到 HTTP 401 响应,但状态码被忽略,连接直接断开
-
socket.readyState停留在0(CONNECTING),永远不会变成3(CLOSED)
根本原因:浏览器只认符合 RFC 7235 的完整 401 响应,尤其是必须带 WWW-Authenticate 头,且不能在 upgrade 已发生后才发 401。
Netty 中正确返回 401 的三个硬性条件
要在握手阶段让浏览器真正识别 401,Netty 服务端必须同时满足:
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
- 在
WebSocketServerProtocolHandler介入前拦截原始FullHttpRequest,不能等协议升级完成再判断 - 响应必须包含
WWW-Authenticate头,例如:WWW-Authenticate: Bearer realm="api"(即使你不用 Basic/Bearer,也得有) - 响应体不能为空,
Content-Length必须与实际字节一致;推荐写一句明文,如"Invalid or expired token"
示例关键片段:
if (!validateToken(request)) {
FullHttpResponse resp = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.UNAUTHORIZED);
ByteBuf content = Unpooled.copiedBuffer("Invalid token", CharsetUtil.UTF_8);
resp.content().writeBytes(content);
resp.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
resp.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
resp.headers().set(HttpHeaderNames.WWW_AUTHENTICATE, "Bearer realm=\"ws\"");
ctx.writeAndFlush(resp).addListener(ChannelFutureListener.CLOSE);
ReferenceCountUtil.release(request);
return;
}
前端如何可靠捕获并区分 401 握手失败?
仅靠 onerror 不够,它在握手失败时不一定触发。更稳的方式是结合超时 + readyState 判断:
- 不要依赖
onerror获取具体错误码——它在握手失败时经常不调用 - 监听
onopen,若 3–5 秒内未触发,且readyState === 0,基本可判定握手被拒绝(大概率是 401/403) - 在
onclose中检查event.code:浏览器对握手失败的 close code 通常为1006(abnormal closure),不是 401;所以不能靠它反推状态码 - 真要定位问题,必须配合后端日志或抓包(如 Chrome DevTools → Network → Filter “ws” → 查看 Initiator 对应的 HTTP 请求)
简单兜底逻辑:
const socket = new WebSocket(url);
let handshakeTimeout = setTimeout(() => {
if (socket.readyState === WebSocket.CONNECTING) {
console.warn("WebSocket handshake likely failed (e.g., 401)");
// 此处可重试、跳转登录页、提示 token 问题
}
}, 4000);
socket.onopen = () => clearTimeout(handshakeTimeout);
为什么加了 WWW-Authenticate 头还是没用?
两个最常踩的坑:
-
WebSocketServerProtocolHandler配置了checkStartsWith或路径匹配,导致你的拦截逻辑没走到——确保你在 pipeline 最前面加自定义 handler,比如channel.pipeline().addFirst("auth-handler", new AuthHandshakeHandler()) - 客户端 URL 里带了
token=xxx查询参数,但服务器解析时没做 URL decode,导致 token 中含%20等被截断失效,验证自然失败;务必用QueryStringDecoder解析参数
复杂点在于:401 是 HTTP 层语义,而 WebSocket 是协议升级后的通道。一旦升级完成,再发 401 就只是普通消息,浏览器不会重走认证流程。所以所有身份校验,必须卡死在 upgrade request 到达的那一刻。

















