Json.encodeToSink 返回了,大 JSON 响应末尾却没发完
自定义 Ktor 输出把通道包装为缓冲 Sink 后,Json.encodeToSink 不会自动 flush。区分借用资源与自有资源,在响应结束前送出尾部缓存。
小 JSON 正常,大响应却缺少结尾,客户端在解析时失败;服务端的 Json.encodeToSink() 已经返回,也没有记录编码异常。如果自定义 Ktor 输出把 ByteWriteChannel 包装成了缓冲的 kotlinx.io.Sink,先检查最后一段字节是否仍留在这个缓冲层。
encodeToSink 完成的是向给定 Sink 编码,不表示所有缓冲层都已刷新到下游。相关行为在 kotlinx.serialization 1.9.0 中就存在;正式 1.12.0 的 JSON I/O writer 释放方法仍是空操作,不会代替调用方刷新或关闭借来的 Sink。
这条场景针对应用自己的输出适配,不应据此宣称 Ktor 内置 ContentNegotiation 普遍丢响应。没有新增缓冲层,或错误发生在编码之前,需另查实际发送路径。
谁创建缓冲层,谁负责交代尾部字节
完整缓冲块可能在写入过程中逐步下传,最后不足一块的数据却仍留在内存里。于是“开始有输出”与“所有 JSON 已输出”可以同时呈现不同结果;响应大小变化,也可能让故障看起来偶发。
如果 Sink 是框架提供或底层资源仍由框架管理,不应随意调用 close() 结束整个通道。应按原适配器的所有权约定,在成功编码后、返回并结束响应之前刷新自己引入的缓冲层。
以下是接收已有 Sink 的编码辅助方法,项目需依赖 kotlinx-serialization-json-io 与匹配的 kotlinx-io。调用方负责在正确的响应生命周期和 I/O 执行位置调用它:
import kotlinx.io.Sink
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.io.encodeToSink
@OptIn(ExperimentalSerializationApi::class)
fun <T> writeJson(
json: Json,
serializer: SerializationStrategy<T>,
value: T,
sink: Sink
) {
json.encodeToSink(serializer, value, sink)
sink.flush()
}
flush() 会将缓冲数据交给底层 Sink,并要求底层刷新;它不等于客户端已确认接收,也不是磁盘持久化保证。这个方法不关闭借用资源,最终结束响应的动作仍由原框架入口负责。
如果文件 Sink 是应用自己创建、自己拥有的,可以用 use 管理关闭,让缓冲内容在正常关闭中完成发送;这与关闭框架借来的通道不同。不要同时让辅助函数与框架各自无条件关闭同一个资源。
编码失败时不能继续假装响应完整
不要把 flush() 简单放进 finally,无论编码是否成功都当作完整 JSON 发出。大载荷在抛异常前可能已经下传了一部分字节,缓冲输出也不能自动回滚;按框架规则取消或结束失败响应,并保留真实异常。
同样,收到已经关闭的 Sink 时,再 flush 可能抛 IllegalStateException;它反映调用生命周期错误,不应该通过吞掉异常把请求记为成功。应确认输出函数返回前缓冲仍处于可写状态,且没有其他任务并发结束它。
检查小响应、跨多个缓冲块的大响应、最后一块不足整块、编码中途失败与客户端取消。比较实际收到的完整 JSON 和业务条目,不只检查 200 或首段数据。保存原适配器与生命周期约定,撤回修改时一起恢复,避免把尾部丢失改成另一处提前关闭。


