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

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()
}

核心逻辑说明

  1. 前端:通过flatMap将每个文件的元数据(JSON字符串)、文件二进制、分隔符拆分为Blob的独立元素,Blob会自动按顺序合并这些内容,不会触发File的toString()转换。
  2. 后端:直接读取字节流,通过查找自定义的---分隔符,区分元数据和文件内容;根据元数据中的文件大小,精准读取对应字节写入文件,避免二进制损坏。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 20:25:55