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

在Ktor中创建multipart/form-data POST请求遇400错误求指导

问题描述

已通过curl实现multipart/form-data类型的POST请求,表单字段new_profile存储序列化后的JSON字符串,但用Ktor框架复刻时返回400 Bad Request错误,请求指导正确实现方式。

curl参考命令

curl -X POST -H "Content-Type: multipart/form-data" -F "[name]=[json content]" "[request url]"

我的Kotlin代码

class MultiPartContent(val parts: List<Part>) : OutgoingContent.WriteChannelContent() {
    // val uuid = UUID.randomUUID()
    // val boundary = "***ktor-$uuid-ktor-${System.currentTimeMillis()}***"

    data class Part(
        val name: String,
        val filename: String? = null,
        val headers: Headers = Headers.Empty,
        val writer: suspend ByteWriteChannel.() -> Unit
    )

    override suspend fun writeTo(channel: ByteWriteChannel) {
        for (part in parts) {
            // channel.writeStringUtf8("--$boundary\r\n")
            val partHeaders = Headers.build {
                val fileNamePart = if (part.filename != null) "; filename=\"${part.filename}\"" else ""
                append("Content-Disposition", "form-data; name=\"${part.name}\"$fileNamePart")
                appendAll(part.headers)
            }
            for ((key, value) in partHeaders.flattenEntries()) {
                channel.writeStringUtf8("$key: $value\r\n")
            }
            channel.writeStringUtf8("\r\n")
            part.writer(channel)
            channel.writeStringUtf8("\r\n")
        }
        // channel.writeStringUtf8("--$boundary--\r\n")
    }

    override val contentType = ContentType.MultiPart.FormData
        // .withParameter("boundary", boundary)
        .withCharset(Charsets.UTF_8)

    class Builder {
        val parts = arrayListOf<Part>()

        fun add(part: Part) {
            parts += part
        }

        fun add(
            name: String,
            filename: String? = null,
            contentType: ContentType? = null,
            headers: Headers = Headers.Empty,
            writer: suspend ByteWriteChannel.() -> Unit
        ) {
            val contentTypeHeaders: Headers =
                if (contentType != null) headersOf(HttpHeaders.ContentType, contentType.toString()) else headersOf()
            add(Part(name, filename, headers + contentTypeHeaders, writer))
        }

        fun add(name: String, text: String, contentType: ContentType? = null, filename: String? = null) {
            add(name, filename, contentType) { writeStringUtf8(text) }
        }

        fun add(
            name: String,
            data: ByteArray,
            contentType: ContentType? = ContentType.Application.OctetStream,
            filename: String? = null
        ) {
            add(name, filename, contentType) { writeFully(data) }
        }

        internal fun build(): MultiPartContent = MultiPartContent(parts.toList())
    }

    companion object {
        fun build(callback: Builder.() -> Unit) = Builder().apply(callback).build()
    }
}

operator fun Headers.plus(other: Headers): Headers = when {
    this.isEmpty() -> other
    other.isEmpty() -> this
    else -> Headers.build {
        appendAll(this@plus)
        appendAll(other)
    }
}

expect fun httpClient(config: HttpClientConfig<*>.() -> Unit = {}): HttpClient

class Client {
    private val client = httpClient()

    public suspend fun multiport(data: data): HttpStatusCode {
        val json: String =  Json.encodeToString(value = data)
        println(createMyProfileJson)
        val res = client.post("[url]") {
            contentType(ContentType.MultiPart.FormData)
            setBody(
                MultiPartContent.build {
                    add(name = "[name]", text = json, contentType = ContentType.Application.Json)
                }
            )
        }
        return res.status
    }
}

错误信息

400 bad request

解决方案

问题根源

你自定义的MultiPartContent注释掉了boundary生成和写入逻辑,而multipart/form-data请求必须通过boundary分隔不同表单部分,缺少boundary会导致服务端无法解析请求,直接返回400错误。此外,Ktor已经内置了成熟的multipart处理工具,无需自行实现。

修正后的代码

首先确保HttpClient配置了必要插件:

// 以JVM平台为例配置HttpClient
actual fun httpClient(config: HttpClientConfig<*>.() -> Unit): HttpClient {
    return HttpClient(OkHttp) {
        install(ContentNegotiation) {
            json(Json {
                prettyPrint = true
                isLenient = true
                ignoreUnknownKeys = true
            })
        }
        install(Multipart) // 必须安装Multipart插件处理表单请求
        config()
    }
}

class Client {
    private val client = httpClient()

    // 替换Data为你实际的数据类
    public suspend fun multiport(data: Data): HttpStatusCode {
        val json: String = Json.encodeToString(data)
        println(json)

        val response = client.post("[你的请求URL]") {
            setBody(
                MultiPartFormDataContent(
                    formData {
                        append(
                            "[name]", // 替换为实际字段名(比如new_profile)
                            json,
                            Headers.build {
                                append(HttpHeaders.ContentType, ContentType.Application.Json.toString())
                            }
                        )
                    }
                )
            )
        }
        return response.status
    }
}

// 示例数据类,替换为你的实际数据结构
data class Data(val username: String, val email: String)

关键改动说明

  1. 移除自定义实现:用Ktor内置的MultiPartFormDataContent和formData构建器替代自定义类,自动处理boundary生成、请求格式拼接
  2. 配置必要插件:HttpClient必须安装Multipart插件,否则无法处理multipart请求
  3. 修正代码错误:修复了变量名错误(createMyProfileJson改为json,data: data改为合法数据类参数)
  4. 自动处理Content-Type:无需手动设置contentType(ContentType.MultiPart.FormData),MultiPartFormDataContent会自动添加包含boundary的正确Content-Type头

这样发送的请求格式会和curl完全一致,服务端即可正常解析。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 17:54:56