鸿蒙 HTTP 文件上传:用 multiFormDataList 正确发送 multipart

前言

在鸿蒙应用里上传用户头像、附件是再常见不过的需求,但社区里关于“服务端收不到 multipart”“请求一直 pending”的提问非常集中。根因几乎都出在两点:extraData 不能直接传文件对象,以及忘记调用 destroy() 释放连接。本文给出 API 11+ 的标准写法,并补充 rcp 更简洁的替代方案。

问题描述

开发者常踩的两个坑:

  1. 服务端 req.file 是 undefined:照着 Web 前端的习惯,把文件塞进 extraData: { file: fileObj }。但 extraData 只接受 string / Object / ArrayBuffer 类型——传普通对象会被 JSON 序列化,服务端自然解析不出文件流。
  2. 请求永久 pending、连接泄漏:手动拼接 multipart 字符串时,一旦 boundary 分隔符或 Content-Length 算错,底层 TCP 会一直等待剩余数据,导致请求卡死;再加上 http.createHttp() 用完不释放,连接池被占满后后续请求全部挂起。

细节解析

官方推荐的正确做法(API 11+)是使用 multiFormDataList 字段(类型 Array<MultiFormData>)。系统会自动处理 boundary 分隔符、Content-Length 和请求体组装,禁止手填这些头。要点:

  • 一个数组元素里,filePath(文件沙箱路径)和 data(文本)互斥,文件字段用 filePath,文本字段用 data,分别放在数组的不同对象中。
  • name 必须与后端接收字段名一致(如后端用 file,你就写 name: 'file')。
  • filePath 必须是应用沙箱绝对路径context.filesDir 下的路径)。相册返回的 URI 要先复制到沙箱;不能直接传 file:// 前缀或 fd。
  • http.createHttp() 每次返回的 HttpRequest 不可复用,用完必须 destroy(),推荐 try/finally 保证执行。
  • module.json5 需要声明 ohos.permission.INTERNET

如果上传逻辑复杂(断点续传、重试、后台托管),可以用 @kit.NetworkKitrequest.agent()(API 10+)任务托管,或 @kit.RemoteCommunicationKitrcp 模块——后者对 multipart 封装更友好。

示例代码

标准写法(http + multiFormDataList,Promise 风格)

import { http } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { common } from '@kit.AbilityKit';

async function uploadAvatar(ctx: common.UIAbilityContext, serverUrl: string, filePath: string) {
  const req = http.createHttp();
  try {
    const res = await req.request(serverUrl, {
      method: http.RequestMethod.POST,
      header: { 'Content-Type': 'multipart/form-data' },
      multiFormDataList: [
        {
          name: 'file',                 // 与后端字段一致
          contentType: 'image/jpeg',
          remoteFileName: 'avatar.jpg',  // 服务端保存的文件名
          filePath: filePath            // 沙箱绝对路径,非 file:// URI
        },
        {
          name: 'name',                 // 文本字段与文件字段分开
          contentType: 'text/plain',
          data: '用户昵称'
        }
      ],
      connectTimeout: 15000,
      readTimeout: 30000
    });
    if (res.responseCode < 200 || res.responseCode >= 300) {
      throw new Error(`上传失败:${res.responseCode}`);
    }
    return res.result;
  } finally {
    req.destroy(); // 无论成功失败都释放连接
  }
}

更简洁的 rcp 写法

import { rcp } from '@kit.RemoteCommunicationKit';

function uploadWithRcp(serverUrl: string, filePath: string): void {
  const session = rcp.createSession();
  const form = new rcp.MultipartForm({
    name: '用户昵称',
    file: { remoteFileName: 'avatar.jpg', contentType: 'image/jpeg', contentOrPath: filePath }
  });
  const req = new rcp.Request(serverUrl);
  req.method = 'POST';
  req.content = form;
  session.fetch(req)
    .then((resp) => console.info(`upload ok: ${resp.statusCode}`))
    .catch((err: BusinessError) => console.error(`fail: ${err.message}`))
    .finally(() => session.close());
}

相册 URI 转沙箱路径(上传前必备)

import { fileIo as fs } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';

function copyToSandbox(ctx: common.UIAbilityContext, srcUri: string): string {
  const dest = `${ctx.filesDir}/upload_${Date.now()}.jpg`;
  fs.copyFileSync(srcUri, dest); // 相册 URI 必须先复制进沙箱再传 filePath
  return dest;
}

总结

文件上传的正确姿势:用 multiFormDataList 让系统自动组装 multipart,不手拼字符串;name 对齐后端字段;filePath 用沙箱绝对路径;destroy() 放进 finally。想要断点续传或后台托管用 request.agent(),想要代码简洁用 rcp。记住:extraData 传文件对象是行不通的,连接不释放才是 pending 的真凶。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐