如何强制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
相关产品推荐
相关产品推荐

