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

如何强制Ktor-Client向Cloudflare R2发送带application/octet-stream头的文件?

问题与解决方案

问题详情

使用curl发送PUT请求到Cloudflare R2存储时能成功返回200:

curl --location --request PUT 'https://*.r2.cloudflarestorage.com/***' \
--header 'Content-Type: application/octet-stream' \
--form 'filename=@"smhjrc-kc/file.jpg"'

但通过c2k工具生成的Ktor-Client代码发送请求时,返回403 SignatureDoesNotMatch:

suspend fun uploadR2File(
    name: String,
    file: File,
    contentLength: Long,
) = client.put("https://*.r2.cloudflarestorage.com/***") {
    headers.append("Content-Type", "application/octet-stream")

    setBody(
        MultiPartFormDataContent(
            formData {
                append("filename", ChannelProvider(size = contentLength) {
                    file.readChannel()
                },
                    Headers.build {
                        append(HttpHeaders.ContentType, "application/octet-stream")
                        append(HttpHeaders.ContentDisposition, "filename=\"$name\"")
                    })
            }
        )
    )
}

核心问题:手动设置的Content-Type: application/octet-stream请求头被Ktor自动替换为multipart/form-data(因为使用了MultiPartFormDataContent),而不带该头的curl请求同样返回403,需强制Ktor保留指定的Content-Type头。

解决方案

方法1:绕过自动Content-Type生成,自定义请求体

Ktor使用MultiPartFormDataContent时会强制设置multipart/form-data类型的Content-Type,因此我们可以先构建multipart内容,再将其作为原始字节通道设置为请求体,从而手动控制Content-Type:

suspend fun uploadR2File(
    name: String,
    file: File,
    contentLength: Long,
) = client.put("https://*.r2.cloudflarestorage.com/***") {
    // 构建multipart表单内容
    val multipartContent = MultiPartFormDataContent(
        formData {
            append("filename", ChannelProvider(size = contentLength) {
                file.readChannel()
            },
                Headers.build {
                    append(HttpHeaders.ContentType, "application/octet-stream")
                    append(HttpHeaders.ContentDisposition, "filename=\"$name\"")
                })
        }
    )

    // 将multipart内容转换为字节通道,作为原始请求体
    setBody(object : OutgoingContent.ReadChannelContent() {
        override val contentLength: Long? = multipartContent.contentLength
        override val channel: ByteReadChannel = multipartContent.readFrom()
    })

    // 强制设置全局请求的Content-Type
    contentType(ContentType.Application.OctetStream)
}

方法2:直接上传文件内容(若R2接口允许)

如果Cloudflare R2的目标接口仅需要文件内容而非表单结构,可简化代码,直接上传文件并设置Content-Type:

suspend fun uploadR2File(
    name: String,
    file: File,
) = client.put("https://*.r2.cloudflarestorage.com/***") {
    contentType(ContentType.Application.OctetStream)
    setBody(file.readChannel())
    // 若需要传递文件名,可添加到请求头或URL参数
    headers.append(HttpHeaders.ContentDisposition, "filename=\"$name\"")
}

关键说明

  • Ktor的MultiPartFormDataContent会自动生成并覆盖Content-Type头,因此必须绕过该逻辑才能强制设置application/octet-stream。
  • 方法1保留了原curl请求的表单结构,适合必须传递表单字段的场景;方法2更简洁,适合仅需上传文件的场景。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.12 11:45:55