将Retrofit Multipart请求转换为Ktor Client时遇阻求助
问题解决:Retrofit Multipart转Ktor Client实现及日志排查
一、修复Multipart请求实现
你的Ktor代码存在两个关键问题:
- 每个文件part的
name设置错误(你用了"content",但Retrofit里是"image_$index") - 不必要地多次创建
formData并合并,导致请求结构异常
修正后的代码如下:
private suspend fun sendFiles(files: List<File>): HttpResponse { return httpClient.post("rest/sendPhotos") { setBody(MultiPartFormDataContent(formData { files.forEachIndexed { index, file -> append( name = "image_$index", value = InputProvider(file.length()) { file.inputStream().asInput() }, headers = Headers.build { append(HttpHeaders.ContentType, "multipart/form-items") append( HttpHeaders.ContentDisposition, "form-data; name=\"image_$index\"; filename=\"${file.name}\"" ) } ) } })) } }
或者用Ktor内置的ContentDisposition构建器避免手动拼接字符串出错:
import io.ktor.http.content.* private suspend fun sendFiles(files: List<File>): HttpResponse { return httpClient.post("rest/sendPhotos") { setBody(MultiPartFormDataContent(formData { files.forEachIndexed { index, file -> append( name = "image_$index", value = file.readBytes(), headers = Headers.build { append(HttpHeaders.ContentType, "multipart/form-items") append( HttpHeaders.ContentDisposition, ContentDisposition.File .withParameter(ContentDisposition.Parameters.Name, "image_$index") .withParameter(ContentDisposition.Parameters.FileName, file.name) .toString() ) } ) } })) } }
说明:
- 直接在
formDataDSL中一次性构建所有文件part,保证请求结构和Retrofit完全对齐 - 每个part的
name严格对应Retrofit中的image_$index - 用
ContentDisposition构建器可避免手动拼接字符串的格式错误
二、解决Ktor日志不显示Multipart请求体的问题
Ktor默认会省略Multipart请求体的日志,可通过以下两种方式解决:
方式1:配置Logging插件打印BODY级日志
确保Ktor Client配置了Logging插件,并设置日志级别为BODY:
import io.ktor.client.plugins.logging.* val httpClient = HttpClient(OkHttp) { install(Logging) { level = LogLevel.BODY logger = object : Logger { override fun log(message: String) { // 替换为你的日志输出方式,比如Android Logcat println(message) } } } }
方式2:自定义拦截器手动打印Multipart内容
如果Logging插件仍无法打印,可添加自定义拦截器解析Multipart内容:
import io.ktor.client.plugins.api.* import io.ktor.http.content.* val httpClient = HttpClient(OkHttp) { install(Plugin) { onRequest { request -> if (request.body is MultiPartFormDataContent) { val multipart = request.body as MultiPartFormDataContent println("=== Multipart Request Body ===") multipart.parts.forEachIndexed { index, part -> println("Part $index:") println(" Content-Disposition: ${part.headers[HttpHeaders.ContentDisposition]}") println(" Content-Type: ${part.headers[HttpHeaders.ContentType]}") if (part is PartData.FileItem) { println(" File Name: ${part.originalFileName}") println(" File Size: ${part.size}") } } println("=============================") } } } }
验证要点
- 对比Retrofit日志,确保Ktor请求的
Content-Type为multipart/form-data; boundary=xxx - 每个文件part的
Content-Disposition包含name="image_$index"和filename="xxx.jpg" Content-Type保持和Retrofit一致的multipart/form-items
内容的提问来源于stack exchange,提问作者Barmaley
相关产品推荐
相关产品推荐

