KtorMiddleTechnical
Как Ktor поддерживает загрузку файлов через multipart?
Ktor обрабатывает multipart/form-data через call.receiveMultipart(). Метод возвращает MultiPartData, по которому итерируются части: PartData.FormItem для текста и PartData.FileItem для файлов.
Зависимости
// build.gradle.kts
dependencies {
val ktor_version = "2.3.12"
// multipart поддерживается в ядре, дополнительных зависимостей не нужно
implementation("io.ktor:ktor-server-core-jvm:$ktor_version")
implementation("io.ktor:ktor-server-netty-jvm:$ktor_version")
// для работы с файловой системой
implementation("org.jetbrains.kotlinx:kotlinx-io-core:0.4.0")
}
Базовая обработка загрузки файла
import io.ktor.http.content.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import java.io.File
fun Application.configureUpload() {
routing {
post("/upload") {
val multipart = call.receiveMultipart()
var fileName = ""
var savedPath = ""
multipart.forEachPart { part ->
when (part) {
is PartData.FormItem -> {
// Текстовое поле формы
println("Field '${part.name}' = ${part.value}")
}
is PartData.FileItem -> {
fileName = part.originalFileName ?: "upload_${System.currentTimeMillis()}"
val uploadDir = File("uploads").also { it.mkdirs() }
val destFile = File(uploadDir, fileName)
part.streamProvider().use { input ->
destFile.outputStream().use { output ->
input.copyTo(output)
}
}
savedPath = destFile.absolutePath
}
else -> {}
}
part.dispose() // ОБЯЗАТЕЛЬНО освобождать ресурсы
}
call.respond(mapOf(
"fileName" to fileName,
"path" to savedPath,
"status" to "uploaded"
))
}
}
}
Ограничение размера файла
import io.ktor.server.plugins.requestvalidation.*
fun Application.configureUploadWithLimit() {
// Ограничение через конфигурацию Netty
install(io.ktor.server.netty.NettyApplicationEngine) {}
// Либо через middleware:
routing {
post("/upload-limited") {
val contentLength = call.request.contentLength() ?: 0L
val maxSize = 10 * 1024 * 1024L // 10 MB
if (contentLength > maxSize) {
call.respond(HttpStatusCode.PayloadTooLarge, "File too large")
return@post
}
val multipart = call.receiveMultipart()
multipart.forEachPart { part ->
if (part is PartData.FileItem) {
val bytes = part.streamProvider().readBytes()
if (bytes.size > maxSize) {
call.respond(HttpStatusCode.PayloadTooLarge)
return@forEachPart
}
// сохраняем bytes
}
part.dispose()
}
call.respond(HttpStatusCode.OK)
}
}
}
Загрузка нескольких файлов
post("/upload-multiple") {
val multipart = call.receiveMultipart()
val uploadedFiles = mutableListOf<String>()
multipart.forEachPart { part ->
if (part is PartData.FileItem) {
val name = part.originalFileName ?: "file_${uploadedFiles.size}"
val dest = File("uploads/$name")
part.streamProvider().use { it.copyTo(dest.outputStream()) }
uploadedFiles.add(name)
}
part.dispose()
}
call.respond(mapOf("uploaded" to uploadedFiles))
}
Подводные камни
- Вызов
part.dispose()обязателен для каждой части — утечка без него приводит к исчерпанию памяти под нагрузкой. originalFileName— значение из заголовкаContent-Disposition, которое приходит от клиента; никогда не используйте его как путь к файлу без санитизации (path traversal атака).- Метод
forEachPartчитает тело лениво — повторный вызовreceiveMultipart()вернёт пустой поток. - При использовании nginx как reverse-proxy нужно задать
client_max_body_size— иначе nginx обрежет запрос до того, как Ktor его получит. streamProvider()возвращает поток, который может быть прочитан только один раз; сохраняйте содержимое сразу.- Для очень больших файлов
readBytes()загрузит всё в RAM; используйте потоковую запись черезcopyTo(). - Content-Type multipart/form-data без boundary в заголовке вызовет исключение при
receiveMultipart().
Common mistakes
- Путать термин «multipart upload» с соседним механизмом Ktor.
- Не называть границу lifecycle, transaction, thread или request для «multipart upload».
- Игнорировать production-эффекты «multipart upload»: latency, SQL shape, memory, security или observability.
What the interviewer is testing
- Попросить объяснить механизм «multipart upload» на минимальном примере.
- Проверить, видит ли кандидат failure mode и диагностику для «multipart upload».
- Уточнить, какие настройки или API меняют «multipart upload» в реальном сервисе.