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

使用Ktor发送大文件触发OOM错误,求分块上传解决方案

如何在Ktor中通过form-data分块发送大文件避免OOM错误?

我按照Ktor官方文档的方式发送本地90MB文件,代码如下:

val client = HttpClient(CIO)

val response: HttpResponse = client.post("http://localhost:8080/upload") {
    setBody(MultiPartFormDataContent(
        formData {
            append("description", "Ktor logo")
            append("image", File("ktor_logo.png").readBytes(), Headers.build {
                append(HttpHeaders.ContentType, "image/png")
                append(HttpHeaders.ContentDisposition, "filename=\"ktor_logo.png\"")
            })
        },
        boundary = "WebAppBoundary"
    )
    )
    onUpload { bytesSentTotal, contentLength ->
        println("Sent $bytesSentTotal bytes from $contentLength")
    }
}

运行后触发OutOfMemoryError错误,错误栈如下:

FATAL EXCEPTION: DefaultDispatcher-worker-2
    PID: 8152
    java.lang.OutOfMemoryError: Failed to allocate a 4112 byte allocation with 1424 free bytes and 1424B until OOM, max allowed footprint 100663296, growth limit 100663296
        at java.nio.HeapByteBuffer.<init>(HeapByteBuffer.java:54)
        at java.nio.HeapByteBuffer.<init>(HeapByteBuffer.java:49)
        at java.nio.ByteBuffer.allocate(ByteBuffer.java:281)
        at io.ktor.utils.io.bits.DefaultAllocator.alloc-gFv-Zug(MemoryFactoryJvm.kt:41)
        at io.ktor.utils.io.core.DefaultBufferPool.produceInstance(BufferFactory.kt:58)
        at io.ktor.utils.io.core.DefaultBufferPool.produceInstance(BufferFactory.kt:51)
        at io.ktor.utils.io.pool.DefaultPool.borrow(DefaultPool.kt:51)
        at io.ktor.utils.io.core.internal.ChunkBuffer$Companion$Pool$1.borrow(ChunkBuffer.kt:133)
        at io.ktor.utils.io.core.internal.ChunkBuffer$Companion$Pool$1.borrow(ChunkBuffer.kt:128)
        at io.ktor.utils.io.core.Output.appendNewChunk(Output.kt:102)
        at io.ktor.utils.io.core.Output.prepareWriteHead(Output.kt:354)
        at io.ktor.utils.io.core.internal.UnsafeKt.prepareWriteHead(Unsafe.kt:57)
        at io.ktor.utils.io.ByteBufferChannel.readRemainingSuspend(ByteBufferChannel.kt:3689)
        at io.ktor.utils.io.ByteBufferChannel.access$readRemainingSuspend(ByteBufferChannel.kt:24)
        at io.ktor.utils.io.ByteBufferChannel$readRemainingSuspend$1.invokeSuspend(Unknown Source:16)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
        at kotlinx.coroutines.internal.LimitedDispatcher.run(LimitedDispatcher.kt:42)
        at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:95)
        at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:570)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:677)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:664)
        Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@29cc6a1, Dispatchers.IO]

我认为问题出在readBytes()方法,尝试使用appendInput方式上传,代码如下:

appendInput(
        "MY_KEY",
        Headers.build {
            headers.forEach { h ->
                this.append(h.first, h.second) // Content-Disposition and Content-Type here
            }
        },
        file.length()
    ){
        file.inputStream().asInput()
    }

仍出现相同错误,我的HttpClient配置如下:

HttpClient(OkHttp) {
    install(ContentNegotiation) {
        json(Json {
            prettyPrint = true
            isLenient = true
            ignoreUnknownKeys = true
        })
    }

    install(Resources)
    install(Logging) {
        logger = Logger.DEFAULT
        level = LogLevel.BODY
    }    

    engine {
        config {
            followRedirects(true)
            connectTimeout(100_000, TimeUnit.MILLISECONDS)
            readTimeout(100_000, TimeUnit.MILLISECONDS)
        }
    }
}

解决方案

核心问题分析

OOM的本质是一次性将整个大文件加载到内存,不管是readBytes()直接读全量字节,还是你尝试的appendInput写法,都因为Logging插件的LogLevel.BODY设置导致Ktor把整个请求体读入内存做日志输出,直接触发内存溢出。

正确的流式上传实现

使用Ktor内置的append方法重载,直接传入File对象,底层会自动处理流式分块上传,无需手动操作输入流:

val client = HttpClient(OkHttp) {
    install(ContentNegotiation) {
        json(Json {
            prettyPrint = true
            isLenient = true
            ignoreUnknownKeys = true
        })
    }

    install(Resources)
    install(Logging) {
        logger = Logger.DEFAULT
        // 关键:避免打印请求体,防止全量加载到内存
        level = LogLevel.HEADERS
        // 或者更精细控制:仅排除multipart类型的请求体日志
        filter { request ->
            !request.contentType().toString().startsWith("multipart/form-data")
        }
    }    

    engine {
        config {
            followRedirects(true)
            connectTimeout(100_000, TimeUnit.MILLISECONDS)
            readTimeout(100_000, TimeUnit.MILLISECONDS)
        }
    }
}

val response: HttpResponse = client.post("http://localhost:8080/upload") {
    setBody(MultiPartFormDataContent(formData {
        append("description", "Ktor logo")
        // 直接传入File对象,自动流式分块
        append(
            key = "image",
            file = File("ktor_logo.png"),
            headers = Headers.build {
                append(HttpHeaders.ContentType, "image/png")
                append(HttpHeaders.ContentDisposition, "filename=\"ktor_logo.png\"")
            }
        )
    }))
    onUpload { bytesSentTotal, contentLength ->
        println("已发送 $bytesSentTotal / $contentLength 字节")
    }
}

额外注意事项

  • 确保使用支持流式上传的HttpClient引擎:OkHttp和CIO都支持,避免使用已废弃的Apache引擎。
  • 服务器端也需要配置支持分块接收,避免服务器端出现OOM。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 20:30:33