JS二进制数据POST到Kotlin后端转为[Object]问题排查
解决JS发送文件+元数据时的类型转换问题
你遇到的[object File]---输出,本质是File对象与字符串直接拼接时触发了默认的toString()方法——JavaScript中引用类型和字符串做+运算时,会自动调用对象的toString(),而File的toString()默认返回[object File],导致文件二进制数据被转换成了无效字符串,而非与分隔符拼接。
由于你不想使用FormData,可以利用Blob构造函数支持混合类型数组的特性,将文件元数据、分隔符、文件二进制作为独立元素传入Blob,避免直接拼接触发类型转换。
前端JavaScript修改方案
修改文件拼接逻辑,用flatMap将每个文件的元数据(转成JSON字符串)和File对象拆分为Blob的独立元素,最后添加结束标记:
function uploadFiles() { const folderInput = document.getElementById("folder"); var files = []; for (let i = 0; i < folderInput.files.length; i++) { const file = folderInput.files[i]; files.push({ length: file.size, fileName: file.name, fileData: file, }); } // 用flatMap生成包含元数据、分隔符、文件的一维数组 const parts = files.flatMap((file) => { // 将元数据转为JSON字符串,加上分隔符 const metadata = JSON.stringify({ fileName: file.fileName, length: file.length }) + "---"; return [metadata, file.fileData, "---"]; }); // 添加结束标记,方便后端识别传输完成 parts.push("END---"); var blob = new Blob(parts); fetch("http://localhost:8000/upload", { method: "POST", headers: { // 使用自定义Content-Type,避免被识别为标准multipart "Content-Type": "application/custom-file-stream", }, body: blob, }) .then((response) => { if (response.ok) { console.log("Files uploaded successfully"); response.text().then((data) => { console.log("Return body data: ", data); }); } else { console.error("Error uploading files"); } }) .catch((error) => { console.error("Error:", error); }); return false; }
后端Kotlin修改方案
原后端用字符流读取二进制数据会导致文件损坏,需改为字节流读取,并按照自定义协议解析元数据和文件内容:
1. 导入JSON依赖
如果使用Gradle,在build.gradle中添加:
dependencies { implementation 'org.json:json:20240303' }
2. 修改处理器逻辑
import com.sun.net.httpserver.HttpServer import com.sun.net.httpserver.HttpHandler import com.sun.net.httpserver.HttpExchange import java.io.IOException import java.io.OutputStream import java.net.InetSocketAddress import java.io.FileOutputStream import java.io.File import org.json.JSONObject import org.json.JSONException class MyServer { private lateinit var server: HttpServer fun startServer() { try { server = HttpServer.create(InetSocketAddress(8000), 0) server.createContext("/upload", PutRemoteFiles()) server.executor = null server.start() val host = server.address println("Server started at http://$host") } catch (e: IOException) { println("Error starting the server: ${e.message}") } } fun stopServer() { server.stop(0) println("Server stopped") } private class PutRemoteFiles : HttpHandler { override fun handle(exchange: HttpExchange) { // 设置CORS头 val headers = exchange.responseHeaders headers.add("Access-Control-Allow-Origin", "*") headers.add("Access-Control-Allow-Methods", "POST") headers.add("Access-Control-Allow-Headers", "Content-Type") if (exchange.requestMethod == "POST") { try { val input = exchange.requestBody val separatorBytes = "---".toByteArray(Charsets.UTF_8) val metadataBuffer = mutableListOf<Byte>() var separatorMatchCount = 0 while (true) { val b = input.read() if (b == -1) break // 查找分隔符--- if (b == separatorBytes[separatorMatchCount].toInt()) { separatorMatchCount++ if (separatorMatchCount == 3) { // 解析分隔符前的元数据 val metadataStr = String(metadataBuffer.toByteArray(), Charsets.UTF_8) metadataBuffer.clear() separatorMatchCount = 0 if (metadataStr == "END") { // 传输结束 break } try { val metadata = JSONObject(metadataStr) val fileName = metadata.getString("fileName") val fileSize = metadata.getLong("length") val outputFile = File("DataBase/$fileName") // 创建父目录(如果不存在) outputFile.parentFile?.mkdirs() FileOutputStream(outputFile).use { fos -> val fileBuffer = ByteArray(4096) var remainingSize = fileSize // 读取指定大小的文件字节 while (remainingSize > 0) { val readBytes = input.read(fileBuffer, 0, minOf(fileBuffer.size, remainingSize.toInt())) if (readBytes == -1) break fos.write(fileBuffer, 0, readBytes) remainingSize -= readBytes } } println("成功写入文件: ${outputFile.absolutePath}") // 跳过文件后的---分隔符 separatorMatchCount = 0 while (separatorMatchCount < 3) { val sepByte = input.read() if (sepByte == separatorBytes[separatorMatchCount].toInt()) { separatorMatchCount++ } else { separatorMatchCount = 0 } } } catch (e: JSONException) { println("元数据解析失败: ${e.message}") } } } else { // 不是分隔符的一部分,加入元数据缓冲区 if (separatorMatchCount > 0) { // 把之前匹配的分隔符字节加入缓冲区 repeat(separatorMatchCount) { metadataBuffer.add(separatorBytes[it]) } separatorMatchCount = 0 } metadataBuffer.add(b.toByte()) } } input.close() val response = "所有文件上传完成" exchange.sendResponseHeaders(200, response.length.toLong()) exchange.responseBody.use { os -> os.write(response.toByteArray()) } } catch (e: IOException) { println("读取请求体错误: ${e.message}") exchange.sendResponseHeaders(500, 0) exchange.responseBody.close() } } else { val response = "仅支持POST请求" exchange.sendResponseHeaders(400, response.length.toLong()) exchange.responseBody.use { os -> os.write(response.toByteArray()) } } } } } fun main() { val server = MyServer() server.startServer() println("Server started. Type 'stop' to stop the server.") var command = readLine() while (command != "stop") { command = readLine() } server.stopServer() }
核心逻辑说明
- 前端:通过
flatMap将每个文件的元数据(JSON字符串)、文件二进制、分隔符拆分为Blob的独立元素,Blob会自动按顺序合并这些内容,不会触发File的toString()转换。 - 后端:直接读取字节流,通过查找自定义的
---分隔符,区分元数据和文件内容;根据元数据中的文件大小,精准读取对应字节写入文件,避免二进制损坏。
内容的提问来源于stack exchange,提问作者Wazeback
相关产品推荐
相关产品推荐

