鸿蒙POST请求完全指南:从JSON到multipart手动拼接
示例代码: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
}

三、表单格式:传统但面试常问
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--
关键点就三个:
- boundary(边界):一段随机的分隔符,告诉服务器“这里是一个字段的开始”
- 每个字段的头部:
Content-Disposition是必须的,Content-Type按需添加 - 换行必须是
\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 如果是字符串,会把二进制数据变成乱码。文件内容必须用 ArrayBuffer 或 Uint8Array 直接拼接。
坑三:边界跟内容撞了
边界不够随机,恰好跟图片里的某段二进制数据相同,服务器就会在错误的地方截断。所以上面的生成逻辑用了 16 字节随机数转 hex,概率极低。
发布成功效果
| 客户端发布图文 | 服务端接收图文 |
|---|---|
![]() |
![]() |
五、推荐方式: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 手动拼接能让你真正理解文件上传的原理,而不是只会调库。
知道原理,遇到问题才不会慌。
更多推荐





所有评论(0)