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

Fastify对接Dropbox会话上传时append接口返回closed错误如何解决?

问题原因

你遇到的closed错误核心是以下几个问题导致的:

  • 前端用FormData格式上传文件,后端直接读取req.raw原始请求流往Dropbox传输,流中包含了FormData的边界标识、字段描述等非文件内容,和你计算的offset完全不匹配,Dropbox校验块数据失败直接关闭会话。
  • 你用stream.on('data')绑定异步回调处理分片,即使pause了流也无法保证分片按顺序上传,一旦前一个分片的append请求还没完成,后一个分片就发送,offset就会错乱,Dropbox收到乱序的分片会直接关闭会话。
  • 所有Dropbox接口请求都没有加异常捕获,只要某一次请求超时或失败,会话状态异常后续所有append请求都会返回closed。

解决步骤

  1. 安装@fastify/multipart插件解析FormData,拿到纯文件二进制流,不要直接处理原始请求流
  2. 用异步迭代器处理文件流,保证分片按顺序串行上传,避免offset错乱
  3. 给所有Dropbox接口请求添加异常捕获,出错时直接终止流程返回错误

核心代码修改

首先安装依赖:
npm install @fastify/multipart

修改Fastify服务的路由逻辑:

require("dotenv").config()
const https = require("https")
const JSONbig = require("json-bigint")

const TOKEN = process.env.DROPBOX_ACCESS_TOKEN

const fastify = require("fastify")({
  logger: {
    level: "info"
  }
})

// Promisify https request
const httpRequest = (url, options, data) => {
  return new Promise((resolve, reject) => {
    const req = https.request(url, options, (res) => {
      let body = ""
      res.on("data", (chunk) => {
        body += chunk.toString()
      })
      res.on("error", reject)
      res.on("end", () => {
        if (res.statusCode >= 200 && res.statusCode <= 299) {
          resolve({ statusCode: res.statusCode, headers: res.headers, body: JSON.parse(body) })
        } else {
          reject("Request failed. status: " + res.statusCode + ", body: " + body)
        }
      })
    })
    req.on("error", reject)
    if (data) req.write(data)
    req.end()
  })
}

fastify.register(require("fastify-cors"))
// 注册multipart解析插件
fastify.register(require('@fastify/multipart'), {
  limits: {
    fileSize: 1024 * 1024 * 1024 // 按需调整最大支持的文件大小
  }
})

fastify.post("/", async (req, reply) => {
  try {
    // 解析获取上传的文件流
    const uploadFile = await req.file()
    const fileStream = uploadFile.file

    // 初始化上传会话
    const initResponse = await httpRequest(
      "https://content.dropboxapi.com/2/files/upload_session/start",
      {
        method: "post",
        headers: {
          Authorization: `Bearer ${TOKEN}`,
          "Dropbox-API-Arg": JSON.stringify({
            close: false
          }),
          "Content-Type": "application/octet-stream"
        }
      }
    )
    const sessionId = initResponse.body.session_id
    req.log.info(`Initiating session ${sessionId}`)

    let offset = 0
    // 异步迭代器保证分片串行上传
    for await (const chunk of fileStream) {
      req.log.info(`Sending chunk, size: ${chunk.length}, offset: ${offset}`)
      await httpRequest(
        `https://content.dropboxapi.com/2/files/upload_session/append_v2`,
        {
          method: "post",
          headers: {
            Authorization: `Bearer ${TOKEN}`,
            "Dropbox-API-Arg": JSONbig.stringify({
              cursor: {
                session_id: sessionId,
                offset
              },
              close: false
            }),
            "Content-Type": "application/octet-stream"
          }
        },
        chunk
      )
      offset += chunk.length
    }

    // 完成上传会话
    await httpRequest(
      `https://content.dropboxapi.com/2/files/upload_session/finish`,
      {
        method: "post",
        headers: {
          Authorization: `Bearer ${TOKEN}`,
          "Dropbox-API-Arg": JSONbig.stringify({
            cursor: {
              session_id: sessionId,
              offset: offset
            },
            commit: {
              path: `/Upload/${uploadFile.filename}`, // 使用实际上传的文件名
              mode: "add",
              autorename: true,
              mute: false,
              strict_conflict: false
            }
          }),
          "Content-Type": "application/octet-stream"
        }
      }
    )
    req.log.info("upload session finished")
    return reply.code(204).send()
  } catch (err) {
    req.log.error(err)
    return reply.code(500).send({ error: err })
  }
})

fastify.listen(3000, (err) => {
  if (err) {
    fastify.log.fatal(err)
    process.exit(1)
  }
  console.log(`server listening on ${fastify.server.address().port}`)
})

前端代码无需修改,保持原有FormData上传逻辑即可正常运行。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.25 22:27:03