用户登录

技术文章

外部链接将在新标签页打开。

微信小程序 / H5 调用第三方 API 的正确姿势:把直连改成三段式调用链

一句话回答

正确做法不是在页面里直接请求第三方接口,而是拆成三段:不可信的前端、持有密钥的服务端代理、以及上游 API。小程序受平台合法域名约束,H5 受同源策略约束,两者都因为代理而简化;真正决定可用性的,是显式超时、只对幂等请求重试、把错误分类后再决定用户可见状态,并且不把上游原始报错与密钥一起送到客户端。

核心要点 TL;DR
  • 前端直连会暴露密钥;可撤销的密钥只应存在于服务端环境变量中。
  • 小程序合法域名与 H5 CORS 是两条不同的限制,用自有域名做代理可以同时绕开它们的细节。
  • 重试前先确认请求幂等;对非幂等请求重试是重复扣费与重复写入的常见来源。
  • 错误要分类后再返回前端,上游原始报错与 request id 的公开范围需要单独决定。

把问题问得更具体一点:微信小程序和 H5 调用第三方 API 时,代码应该写在哪一侧、密钥放在哪、失败时返回什么?简短回答是:不要在页面里直接请求上游接口。比较稳妥的结构是三段式——前端只调用自有域名下的一个接口,由服务端持有密钥并访问上游,前端只消费归一化后的结果。下面按这三个位置展开,涉及平台规则的部分以官方文档为准(本次未核验具体条款)。

一、先决定谁持有密钥#

密钥暴露与否,取决于请求从哪台机器发出,而不是取决于代码写得多隐蔽。小程序包可以被解包,H5 的 JS 可以被直接阅读,写在里面的密钥等同于公开。

调用位置密钥是否到达用户设备说明
小程序 / H5 代码直连上游会只适合完全公开、无配额的端点;有配额的接口不建议
自有服务端代理否密钥放环境变量,客户端只认自己的域名
云函数 / 边缘函数否,前提是密钥由平台环境变量注入要额外检查日志、冷启动与出网限制

使用 API.PLUS 这类第三方 API 服务时,这一点同样成立:密钥依然只能放在服务端,具体接入方式、可用端点与计费口径以官方文档为准(待核实)。

二、域名与跨域是两条不同的限制#

  • 小程序:网络请求受平台侧合法域名配置约束,通常要求 HTTPS 且不能是裸 IP 或 localhost,具体字段与数量限制请对照微信官方文档。
  • H5:受浏览器同源策略约束。跨域请求会触发预检(OPTIONS),服务端需要返回允许的来源、方法与头部,否则浏览器在拿到响应前就拦掉。

两者的共同解法是同一个:把上游域名藏到服务端,前端只面向自己的域名。这样小程序端只需要配置一个域名,H5 端也就不存在跨域问题。

三、超时、重试与幂等#

客户端库的默认超时往往偏长或不存在,必须显式设置。重试则要分情况:

  1. GET 等幂等请求可以有限重试,建议指数退避加随机抖动,避免同一时刻集体重试。
  2. POST 只有在请求自带幂等键、且上游确实按幂等键去重时才可重试;否则重试会变成重复下单、重复扣费。
  3. 4xx 通常不该重试;429 与 5xx 可以,但要设上限与总时长。

重试放在服务端比放在前端更可控:前端一个页面可能在用户切后台后被挂起,重试节奏就断了。

四、失败路径:先分类,再决定给用户看什么#

把错误至少分成四类:网络不可达、上游超时、上游返回错误、请求本身不合法。这四类对用户的含义不同,可恢复性也不同。工程上要遵守两点:

  • 不要把上游原始报错原样透传给客户端,其中可能含内部端点、账号信息或密钥片段。
  • 记录一个 request id 并回传,方便用户报障时定位;但公开范围要单独评估。

日志同样需要脱敏:请求头里的 authorization、查询串里的 key 参数都不应落盘。

五、一个最小代理示例#

下面代码未执行,仅为结构演示,域名与密钥都是占位(example.com、YOUR_KEY)。环境假设为 Node.js 18+ 与 Express 4.x。

// 未执行的示例:服务端代理,密钥从环境变量注入
import express from "express";

const app = express();
app.use(express.json()); // 否则 req.body 为 undefined

const UPSTREAM = "https://api.example.com/v1/endpoint"; // 占位域名
const KEY = process.env.UPSTREAM_KEY;                  // 不要把 YOUR_KEY 写进代码

app.post("/api/proxy", async (req, res) => {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), 8000); // 显式超时

  try {
    const upstream = await fetch(UPSTREAM, {
      method: "POST",
      headers: {
        "content-type": "application/json",
        authorization: `Bearer ${KEY}`,
      },
      body: JSON.stringify(req.body),
      signal: controller.signal,
    });

    const requestId = upstream.headers.get("x-request-id") ?? "unknown";
    res.setHeader("x-request-id", requestId);

    if (!upstream.ok) {
      // 不透传上游原始报错
      return res.status(upstream.status).json({ error: "upstream_failed" });
    }

    res.json(await upstream.json());
  } catch (err) {
    const timedOut = err.name === "AbortError";
    res.status(timedOut ? 504 : 502).json({
      error: timedOut ? "upstream_timeout" : "upstream_unreachable",
    });
  } finally {
    clearTimeout(timer);
  }
});

app.listen(3000);

注意这段示例没有做鉴权、限流与请求体大小限制。对外可用之前,这三项都需要补上,否则代理会变成任何人都能使用的免费额度出口。

六、边界与仍需核对的事项#

  • 小程序的合法域名规则、并发与超时上限,以微信官方文档为准,本文未逐条核验。
  • 上游 API 的速率限制、计费单位与是否支持幂等键,需查对应服务的文档;不同服务差异很大,不能按通则推断。
  • 若涉及流式响应,代理层需要正确转发分块数据并处理中途断开,本文示例不涵盖这种情况。

总结成一句可执行的判断:密钥在哪台机器上、超时是否显式设置、非幂等请求会不会被重试、上游报错会不会漏到用户端——这四项定下来,调用姿势基本就正确了。

作者:沐小悠

本文约 8 分钟读完 · 关键词:微信小程序 调用第三方 API、H5 跨域 CORS、API 密钥安全、服务端代理、超时与重试

更多文章 GET 示例接口

「」标签下的其他文章