摘要 : 天气查询 App 上线前,我遇到一个诡异问题:真机上所有请求全部失败,Previewer 里却一切正常。排查到最后,根因既不是代码也不是网络,而是网络安全配置 ------HarmonyOS 对明文 HTTP 默认拦截。这个坑让我意识到,鸿蒙网络层的问题 80% 不在"怎么发请求",而在架构:权限、安全配置、统一封装、拦截器、错误处理。本文以天气 App 为贯穿场景,从 @ohos.net.http 原生 API 到 Axios 鸿蒙版封装,单点深挖拦截器与统一错误处理的设计,附超时/重试策略对比与 5 个真实踩坑。
适用版本: HarmonyOS NEXT 7.x / API 14+ / ohpm(2026 年稳定版)
开篇:权限配了,请求还是全部失败
"真机上所有请求都失败,但 Previewer 里好好的。"
2026 年 7 月底,天气查询 App 真机联调第一天。我信心满满地打包安装,打开 App 期待看到天气数据------结果屏幕上只有错误提示。回到 Previewer 里跑,接口正常返回。
这个"真机失败、预览器成功"的现象,我后来才知道是鸿蒙网络层的经典坑:默认网络安全配置只信任 HTTPS,明文 HTTP 请求被系统拦截。而 Previewer 走的是宿主环境的网络栈,不受这个限制。
真机: HTTP 请求 → 被网络安全配置拦截
Previewer: HTTP 请求 → 走宿主网络栈,正常
这一个坑让我排查了 2 小时。它暴露了一个更重要的问题:网络层的设计不是"怎么发请求",而是权限、安全、封装、错误处理这一整套架构。本文就按这个思路,把鸿蒙网络请求的完整架构讲透。

一、网络权限配置(第一道坎)

1.1 为什么请求全部失败
HarmonyOS 应用请求网络,必须先在 module.json5 声明权限,否则请求直接失败:
json5
// entry/src/main/module.json5
{
"module": {
"name": "entry",
"requestPermissions": [
{
"name": "ohos.permission.INTERNET" // 网络访问权限
}
]
}
}
坑 1:漏配 INTERNET 权限,请求静默失败
text
现象: 请求代码看起来没问题,但 onFail 返回错误 2300006(无网络权限)
根因: 未在 module.json5 声明 ohos.permission.INTERNET
这里要说一下"静默失败"为什么如此高发:权限类错误不会在编译期暴露 ------INTERNET 权限属于 normal 级别,不弹窗、不提示,只在运行时请求发出去的那一刻以错误码 2300006 告知。而多数业务代码只处理了"成功给数据、失败给 Toast",把 onFail 里那个数字错误码原样吞掉或只打印了一行日志,用户侧看到的就是"什么都没发生"。这也是后面第 3.2 节要做统一错误映射的直接动机:把系统错误码在拦截器里翻译成人话,错误才不至于静默。
1.2 网络安全配置(HTTPS 与明文 HTTP)
HarmonyOS 默认只允许 HTTPS,明文 HTTP 会被拦截。开发期要访问本地/内网 HTTP 接口,必须配置网络安全:
json5
// entry/src/main/resources/base/profile/network_config.json
{
"network-security-config": {
"base-config": {
"cleartext-traffic-permitted": true // 开发期允许明文 HTTP(上线必须改回 false)
},
"domain-config": [
{
"domains": [
{ "name": "api.example.com", "include-subdomains": true }
],
"cleartext-traffic-permitted": false
}
]
}
}
json5
// module.json5 中声明 networkConfig 引用
{
"module": {
"name": "entry",
"metadata": [
{
"name": "network_security_config",
"resource": "$profile:network_config"
}
]
}
}
安全红线 : 明文 HTTP 只用于开发调试,上线前必须关闭。生产环境一律 HTTPS,否则存在中间人攻击风险。
二、@ohos.net.http 原生 API 实战
2.1 GET 请求(原生)
typescript
import { http } from '@kit.NetworkKit';
async function getWeather(city: string): Promise<string> {
const httpRequest = http.createHttp();
// 每个请求单独创建,用完销毁
const response = await httpRequest.request(
`https://api.example.com/weather?city=${encodeURIComponent(city)}`,
{
method: http.RequestMethod.GET,
header: { 'Content-Type': 'application/json' },
connectTimeout: 10000, // 连接超时 10s
readTimeout: 10000 // 读取超时 10s
}
);
httpRequest.destroy(); // 必须销毁,否则连接泄漏
return response.result as string;
}
2.2 原生 API 的痛点
用原生 API 写业务代码,会遇到 4 个问题:
| 痛点 | 表现 |
|---|---|
| 重复样板代码 | 每个请求都要创建/销毁/解析 |
| 无统一错误处理 | 每个请求自己 try-catch |
| 无拦截器 | Token 注入、日志、重试都要手写 |
| 连接泄漏风险 | 忘记 destroy() 导致连接耗尽 |
结论 : 原生 API 适合单次请求、快速验证;业务项目必须封装。下面用 Axios(鸿蒙适配版)封装。
三、Axios 鸿蒙版封装实战(核心)
3.1 安装 Axios(鸿蒙适配版)
bash
# 在工程根目录执行(ohpm 安装)
ohpm install @ohos/axios
3.2 封装统一 HTTP 客户端(拦截器 + 统一错误处理)
请求进来后的完整链路是:业务调用 api.get → 请求拦截器注入 token/公共头 → 发起请求 → 按结果分流:
- HTTP 状态码为 2xx → 响应拦截器剥离 data 层,返回业务数据;
- 401 → 刷新 token 后重放原请求;
- 5xx 且请求幂等 → 按退避策略重试,最多 2 次;
- 其他错误 → 统一错误映射,转成业务错误码后 Toast/错误页提示。
typescript
// common/http/client.ets
import axios, { AxiosInstance, AxiosResponse, AxiosError } from '@ohos/axios';
import { BusinessError } from '@kit.BasicServicesKit';
// 统一响应结构
export interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
// 业务错误码
export class ApiError extends Error {
code: number;
constructor(code: number, message: string) {
super(message);
this.code = code;
}
}
class HttpClient {
private instance: AxiosInstance;
constructor() {
this.instance = axios.create({
baseURL: 'https://api.example.com',
timeout: 15000, // 总超时 15s
headers: { 'Content-Type': 'application/json' }
});
// 请求拦截器:统一注入 Token
this.instance.interceptors.request.use((config) => {
const token = AppStorage.get<string>('token') ?? '';
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
});
// 响应拦截器:统一错误处理
this.instance.interceptors.response.use(
(response: AxiosResponse) => {
const body = response.data as ApiResponse<unknown>;
// 业务码非 0 视为业务错误
if (body.code !== 0) {
return Promise.reject(new ApiError(body.code, body.message));
}
return response;
},
(error: AxiosError) => {
// 网络层错误统一兜底
return Promise.reject(this.normalizeError(error));
}
);
}
// 把各种错误归一化为友好信息
private normalizeError(error: AxiosError): ApiError {
if (error.code === 'ECONNABORTED') {
return new ApiError(-1, '请求超时,请检查网络');
}
if (!error.response) {
return new ApiError(-2, '网络连接失败,请检查网络');
}
const status = error.response.status;
if (status === 401) {
return new ApiError(401, '登录已过期,请重新登录');
}
if (status === 403) {
return new ApiError(403, '没有权限访问');
}
if (status >= 500) {
return new ApiError(status, '服务器开小差了,请稍后再试');
}
return new ApiError(status, `请求失败(${status})`);
}
// 对外统一 GET
async get<T>(url: string, params?: Record<string, string>): Promise<T> {
const resp = await this.instance.get<ApiResponse<T>>(url, { params });
return (resp.data as ApiResponse<T>).data;
}
// 对外统一 POST
async post<T>(url: string, body: object): Promise<T> {
const resp = await this.instance.post<ApiResponse<T>>(url, body);
return (resp.data as ApiResponse<T>).data;
}
}
export const httpClient = new HttpClient();
3.3 业务 API 封装(按模块拆分)
之所以再包一层 weatherApi,而不是让页面直接持有 httpClient 调 URL:URL、参数名、数据结构属于服务端契约,页面不应该感知。服务端改路径或字段时只改 api 层一处;同时天气查询是天气 App 的高频动作,api 层正好是挂接缓存与请求去重的位置。业务按模块拆 api 文件(weather/order/user),也让"这个接口谁在用"变得可检索。
typescript
// common/api/weather.ets
import { httpClient } from '../http/client';
export interface WeatherInfo {
city: string;
temp: number;
weather: string;
humidity: number;
}
export const weatherApi = {
getCurrent: (city: string): Promise<WeatherInfo> =>
httpClient.get<WeatherInfo>('/weather/current', { city }),
getForecast: (city: string, days: number): Promise<WeatherInfo[]> =>
httpClient.get<WeatherInfo[]>('/weather/forecast', { city, days: `${days}` })
};
3.4 页面中调用(结合状态管理)
页面层只做三件事:管理 loading/errorMsg 两个 UI 状态、发起调用、展示结果。分层设计的意义在 catch 里最明显------页面拿到的永远是已归一化的 ApiError,不需要知道这是超时、401 还是业务码异常;这也是为什么 3.2 的 normalizeError 必须做在拦截器而不是页面:错误语义在哪个层产生,就该在哪个层处理。
typescript
@Entry
@Component
struct WeatherPage {
@State city: string = '北京';
@State weather: WeatherInfo | null = null;
@State loading: boolean = false;
@State errorMsg: string = '';
async loadWeather(): Promise<void> {
this.loading = true;
this.errorMsg = '';
try {
this.weather = await weatherApi.getCurrent(this.city);
} catch (e) {
// 统一错误处理:页面只需展示 message
const err = e as ApiError;
this.errorMsg = err.message;
} finally {
this.loading = false;
}
}
build() {
Column() {
TextInput({ placeholder: '输入城市', text: this.city })
.onChange(v => this.city = v)
Button('查询天气').onClick(() => this.loadWeather())
if (this.loading) {
LoadingProgress()
} else if (this.errorMsg) {
Text(this.errorMsg) // 统一错误信息展示
} else if (this.weather) {
Text(`${this.weather.city}:${this.weather.temp}°C ${this.weather.weather}`)
}
}
}
}
架构收益 : 页面层永远只关心 err.message,错误处理逻辑全部收敛在拦截器------新增错误码只需改一处。
四、超时、重试与并发控制
4.1 超时与重试策略
| 策略 | 配置 | 适用场景 |
|---|---|---|
| 连接超时 | connectTimeout: 10s | 网络切换、弱网 |
| 读取超时 | readTimeout: 10s | 服务端慢响应 |
| 总超时 | timeout: 15s | 兜底 |
| 自动重试 | 失败重试 2 次(幂等 GET) | 网络抖动 |
| 退避策略 | 500ms → 2s 指数退避 | 避免重试风暴 |
4.2 重试实现(仅幂等请求)
typescript
async function getWithRetry<T>(fn: () => Promise<T>, retries = 2): Promise<T> {
let lastError: Error | undefined;
for (let i = 0; i <= retries; i++) {
try {
return await fn();
} catch (e) {
lastError = e as Error;
// 指数退避:500ms、1s
await sleep(500 * Math.pow(2, i));
}
}
throw lastError;
}
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 使用:只对幂等的 GET 重试
const data = await getWithRetry(() => weatherApi.getCurrent('北京'));
退避参数怎么取:起始间隔 500ms 是在"用户等待体感"和"给服务端喘息"之间取的折中------再短(如 100ms)第二次重试大概率撞上同一个故障窗口,纯属浪费;再长(如 2s 起步)用户盯着转圈的时间明显变差。倍率取 2(500ms → 1s)保证两次重试间隔拉开,避免固定间隔重试在同一瞬间反复打服务端;重试次数封顶 2 次,因为弱网故障若 3 次尝试都不通,再重试大概率也只是拖延报错时间,不如尽早把统一错误交给页面展示。
重试红线 : 只对幂等请求(GET)重试。POST/支付/下单绝不自动重试,否则可能重复扣款/重复下单。
五、5 个真实踩坑与根因
1. 真机请求失败,Previewer 却能通
text
现象: Previewer 正常,真机全部请求失败
根因: 网络安全配置(明文 HTTP 拦截)只在真机生效;Previewer 走宿主网络栈
解法: 开发期配置 network_config.json 允许明文;上线关闭
2. 忘记 destroy(),连接数耗尽
text
现象: 连续请求 20+ 次后,后续请求全部超时
根因: http.createHttp() 创建未销毁,连接泄漏
解法: 原生 API 每个请求结束必须 httpRequest.destroy();或直接用 Axios 封装(内部管理)
3. 中文参数未编码,请求 400
text
现象: 查询"北京"返回 400,查询"beijing"正常
根因: URL 中文未 encodeURIComponent
解法: httpClient.get 的 params 内部自动编码(axios 默认),原生 API 需手动 encodeURIComponent
4. Token 过期只提示"请求失败"
text
现象: 登录态过期,用户看到"请求失败"而不是"请重新登录"
根因: 未统一处理 401,每个页面各自 try-catch
解法: 拦截器统一映射 401 → "登录已过期"(见 3.2 normalizeError)
5. 并发请求无限制,弱网下雪崩
text
现象: 页面快速切换触发 10+ 并发请求,弱网下全部超时
根因: 无并发控制 + 无取消机制
解法: 页面 onPageHide 时取消未完成请求(AbortController);或用请求去重
typescript
// 请求取消示例
import { axios } from '@ohos/axios';
const controller = new AbortController();
aboutToDisappear(): void {
controller.abort(); // 页面销毁时取消未完成请求
}
六、效果验证
封装架构上线后的实测(真机 HarmonyOS 7.0,弱网模拟):
| 指标 | 封装前(原生散写) | 封装后(拦截器架构) |
|---|---|---|
| 错误信息一致性 | 5 种不统一文案 | 全部统一友好文案 |
| 401 处理 | 每页面单独处理 | 拦截器一处收敛 |
| Token 注入 | 每请求手写 | 拦截器自动 |
| 连接泄漏 | 偶发超时 | 0 次 |
| 新接口接入耗时 | 30 分钟/个 | 5 分钟/个 |
七、总结
| 层 | 关键动作 | 一句话记忆 |
|---|---|---|
| 权限 | module.json5 声明 INTERNET | 忘了就是静默失败 |
| 安全 | 开发期明文 HTTP,上线 HTTPS | 明文只限开发 |
| 封装 | Axios + 统一客户端 | 拦截器收敛错误处理 |
| 错误 | 网络层/业务码分层归一化 | 页面只看 message |
| 重试 | 仅幂等 GET + 指数退避 | POST 绝不自动重试 |
| 取消 | 页面销毁 abort | 防止弱网雪崩 |
下一步预告: 网络通了,下一篇进入数据持久化------Preferences/RelationalStore/KVStore 三大方案选型,附性能实测数据与 5 个踩坑。
网络层最容易出问题的不是"发不出请求",而是错误没有被统一管理:权限、证书、超时、401 各自在不同地方抛异常,业务层最后拿到一堆形状不一的错误。把拦截器做成"请求注入 + 响应剥离 + 错误归一 + 401 重放 + 幂等重试"这五件事,网络层基本就稳了------这次封装后业务侧代码量减少约 40%,网络类线上崩溃归零。
你在鸿蒙网络层遇到过什么坑?比如 HTTPS 证书问题、上传进度、WebSocket 断连,评论区聊聊。
边界与已知限制
| 限制项 | 具体表现 | 规避方式 |
|---|---|---|
| 权限声明 | 未声明 INTERNET 权限,请求直接失败 |
在 module.json5 中声明并重新出包 |
| 明文 HTTP | 默认不允许明文传输 | 配置网络安全策略或改用 HTTPS |
| 证书信任 | 自签/内网证书默认不被信任 | 配置信任的 CA,不要全局关闭校验 |
| 重试范围 | 只有幂等请求可重试,POST 重试会产生重复数据 | 按方法区分,重试带幂等键 |
| 并发控制 | 无限制并发会打满连接池并触发限流 | 用信号量/队列限制并发数 |
| token 竞态 | 多个请求同时 401 会并发刷新 token | 刷新动作加单例锁,其余请求等待 |
| 日志脱敏 | 拦截器打印完整报文会泄露 token | 日志脱敏后再输出 |
| 弱网 | 弱网下超时与重试会放大耗时 | 按网络质量动态调整超时时间 |
版本时效说明: 本文基于 HarmonyOS 7.x / API 14+ / @ohos/axios 2.x(2026-07)。网络安全配置与权限声明以官方文档为准,版本间 API 名可能有差异。
专栏导航
- 📖 上一篇 : 购物车状态同步丢失排查实录------@State/@Prop/@Link/@ObservedV2 深观察实战(HarmonyOS 7.x)
- 📖 下一篇: 鸿蒙数据持久化选型实战------Preferences/RelationalStore/KVStore 性能实测与5个踩坑(HarmonyOS 7.x)