有个容易被忽略的细节:图像超分页面不一定卡在模型推理上。用户放大了某张图片,应用为了拿到远端高清派生图,顺手发起一次回源查询;接口返回429,页面却立刻重发,再返回429。界面看似一直在努力,实际是在增加服务端压力。更麻烦的是,连续点击“重新加载”会制造多个计时器,已经关闭的页面也可能被迟到回包写回。

这次把问题限制在HTTP回源资源领取,不讨论超分算法自身。示例项目叫RateFrame,页面RateFramePage,任务号固定RF-909,目标素材IMG-072。演示脚本预置两次响应:17:45:10首次请求得到429和Retry-After: 5,等待五秒后第二次得到200,最终状态READY。这个时间线属于设计模型,未真实访问服务器、也未运行图像超分能力。

一、先把429当成调度信号,而不是普通失败

收到401、404、429或503时,业务处理不应全部走同一个“网络错误重试”分支。401需要重新判断身份,404可能是资源不存在,503可能是服务暂时不可用;429表达的是请求频率受限。它有机会携带Retry-After,告诉客户端何时再试。这个头不是必然存在,也不能无边界地相信其数值。

RateFrame为响应定义一份很窄的账本:任务ID、图片业务ID、尝试次数、HTTP状态码、下一次允许重试的时刻、页面代次以及当前阶段。阶段只接受IDLE→REQUESTING→COOLDOWN→REQUESTING→READY,必要时进入FAILED或CLOSED。按钮并不是请求的拥有者,它只是修改状态机的入口。当阶段是COOLDOWN,再点十次按钮也只能观察同一时钟,不能生成十个新请求。

这样的模型还有一个好处:真正的超分算子、图片缓存与网络回源可以分开。对于已经在本地可看的低清预览,429不应导致整页空白;界面继续展示旧图和重试倒计时。只有准备替换资源的那一刻才需要核对回源结果是否属于当前图片。用户切换到其他素材时,IMG-072的迟到回包不能覆盖新的图片。

二、HTTP请求对象要一轮一建、一轮一销

HarmonyOS Network Kit的http.createHttp()创建请求对象,request()发起调用,destroy()负责终止并回收请求任务。下面代码只保留这一层可查证的API,Retry-After解读与界面调度交给应用自己的函数。生产项目还应配置网络权限、HTTPS证书校验、合法域名和实际的返回体格式。

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

interface HttpSnapshot {
  code: number;
  retryAfter: string;
}

export async function readSource(url: string): Promise<HttpSnapshot> {
  const lease = http.createHttp(); // 每次调用独立创建
  try {
    const result = await lease.request(url, {
      method: http.RequestMethod.GET,
      connectTimeout: 8000,
      readTimeout: 8000
    });
    const header = result.header as Record<string, string>;
    return {
      code: result.responseCode as number,
      retryAfter: String(header['Retry-After'] ?? header['retry-after'] ?? '')
    };
  } finally {
    lease.destroy();
  }
}

这里url只能来自通过业务白名单校验的资源地址;本文没有提供可访问的演示服务器。最关键的不是超时数值,而是finally:无论结果正常、拒绝、解析失败还是页面生命周期改变,请求对象都需要有终点。destroy()不能替代应用层代次校验,因为网络回调与界面切换可能交错,资源停止不等于旧回包永远不会抵达回调。

由于响应头字段大小写可能经过网络栈或网关处理,代码仅展示两种常见拼写,正式工程应在适配层对键名统一大小写并接受更宽的头值类型。尤其不能把429响应体当成JPEG文件;状态码分流必须发生在解码之前。

三、Retry-After需要上限、下限和可信时钟

RFC允许Retry-After使用秒数或HTTP日期。产品逻辑并不需要把它直接暴露给页面,而应转成等待毫秒数,并与明确的上限合并。服务端给出极大值时可以提示稍后再试;给出负值、无法解析内容时则采用受控的默认值。这里把演示策略固定为最短1秒、最长30秒、默认2秒,不代表系统SDK内置这些数值。

function clampDelay(ms: number): number {
  return Math.max(1000, Math.min(30000, ms));
}

export function retryDelayMs(raw: string, now: number): number {
  const value = raw.trim();
  if (/^\d+$/.test(value)) {
    return clampDelay(Number(value) * 1000);
  }
  const dateMs = Date.parse(value);
  if (Number.isFinite(dateMs)) {
    return clampDelay(dateMs - now);
  }
  return 2000; // 应用自定义回退值
}

这段纯函数处理的是业务规则,便于用构造样本做单元测试:5得到5000毫秒,空值取默认2000毫秒,超大秒数被限制为30000毫秒。值得注意,HTTP日期依赖设备墙上时间,可能受到时间校准影响;调度器应在得到等待时长后依据单调时间或至少同一时钟基准计算剩余值,不应每次重绘都重新读取响应头。页面只展示倒计时结果,不重新创建计时器。

图中RF-909的“5秒”是第一次429后的业务决策,不是HTTP请求的耗时,更不是超分耗时。写日志时应分别记录请求时间、服务器建议等待、实际开始重试时间,不能把它们合并成一个“执行时间”。如果服务端没有明确允许,涉及计费、创建资源等非幂等POST不能照搬GET的自动重试策略。

四、把计时器和页面生命周期绑定

在当前Demo中,同一个任务只允许一个等待时钟。页面切换时增加世代编号,让回到页面的旧结果失去写入资格。取消计时器只是停止未来触发,世代检查则处理取消前已经排队的回调。下面是控制器的关键结构,UI的@State更新应在实际组件层接收其快照后进行。

export class RetryGate {
  private epoch: number = 0;
  private timerId: number = -1;
  private phase: string = 'IDLE';

  canStart(): boolean { return this.phase === 'IDLE'; }
  begin(): number {
    this.phase = 'REQUESTING';
    return ++this.epoch;
  }
  wait(token: number, ms: number, next: () => void): void {
    if (token !== this.epoch) return;
    this.phase = 'COOLDOWN';
    this.timerId = setTimeout(() => {
      this.timerId = -1;
      if (token !== this.epoch) return;
      this.phase = 'REQUESTING';
      next();
    }, ms);
  }
  complete(token: number): void {
    if (token === this.epoch) this.phase = 'READY';
  }
  close(): void {
    ++this.epoch;
    if (this.timerId >= 0) clearTimeout(this.timerId);
    this.timerId = -1;
    this.phase = 'CLOSED';
  }
}

这个实现是调度骨架,不是整套网络客户端:它没有替代响应体校验、网络权限、界面状态订阅或后台任务机制。组件在aboutToDisappear、主动关闭、切换业务图片时应停止这份租约;如果设计要求退后台继续下载,则应使用相应后台能力与状态持久化,不可寄希望于普通页面计时器一直准时执行。重新进入页面要创建新控制器或显式重置,不能从CLOSED私自跳到READY。

五、演示日志与验收边界

本轮配图采用一套固定的构造数据:17:45:10 [RateFrame] RF-909 attempt=1 code=429;同一秒记录retryAfter=5s gate=COOLDOWN;五秒后记录attempt=2 code=200 state=READY。手机演示页保留IMG-072低清预览,显示尝试2次、等待5秒、最终READY。任何截图出现“真实服务器200”或“已经完成端侧超分”,都超出了这些数据能证明的范围。

实际验收还需要在设备与测试网络下分别模拟429无头、有效秒数、HTTP日期、错误日期、超时、切图、页面销毁以及用户连续点击。观察重点不是日志有多少行,而是请求对象创建数是否与允许的尝试数一致,是否有多余的倒计时,以及旧回包是否能改变新图片。当前实现故意不展示下载速度和模型输出画质,因为没有运行那些链路。

还有两条值得写进验收表的规则。第一,429的重试计时只属于当前素材与当前租约,不能共享为所有图像的全局重试倒计时,否则IMG-072被限速后,用户切到本地缓存完好的IMG-081也会被错误阻塞。第二,当等待时间达到产品上限,界面应提供明确的手动恢复入口,而不是无条件继续自动循环。自动重试需要次数预算,超过预算直接FAILED,保留已显示的预览并给出可解释的提示。此处演示只经历一次429,不能用它替代高并发下的限流压测,更不能把无错误的构造响应解释为服务质量承诺。

从日志治理看,任务号、素材号和attempt应该成组记录,响应体不能整块写入日志,尤其不能把URL查询参数中可能包含的访问令牌照搬进HiLog。正式上线上报只需要状态码类别、受控的等待时长和销毁结果,足够复盘回源调度,也减少隐私与凭证外泄风险。

我更愿意把429视为一条明确的产品边界:用户能继续看原图,但界面不会为了制造“正在加载”的感觉去反复施压服务端。把等待时钟、请求租约和回包所有权分开,下一次再接入真实图像超分输出时,才不会把网络节流问题伪装成图像算法异常。

参考:华为《HTTP数据请求》https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-v5/http-request-V5;IETF RFC 9110 的Retry-After语义。以上接口用法依据可访问的官方文档整理,未进行真实设备验证。

Logo

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

更多推荐