HarmonyOS开发HTTP请求配置:超时、重试、拦截器

打造健壮的网络请求层,让应用在弱网环境也能稳如泰山

一、为什么需要高级配置?

你有没有遇到过这种情况:用户在电梯里打开你的App,页面转圈转了半分钟最后提示"网络错误";或者提交订单时因为网络抖动,用户以为没成功又点了一次,结果下了两单。

这些问题的根源都是网络请求配置不当。移动端网络环境复杂多变——WiFi、4G、5G、弱信号、网络切换、服务器响应慢……如果只是简单地发请求、等响应,用户体验必然糟糕。

高级HTTP配置要解决三个核心问题:

超时控制:不能让用户无休止地等待。该断就断,断得优雅,给用户明确反馈。

重试机制:网络瞬断很常见,自动重试能大幅提升成功率。但重试也要有策略,不能无脑重试。

拦截器设计:统一处理token注入、日志记录、错误转换等横切逻辑,让业务代码更纯粹。

二、核心原理:请求配置的架构设计

未达上限

已达上限

业务代码发起请求

请求拦截器

注入Token?

添加Authorization头

跳过

记录请求日志

发送HTTP请求

响应超时?

触发超时处理

接收响应

重试次数?

响应拦截器

等待重试间隔

返回错误

响应成功?

返回数据

统一错误处理

2.1 超时机制详解

超时分为两种:

超时类型 含义 默认值 建议值
connectTimeout 建立TCP连接的超时时间 60000ms 15000-30000ms
readTimeout 等待服务器响应的超时时间 60000ms 根据接口特性调整

连接超时:从发起请求到TCP连接建立完成。如果服务器宕机或网络不通,这个超时会触发。建议设置较短,快速失败。

读取超时:从发送完请求到收到完整响应。如果服务器处理慢、响应体大,这个超时会触发。建议根据接口特性设置。

2.2 重试策略设计

重试不是简单的"失败就再来一次",需要考虑:

重试条件:哪些错误值得重试?网络错误可以重试,业务错误(如密码错误)不能重试。

重试次数:重试几次合适?通常2-3次,太多会浪费资源。

重试间隔:立即重试还是等待一会儿?建议指数退避,避免雪崩。

幂等性:GET请求天然幂等,POST请求要谨慎重试。

2.3 拦截器模式

拦截器借鉴了OkHttp的设计思想,分为请求拦截器和响应拦截器:

请求拦截器:在请求发出前介入,可以修改请求参数、添加通用头、记录日志。

响应拦截器:在响应返回后介入,可以统一解析、错误转换、缓存处理。

三、代码实战:完整配置实现

场景一:智能超时配置

根据不同接口类型设置不同的超时策略。

import http from '@ohos.net.http';

// 超时配置策略
class TimeoutStrategy {
  // API类型枚举
  static readonly API = {
    // 普通查询接口:快速响应
    QUERY: { connect: 15000, read: 20000 },
  
    // 登录认证:要求快速
    AUTH: { connect: 10000, read: 15000 },
  
    // 数据提交:允许较长时间
    SUBMIT: { connect: 20000, read: 30000 },
  
    // 文件上传:长时间等待
    UPLOAD: { connect: 30000, read: 120000 },
  
    // 文件下载:长时间等待
    DOWNLOAD: { connect: 30000, read: 300000 },
  
    // 实时数据:低延迟要求
    REALTIME: { connect: 5000, read: 10000 }
  };
  
  // 根据URL自动判断类型
  static detectByUrl(url: string): { connect: number; read: number } {
    if (url.includes('/auth/') || url.includes('/login')) {
      return this.API.AUTH;
    }
    if (url.includes('/upload') || url.includes('/file')) {
      return this.API.UPLOAD;
    }
    if (url.includes('/download')) {
      return this.API.DOWNLOAD;
    }
    if (url.includes('/submit') || url.includes('/save')) {
      return this.API.SUBMIT;
    }
    return this.API.QUERY;
  }
}

// 使用示例
async function smartRequest(url: string, method: http.RequestMethod): Promise<http.HttpResponse> {
  let httpRequest = http.createHttp();
  
  try {
    // 自动检测并应用超时策略
    let timeout = TimeoutStrategy.detectByUrl(url);
  
    let response = await httpRequest.request(url, {
      method: method,
      connectTimeout: timeout.connect,
      readTimeout: timeout.read
    });
  
    return response;
  
  } finally {
    httpRequest.destroy();
  }
}

场景二:指数退避重试机制

实现智能重试,失败后等待时间逐渐增加。

import http from '@ohos.net.http';
import { BusinessError } from '@ohos.base';

// 重试配置
interface RetryConfig {
  maxRetries: number;           // 最大重试次数
  initialDelay: number;          // 初始延迟(毫秒)
  maxDelay: number;              // 最大延迟(毫秒)
  backoffFactor: number;         // 退避因子(指数基数)
  retryableErrors: number[];     // 可重试的错误码
}

// 默认重试配置
const DEFAULT_RETRY_CONFIG: RetryConfig = {
  maxRetries: 3,
  initialDelay: 1000,
  maxDelay: 30000,
  backoffFactor: 2,
  retryableErrors: [
    http.ResponseCode.INTERNAL_ERROR,    // 500
    http.ResponseCode.BAD_GATEWAY,       // 502
    http.ResponseCode.SERVICE_UNAVAILABLE, // 503
    http.ResponseCode.GATEWAY_TIMEOUT    // 504
  ]
};

// 延迟工具函数
function delay(ms: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// 带重试的HTTP请求
async function requestWithRetry(
  url: string,
  options: http.HttpRequestOptions,
  retryConfig: RetryConfig = DEFAULT_RETRY_CONFIG
): Promise<http.HttpResponse | null> {
  
  let lastError: BusinessError | null = null;
  let attempt = 0;
  
  while (attempt <= retryConfig.maxRetries) {
    let httpRequest = http.createHttp();
  
    try {
      console.info(`请求尝试 ${attempt + 1}/${retryConfig.maxRetries + 1}: ${url}`);
    
      let response = await httpRequest.request(url, options);
    
      // 检查是否需要重试
      if (response.responseCode >= 200 && response.responseCode < 300) {
        // 成功,直接返回
        return response;
      }
    
      // 判断是否可重试的错误码
      if (!retryConfig.retryableErrors.includes(response.responseCode)) {
        // 不可重试的错误,直接返回响应
        return response;
      }
    
      console.warn(`服务器错误 ${response.responseCode},准备重试`);
    
    } catch (error) {
      lastError = error as BusinessError;
      console.error(`请求异常: ${lastError.code} - ${lastError.message}`);
    
      // 网络错误通常可以重试
    } finally {
      httpRequest.destroy();
    }
  
    // 检查是否还有重试机会
    if (attempt < retryConfig.maxRetries) {
      // 计算退避延迟:initialDelay * backoffFactor^attempt
      let delayTime = Math.min(
        retryConfig.initialDelay * Math.pow(retryConfig.backoffFactor, attempt),
        retryConfig.maxDelay
      );
    
      console.info(`等待 ${delayTime}ms 后重试...`);
      await delay(delayTime);
    }
  
    attempt++;
  }
  
  // 所有重试都失败了
  console.error(`重试${retryConfig.maxRetries}次后仍然失败`);
  throw lastError || new Error('请求失败');
}

// 使用示例:提交订单(带重试)
async function submitOrder(orderData: object): Promise<boolean> {
  try {
    let response = await requestWithRetry(
      'https://api.shop.com/v1/orders',
      {
        method: http.RequestMethod.POST,
        header: { 'Content-Type': 'application/json' },
        extraData: JSON.stringify(orderData)
      },
      {
        maxRetries: 2,          // 订单提交最多重试2次
        initialDelay: 2000,     // 初始等待2秒
        maxDelay: 10000,        // 最多等待10秒
        backoffFactor: 2,
        retryableErrors: [500, 502, 503, 504]
      }
    );
  
    return response !== null && response.responseCode === 200;
  
  } catch (error) {
    console.error('订单提交失败:', error);
    return false;
  }
}

场景三:拦截器实现

实现类似OkHttp的拦截器机制,统一处理请求和响应。

import http from '@ohos.net.http';

// 请求上下文
interface RequestContext {
  url: string;
  options: http.HttpRequestOptions;
  metadata: Map<string, any>;  // 拦截器间传递的数据
}

// 响应上下文
interface ResponseContext {
  response: http.HttpResponse;
  request: RequestContext;
}

// 拦截器接口
interface Interceptor {
  // 请求拦截:返回修改后的请求配置
  interceptRequest?(context: RequestContext): RequestContext;
  
  // 响应拦截:返回修改后的响应或抛出错误
  interceptResponse?(context: ResponseContext): http.HttpResponse;
}

// Token注入拦截器
class AuthInterceptor implements Interceptor {
  private tokenProvider: () => string | null;
  
  constructor(tokenProvider: () => string | null) {
    this.tokenProvider = tokenProvider;
  }
  
  interceptRequest(context: RequestContext): RequestContext {
    let token = this.tokenProvider();
  
    if (token) {
      // 添加Authorization头
      if (!context.options.header) {
        context.options.header = {};
      }
      (context.options.header as Record<string, string>)['Authorization'] = `Bearer ${token}`;
    
      console.info('已注入认证Token');
    }
  
    return context;
  }
}

// 日志拦截器
class LoggingInterceptor implements Interceptor {
  interceptRequest(context: RequestContext): RequestContext {
    console.info(`[HTTP] >>> ${context.options.method} ${context.url}`);
    console.info(`[HTTP] Headers: ${JSON.stringify(context.options.header)}`);
  
    // 记录请求开始时间
    context.metadata.set('startTime', Date.now());
  
    return context;
  }
  
  interceptResponse(context: ResponseContext): http.HttpResponse {
    let startTime = context.request.metadata.get('startTime') as number;
    let duration = Date.now() - startTime;
  
    console.info(`[HTTP] <<< ${context.response.responseCode} (${duration}ms)`);
  
    return context.response;
  }
}

// 错误处理拦截器
class ErrorHandlerInterceptor implements Interceptor {
  interceptResponse(context: ResponseContext): http.HttpResponse {
    let code = context.response.responseCode;
  
    // 401未授权:跳转登录
    if (code === http.ResponseCode.UNAUTHORIZED) {
      console.warn('Token已过期,需要重新登录');
      // 触发全局登录状态更新
      // EventBus.emit('auth:expired');
    }
  
    // 403禁止访问:提示无权限
    if (code === http.ResponseCode.FORBIDDEN) {
      console.error('无权限访问该资源');
    }
  
    // 5xx服务器错误:记录日志
    if (code >= 500) {
      console.error(`服务器错误: ${code}`);
      // 上报错误日志
      // ErrorReporter.report(context.request.url, code);
    }
  
    return context.response;
  }
}

// HTTP客户端(带拦截器)
class HttpClient {
  private interceptors: Interceptor[] = [];
  
  // 添加拦截器
  addInterceptor(interceptor: Interceptor): void {
    this.interceptors.push(interceptor);
  }
  
  // 执行请求
  async request(url: string, options: http.HttpRequestOptions): Promise<http.HttpResponse> {
    // 构建请求上下文
    let context: RequestContext = {
      url: url,
      options: { ...options },
      metadata: new Map()
    };
  
    // 执行请求拦截器链
    for (let interceptor of this.interceptors) {
      if (interceptor.interceptRequest) {
        context = interceptor.interceptRequest(context);
      }
    }
  
    // 发送实际请求
    let httpRequest = http.createHttp();
  
    try {
      let response = await httpRequest.request(context.url, context.options);
    
      // 执行响应拦截器链
      let responseContext: ResponseContext = {
        response: response,
        request: context
      };
    
      for (let interceptor of this.interceptors) {
        if (interceptor.interceptResponse) {
          response = interceptor.interceptResponse(responseContext);
          responseContext.response = response;
        }
      }
    
      return response;
    
    } finally {
      httpRequest.destroy();
    }
  }
}

// 全局HTTP客户端实例
const globalHttpClient = new HttpClient();

// 初始化拦截器
function initHttpClient() {
  // Token注入
  globalHttpClient.addInterceptor(new AuthInterceptor(() => {
    // 从本地存储获取token
    return 'your_stored_token';
  }));
  
  // 日志记录
  globalHttpClient.addInterceptor(new LoggingInterceptor());
  
  // 错误处理
  globalHttpClient.addInterceptor(new ErrorHandlerInterceptor());
}

// 使用示例
@Entry
@Component
struct ApiDemoPage {
  @State result: string = '';
  
  async aboutToAppear() {
    initHttpClient();
  
    try {
      let response = await globalHttpClient.request(
        'https://api.example.com/user/profile',
        {
          method: http.RequestMethod.GET,
          header: { 'Content-Type': 'application/json' }
        }
      );
    
      if (response.responseCode === 200) {
        this.result = response.result as string;
      }
    } catch (error) {
      console.error('请求失败:', error);
    }
  }
  
  build() {
    Column() {
      Text(this.result || '加载中...')
        .width('100%')
        .padding(20)
    }
  }
}

四、踩坑与注意事项

坑点一:重试导致的重复提交

POST请求重试可能导致重复提交数据,需要特别注意。

// ❌ 错误:POST请求无条件重试
async function badExample() {
  await requestWithRetry(url, {
    method: http.RequestMethod.POST,
    extraData: orderData
  });  // 可能导致重复下单!
}

// ✅ 正确:POST请求使用幂等键
async function goodExample() {
  // 生成唯一幂等键
  let idempotencyKey = `order_${Date.now()}_${Math.random()}`;
  
  await requestWithRetry(url, {
    method: http.RequestMethod.POST,
    header: {
      'Content-Type': 'application/json',
      'X-Idempotency-Key': idempotencyKey  // 服务器根据此键去重
    },
    extraData: orderData
  });
}

// ✅ 另一种方案:POST请求不自动重试,由用户决定
async function safePostRequest(url: string, data: object) {
  // POST请求默认不重试
  return requestWithRetry(url, {
    method: http.RequestMethod.POST,
    extraData: JSON.stringify(data)
  }, {
    maxRetries: 0,  // 不重试
    initialDelay: 0,
    maxDelay: 0,
    backoffFactor: 1,
    retryableErrors: []
  });
}

坑点二:超时时间单位混淆

鸿蒙HTTP超时单位是毫秒,容易与其他库混淆。

// ❌ 错误:误以为是秒
let options = {
  connectTimeout: 30,  // 实际只有30毫秒!
  readTimeout: 60      // 实际只有60毫秒!
};

// ✅ 正确:明确使用毫秒
let options = {
  connectTimeout: 30000,  // 30秒
  readTimeout: 60000      // 60秒
};

// ✅ 推荐:使用常量定义
const SECOND = 1000;
const MINUTE = 60 * SECOND;

let options = {
  connectTimeout: 30 * SECOND,
  readTimeout: 1 * MINUTE
};

坑点三:拦截器顺序问题

拦截器的执行顺序很重要,顺序错误可能导致问题。

// ❌ 错误顺序:日志拦截器在Token注入之前
client.addInterceptor(new LoggingInterceptor());   // 先记录日志
client.addInterceptor(new AuthInterceptor(...));   // 后注入Token
// 结果:日志中没有Token信息

// ✅ 正确顺序:先注入Token,再记录日志
client.addInterceptor(new AuthInterceptor(...));   // 先注入Token
client.addInterceptor(new LoggingInterceptor());   // 后记录日志
// 结果:日志中包含完整请求信息

坑点四:超时后的资源未释放

超时异常发生后,HTTP请求对象可能未被正确销毁。

// ❌ 错误:超时后未销毁
async function badTimeout() {
  let httpRequest = http.createHttp();
  
  try {
    await httpRequest.request(url, {
      connectTimeout: 5000
    });
  } catch (error) {
    // 超时异常,但没有销毁对象
    console.error('超时');
  }
  // httpRequest未销毁,内存泄漏!
}

// ✅ 正确:使用finally确保销毁
async function goodTimeout() {
  let httpRequest = http.createHttp();
  
  try {
    await httpRequest.request(url, {
      connectTimeout: 5000
    });
  } catch (error) {
    console.error('超时:', error);
  } finally {
    httpRequest.destroy();  // 无论如何都销毁
  }
}

五、HarmonyOS 6适配指南

5.1 新增配置项

HarmonyOS 6新增了若干请求配置选项:

// HarmonyOS 6 新增配置
let options: http.HttpRequestOptions = {
  method: http.RequestMethod.GET,
  
  // 新增:请求优先级
  priority: http.RequestPriority.HIGH,
  
  // 新增:HTTP协议版本
  usingProtocol: http.HttpProtocol.HTTP2,  // 支持HTTP/2
  
  // 新增:期望的数据类型
  expectDataType: http.HttpDataType.OBJECT,  // 自动解析为对象
  
  // 原有配置
  connectTimeout: 30000,
  readTimeout: 30000
};

5.2 重试机制优化

HarmonyOS 6提供了系统级的重试支持:

// HarmonyOS 6:系统级重试配置
let options: http.HttpRequestOptions = {
  method: http.RequestMethod.GET,
  
  // 新增:自动重试配置
  autoRetry: {
    maxRetries: 3,              // 最大重试次数
    retryDelay: 1000,           // 重试延迟(毫秒)
    retryOn: [500, 502, 503]    // 触发重试的状态码
  }
};

5.3 拦截器性能优化

HarmonyOS 6对拦截器链的执行进行了优化:

// 使用async/await确保拦截器顺序执行
class AsyncInterceptor implements Interceptor {
  async interceptRequest(context: RequestContext): Promise<RequestContext> {
    // 异步操作,如从存储读取token
    let token = await this.getStoredToken();
  
    if (token) {
      context.options.header = {
        ...context.options.header,
        'Authorization': `Bearer ${token}`
      };
    }
  
    return context;
  }
  
  private async getStoredToken(): Promise<string | null> {
    // 模拟异步读取
    return new Promise(resolve => {
      setTimeout(() => resolve('async_token'), 100);
    });
  }
}

六、总结一下下

HTTP请求的高级配置是打造健壮应用的关键一环。本文从三个维度展开:

超时配置:根据接口特性设置合理的超时时间。快速接口用短超时,耗时操作用长超时。自动检测URL类型并应用对应策略,既保证用户体验,又避免无谓等待。

重试机制:指数退避是重试策略的核心。失败后等待时间逐渐增加,既给服务器喘息机会,又避免雪崩效应。特别注意POST请求的重试问题,使用幂等键或禁用重试。

拦截器设计:借鉴成熟框架的设计思想,将横切逻辑抽离到拦截器中。Token注入、日志记录、错误处理各司其职,业务代码保持纯粹。注意拦截器顺序,先修改再记录。

HarmonyOS 6带来了更多系统级支持,如请求优先级、HTTP/2协议、自动重试等。合理利用这些新特性,可以让网络层更加健壮高效。

下一篇文章,我们将进入HTTPS与证书校验的世界,探讨移动端网络安全的最佳实践!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐