调用part.readbytes()前必须先确认part是partdata.binaryitem,否则会抛classcastexception;formitem需用value获取字符串;readbytes()会全量加载内存,大文件易oom,应配合maxrequestsize限制及streamprovider流式处理。

multipart请求中调用part.readBytes()前必须先检查part is PartData.BinaryItem
直接对任意PartData调用readBytes()会抛ClassCastException,因为只有BinaryItem才支持该方法。表单里的文本字段(如name="username")是PartData.FormItem,它没有readBytes()——你得用formItem.value取字符串。
常见错误现象:java.lang.ClassCastException: class io.ktor.http.content.PartData$FormItem cannot be cast to class io.ktor.http.content.PartData$BinaryItem
- 用
when分支区分处理类型,不要用is硬转 -
BinaryItem对应文件上传;FormItem对应普通字段;Empty可忽略 - 如果只关心文件,跳过非
BinaryItem即可,不用报错中断整个请求
part.readBytes()会把整个文件加载进内存,大文件容易OOM
readBytes()是同步阻塞方法,它会一次性把InputStream读完并返回ByteArray。上传100MB文件就会分配100MB堆内存,Ktor默认不设限制,生产环境极易触发OutOfMemoryError。
- 务必配合
application.conf里ktor.http.maxRequestSize限制总请求大小 - 对单个
BinaryItem,可用part.originalFileName和part.contentType做前置校验(比如拒绝.exe或超5MB的图片) - 真正要存大文件,改用
part.streamProvider()获取InputStream,再流式写入磁盘或对象存储
完整示例:安全读取上传文件并保存到本地
routing {
post("/upload") {
val multipart = call.receiveMultipart()
multipart.forEachPart { part ->
when (part) {
is PartData.BinaryItem -> {
// 1. 校验文件名和大小
val filename = part.originalFileName ?: "unknown"
if (filename.endsWith(".zip") && part.getSize() > 10 * 1024 * 1024) {
throw BadRequestException("File too large: $filename")
}
// 2. 同步读取字节(仅适用于小文件)
val bytes = part.readBytes()
// 3. 写入本地(注意路径安全,别直接拼接originalFileName)
val safeName = filename.replace(Regex("[^a-zA-Z0-9._-]"), "_")
File("uploads/$safeName").writeBytes(bytes)
}
is PartData.FormItem -> {
println("Form field: ${part.name} = ${part.value}")
}
else -> part.dispose()
}
}
call.respondText("OK")
}
}
为什么part.getSize()有时返回-1?
getSize()依赖HTTP头里的Content-Length,但multipart规范不强制客户端发送该字段,尤其浏览器通过<input type="file">上传时经常缺失。此时getSize()返回-1,不能用来判断大小。
- 别用
getSize() == -1当“是大文件”的依据 - 真要限流,得靠
streamProvider()边读边累计字节数,或依赖反向代理(如Nginx)提前拦截超大请求 - Ktor 2.3+ 支持
maxUploadSize配置项,但它是全局的,无法按路由或字段粒度控制
multipart边界解析、字符编码、临时文件清理这些细节藏得深,但实际出问题往往就卡在readBytes()那一行——先判类型,再看大小,最后动文件。










