示例代码:HttpDemo

你的POST请求为什么总是400?

先讲一个真实场景。

你对着后端给的接口文档,吭哧吭哧写完了登录功能。一运行,报错——400 Bad Request。

你反复检查代码:URL没错,参数没漏,格式也对,但就是调不通。

最后后端同事丢过来一句话:“你 Content-Type 是不是没设对?”

你一拍大腿——果然。

POST 请求看起来简单,但真正用对的人并不多。尤其是 multipart/form-data,很多人会用但说不清原理。今天我们就从 JSON 到 multipart 手动拼接,把鸿蒙里的 POST 请求彻底聊透。

一、GET和POST怎么选?一句话说清楚

场景 用什么
查列表、查详情、搜索 GET
注册、登录、发帖、上传 POST

原则很简单:GET不改变服务器状态,POST会改变服务器状态

不是说 GET 不能带参数,但你总不能用 GET 去上传图片吧?

二、JSON格式:最常用但最容易被忽略的点

这是移动端最常用的 POST 格式,也是大家最熟悉的。

先定义请求参数类型:

interface LoginRequest {
  username: string;
  password: string;
}

然后发送请求:

const loginData: LoginRequest = {
  username: 'momo123',
  password: encryptedPassword
};

const response = await httpRequest.request(url, {
  method: http.RequestMethod.POST,
  header: {
    'Content-Type': 'application/json'   // 这一行不能少
  },
  extraData: JSON.stringify(loginData)
});

就这?就这么简单。

但很多人会在两个地方翻车:

第一,忘记设置 Content-Type。 不设的话,后端不知道你发的是 JSON,直接用默认格式解析,结果就是 400 或 415。

第二,忘记用 JSON.stringify。 extraData 传对象是不行的,必须序列化成字符串。

登录结果:

{
   "code": 200,
   "data": {
       "accessToken": "eyJhbGciOiJIUzI1NiJ9.xxxxxx",
       "refreshToken": "eyJhbGciOiJIUzI1NiJ9.xxxx",
       "userId": 2
   },
   "message": "success",
   "timestamp": 1780753779307
}

Post登录.png

三、表单格式:传统但面试常问

application/x-www-form-urlencoded 这种格式在移动端已经很少用了,但面试经常被问到,而且跟后面的 multipart 理解有关联。

// 注意:必须对值进行 URL 编码
const formBody = `username=${encodeURIComponent(this.username)}&password=${encodeURIComponent(this.password)}`;

const response = await httpRequest.request(url, {
  method: http.RequestMethod.POST,
  header: {
    'Content-Type': 'application/x-www-form-urlencoded'
  },
  extraData: formBody
});

数据格式就是 key1=value1&key2=value2,必须用 & 连接,而且值必须 URL 编码——中文不编码就乱码,空格不编码就是 +,后端一脸懵。

httpbin.org/post 测试,服务器回显什么你就能看到自己发出去的到底是什么样,这比猜来猜去要靠谱得多。

四、multipart手动拼接:这才是今天的核心

文件上传、图文混排发帖,都得用 multipart/form-data

鸿蒙官方提供了 MultiFormDataList 自动处理,但我还是想先带你手动拼一次——理解原理才能写出不崩的代码

4.1 原理预览

先看一个完整请求体长什么样:

POST /api/v1/posts HTTP/1.1
Content-Type: multipart/form-data; boundary=----HarmonyOSBoundaryA1B2C3

------HarmonyOSBoundaryA1B2C3
Content-Disposition: form-data; name="post"
Content-Type: application/json

{"title":"我的帖子","content":"内容"}
------HarmonyOSBoundaryA1B2C3
Content-Disposition: form-data; name="images"; filename="photo.jpg"
Content-Type: image/jpeg

[二进制图片数据]
------HarmonyOSBoundaryA1B2C3--

关键点就三个:

  1. boundary(边界):一段随机的分隔符,告诉服务器“这里是一个字段的开始”
  2. 每个字段的头部Content-Disposition 是必须的,Content-Type 按需添加
  3. 换行必须是 \r\n:用 \n 大概率被服务器拒掉

4.2 代码实现

首先是生成边界:

private generateBoundary(): string {
  const bytes = new Uint8Array(16);
  for (let i = 0; i < 16; i++) {
    bytes[i] = Math.floor(Math.random() * 256);
  }
  const hex = Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
  return `----HarmonyOSBoundary${hex}`;
}

定义请求参数结构:

interface CreatePostRequest {
  title: string;
  content: string;
  location?: string;
  categoryId?: number;
  tags?: string[];
}

interface MultipartBuildResult {
  body: ArrayBuffer;
  boundary: string;
}

然后是核心的构建方法:

private async buildMultipartBody(
  postRequest: CreatePostRequest,
  imageUris: string[]
): Promise<MultipartBuildResult> {
  const boundary = this.generateBoundary();
  const encoder = new util.TextEncoder();
  const parts: Uint8Array[] = [];

  const appendString = (str: string) => {
    parts.push(encoder.encodeInto(str));
  };

  // 1. 添加 post 字段(JSON 字符串)
  const postJson = JSON.stringify(postRequest);
  appendString(`--${boundary}\r\n`);
  appendString(`Content-Disposition: form-data; name="post"\r\n`);
  appendString(`Content-Type: application/json\r\n\r\n`);
  appendString(`${postJson}\r\n`);

  // 2. 添加每个图片文件
  for (const uri of imageUris) {
    const fileData = await this.readFileToUint8Array(uri);
    const fileName = this.getDecodedFileName(uri);
    const mimeType = this.getMimeTypeFromUri(uri);
    
    appendString(`--${boundary}\r\n`);
    appendString(`Content-Disposition: form-data; name="images"; filename="${fileName}"\r\n`);
    appendString(`Content-Type: ${mimeType}\r\n\r\n`);
    parts.push(fileData);
    appendString(`\r\n`);
  }

  // 3. 结束边界
  appendString(`--${boundary}--\r\n`);

  // 合并所有部分
  let totalLength = 0;
  for (const part of parts) totalLength += part.length;
  const result = new Uint8Array(totalLength);
  let offset = 0;
  for (const part of parts) {
    result.set(part, offset);
    offset += part.length;
  }
  return { body: result.buffer as ArrayBuffer, boundary };
}

发送的时候把 extraData 换成构建好的 ArrayBuffer

const result = await this.buildMultipartBody(postRequest, this.selectedImageUris);

const response = await httpRequest.request(url, {
  method: http.RequestMethod.POST,
  header: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': `multipart/form-data; boundary=${result.boundary}`
  },
  extraData: result.body   // 注意这里是 ArrayBuffer,不是字符串
});

4.3 手动拼接的三大坑

坑一:换行符用了 \n 而不是 \r\n

HTTP 协议规定头部和数据之间必须用 \r\n 分隔。用 \n 在某些服务器上可能容忍,但在严格实现的服务器上直接 400。

坑二:图片数据当成字符串处理

extraData 如果是字符串,会把二进制数据变成乱码。文件内容必须用 ArrayBufferUint8Array 直接拼接。

坑三:边界跟内容撞了

边界不够随机,恰好跟图片里的某段二进制数据相同,服务器就会在错误的地方截断。所以上面的生成逻辑用了 16 字节随机数转 hex,概率极低。

发布成功效果

客户端发布图文 服务端接收图文
Post客户端发布图文.png Post服务端接收图文.png

五、推荐方式:MultiFormDataList

手动拼接了解原理就好,实际开发用官方提供的自动方式,省心很多:

import { http } from '@kit.NetworkKit';

const formDataList: http.MultiFormData[] = [
  {
    name: 'post',
    contentType: 'application/json',
    data: JSON.stringify(postRequest)
  },
  {
    name: 'images',
    contentType: 'image/jpeg',
    data: imageArrayBuffer,
    remoteFileName: 'photo.jpg'
  }
];

const response = await httpRequest.request(url, {
  method: http.RequestMethod.POST,
  header: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'multipart/form-data'
  },
  multiFormDataList: formDataList
});

MultiFormDataList 帮你自动完成了:生成边界、添加 Content-Disposition、处理换行符、组装请求体。你只需要关心业务字段,剩下的交给系统。

但有一个容易被忽略的点remoteFileName 必须指定,否则后端可能收不到文件名。

六、避坑总结

问题 原因 解决方案
POST JSON 返回 400 Content-Type 没设或数据格式不对 加上 application/json,用 JSON.stringify
表单中文变乱码 没做 URL 编码 encodeURIComponent
文件上传返回 400 手动拼接时换行符用了 \n 统一用 \r\n
上传后图片打不开 二进制被转成字符串 ArrayBuffer 直接传
Token 过期返回 401 accessToken 过期 实现 refreshToken 自动刷新

七、总结

POST 请求没有魔法,就是按照协议规范把一个字符串或二进制数据发到指定地址,然后接收响应。关键是把每一步做对。

JSON 最简单,大部分场景够用。表单格式传统但面试常问。multipart 手动拼接能让你真正理解文件上传的原理,而不是只会调库。

知道原理,遇到问题才不会慌。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐