Netty 4.2.19 的 WebSocket 关闭握手报 promise already done
服务端先发 close、对端再应答时,Netty 4.2.19 可能把旧关闭结果同步传给新的 promise,随后写关闭帧却收到已完成对象。核对完整父类调用链与实际 Netty 模块版本。
Netty 4.2.19.Final 服务端主动发送 WebSocket close frame,客户端回送自己的 close frame,接着日志出现:
DecoderException: java.lang.IllegalArgumentException: promise already done
若堆栈经过 WebSocketServerProtocolHandler.decode、WebSocketServerHandshaker.close,应检查关闭握手里的 promise 状态,不要先把它归为网络超时或客户端重复发了业务消息。
这里的触发顺序是服务端已经发起关闭、发送结果已完成,随后才收到对端的 close。单纯建立连接再由客户端首先关闭,不一定覆盖同一条路径。
newPromise 之后仍可能变成已完成
4.2.19 的服务端 handler 收到 close frame 时,确实会调用 ctx.newPromise()。但新对象随后还会经过 closeSent(promise),不能只凭“刚创建”就认定它仍未完成。
在 WebSocketProtocolHandler 中,如果已有先前的 closeSent,它会把那个 promise 的结果通过 listener 传给新 promise。先前结果已经完成时,这个级联可以立即完成新 promise。后面的 handshaker 又尝试拿它执行 write-and-flush,于是在校验时拒绝一个已经结束的写入 promise。
这种内部结果复用不表示 WebSocket 关闭状态码本身无效,也不适合靠增加 close 等待时间解决。
核对框架真正加载的 Netty 模块
先从完整堆栈定位类和实际 JAR,核对 netty-codec-http、相关 handler 与其他 Netty 模块的解析版本。Maven 工程可通过已有 wrapper 查看这一组依赖:
./mvnw dependency:tree -Dincludes=io.netty
由 Spring Boot、Reactor Netty 等上层平台管理依赖时,沿用平台兼容的调整方式。不要只替换一个 codec JAR,让它与其他 Netty 模块采用任意不同版本。
正式 4.2.18 的对应父类只更新 closeSent 引用,没有这条将旧结果级联到新 promise 的分支;4.2.19 增加了它。需要临时回退时,只能在项目确认允许的依赖组合中评估此前版本,并一起核对关闭超时和其他修补,不把旧版视为无需检查的长期方案。
不靠关闭协议处理来压住异常
禁用 handleCloseFrames 会把关闭帧的处理责任交给应用。若没有完整的 close 应答、超时、引用计数和连接结束逻辑,不能只加一个开关就认为已经恢复。
也不要把 promise already done 一律吞掉。这样可能隐藏关闭帧根本没有写出的情况,留下连接或缓冲区清理问题。需要采用后续补丁时,应核对正式发布工件或按团队既定方式构建经过审查的依赖;一个已合并的修复不能直接当作当前正式 4.2.19 已包含它。
验收应分别覆盖服务端先关闭、客户端先关闭,以及对端不应答的超时路径。检查关闭帧的状态与原因、channel 最终结束、相关缓冲区释放,而不只确认日志少了一条异常。业务若因断连需要恢复会话,仍沿用自己的重连与状态核对流程。


