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

为何TCP连接关闭时,io.ktor.websocket.WebSocketSession.send抛出CancellationException?

Ktor WebSocket send抛出CancellationException的问题处理

问题背景

当WebSocket会话因TCP连接断开或对等端主动关闭时,io.ktor.websocket.WebSocketSession.send方法会抛出CancellationException,而非更直观的ChannelClosedException类异常。这导致业务代码难以区分用户主动取消协程(比如调用scope.cancel()、withTimeout触发)和连接异常导致的发送失败,既增加了异常处理复杂度,也可能让真实发送故障被忽略。

设计原因

Ktor的WebSocket底层基于协程Channel实现:当连接关闭时,发送通道(WebSocketSession.outgoing)会被取消,而send方法本质是向该通道发送数据,因此会遵循协程Channel的语义抛出CancellationException。这种设计将连接中断视为协程上下文的取消信号,目的是自动终止相关协程任务、避免资源泄漏,但确实给业务层面的异常区分带来了困扰。

哪些函数会抛出此类CancellationException?

除了你列出的主动取消操作,Ktor中所有依赖WebSocket通道的操作,在连接关闭/通道取消时都会抛出CancellationException,例如:

  • WebSocketSession.receive():尝试接收消息时连接已关闭
  • WebSocketSession.outgoing.send():直接操作发送通道时(和send方法等效)
  • 基于WebSocket通道的迭代器操作(如for (frame in incoming))

解决方案

要区分用户主动取消和连接异常导致的发送失败,可以通过以下方式处理:

方案1:封装安全发送方法

扩展WebSocketSession,捕获CancellationException并检查通道状态,重新抛出明确的异常:

import io.ktor.websocket.*
import kotlinx.coroutines.channels.ChannelClosedException

suspend fun WebSocketSession.sendSafe(frame: Frame) {
    try {
        send(frame)
    } catch (e: CancellationException) {
        // 检查发送通道是否因连接关闭而关闭
        if (outgoing.isClosedForSend) {
            throw ChannelClosedException("WebSocket connection closed during send", e)
        } else {
            // 确认为用户主动取消,重新抛出原异常
            throw e
        }
    }
}

方案2:修改异常处理逻辑

在你的测试代码中,使用封装后的sendSafe,即可明确区分两种场景:

@Test
fun testChannelSendCatchCancellationException(): Unit = runBlocking {
    val server = MockWebServer()
    server.start()
    val serverUrl = server.url("/").toString().replaceFirst("http", "ws")
    println(serverUrl)

    val response = MockResponse().withWebSocketUpgrade(object : WebSocketListener() {
        override fun onOpen(webSocket: WebSocket, response: Response) {
            val request = server.takeRequest()
            println(request)
            webSocket.close(3000, "goodbye")
        }
    })
    server.enqueue(response)

    val session = HttpClient { install(WebSockets) }.webSocketSession(serverUrl)
    launch {
        try {
            delay(500L)
            try {
                session.sendSafe(Frame.Text("Hello, world"))
                println("Send success")
                // 发送成功后的业务逻辑
            } catch (e: ChannelClosedException) {
                println("Send failed due to connection closed: $e")
                // 连接关闭的故障处理逻辑
            } catch (e: CancellationException) {
                println("Coroutine cancelled by user: $e")
                // 用户主动取消的处理逻辑
                throw e
            } catch (e: Exception) {
                println("Unexpected send error: $e")
            }
        } catch (e: CancellationException) {
            println("Coroutine cancelled at outer scope: $e")
            throw e
        }
    }
    delay(200L)
    server.close()
    delay(200L)
}

方案3:利用协程取消原因(可选)

如果不需要严格的异常类型区分,可以通过检查CancellationException的message判断,但这种方式依赖框架的message格式,存在兼容性风险:

catch (e: CancellationException) {
    if (e.message?.contains("Channel was cancelled") == true) {
        // 连接关闭导致的发送失败
    } else {
        // 用户主动取消
    }
}

内容的提问来源于stack exchange,提问作者progquester

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 08:40:21