为什么 Proxy Request 耗尽重试次数

问题说明

一次 POST /api/proxy/ 调用返回错误且无数据。错误信息简短且格式固定:

{
  "error": "Download maxTry limit reached",
  "total": 34.812,
  "request": { "...": "..." }
}

无论是因为所有出口都被封禁、所有出口均失效,还是 FourA 几乎在每次尝试中都获取到了真实页面但被您自己的 validate 规则丢弃,该句子的表述都完全相同。这三种情况需要截然相反的修复方案。

解决方案:attemptReport

每个失败的 Proxy 响应都会在错误信息旁附带一个 attemptReport 对象。它统计了各次尝试实际遇到的情况:

{
  "error": "Download maxTry limit reached",
  "attemptReport": {
    "total": 25,
    "noResponse": 0,
    "defense": 0,
    "contentRejected": 25,
    "statusRejected": 0,
    "other": 0,
    "vendors": [],
    "profilesTried": ["default"],
    "summary": "25 attempt(s): 25 returned HTTP 200 with no defense present and were rejected only by your validate.data - the page was fetched, your content rule did not match it"
  },
  "total": 34.812
}

error 字符串特意保持未修改,以便匹配它的客户端能继续正常工作。如果需要单行概述,请读取 attemptReport.summary;如果需要基于计数进行逻辑分支,请读取各项计数。

字段

字段 类型 统计内容
total integer 已发起的尝试次数
noResponse integer 出口节点未响应,因此从未连上目标站点
defense integer 站点已响应,且在响应中识别到 Bot 拦截服务商
contentRejected integer HTTP 200,无 Bot 拦截,仅被您的 validate.data 拒绝
statusRejected integer 站点已响应,无 Bot 拦截,被您的 validate.status 拒绝
other integer 已响应,且不属于上述任何情况
vendors string[] 任务中检测到的所有 Bot 拦截服务商
profilesTried string[] 任务发送的浏览器 Profile,按首次使用顺序排列。default 表示请求完全按照您的原始配置发出。
summary string 根据统计计数生成的一句话说明。可安全用于记录日志或向用户展示。

解读数据

contentRejected 偏高

页面已成功返回,但您的 validate.data 规则未能匹配。

这是您可以自行修复的问题,也是被其他所有指标掩盖的问题:在所有指标看来请求全部失败,而 FourA 全程都在成功交付真实内容。在不使用任何 validate 的情况下通过 POST /api/single/ 获取一次页面,查看实际返回的内容,并针对返回内容重写规则。

常见原因是将同一套规则应用到了结构不同的页面集合上。例如,文章页存在的选择器在视频页不存在,每次请求到视频页都会失败,且会一直持续并产生完整计费。

statusRejected 偏高

站点已响应,但被您的 validate.status 规则拒绝。如果这些状态码是 401、403、429 或 503,说明站点是在拒绝客户端,而不是页面不存在。可以尝试:

  • 更换其他浏览器 Profile(在内部 request 对象上配置 browserosversion
  • 如果内容有地域限制,使用 exitCountries
  • 如果拒绝需要 JavaScript 执行才能解除,使用 POST /api/browser/

defense 偏高

在响应中识别到了 Bot 拦截,vendors 会列出具体服务商。关于 FourA 目前可直接绕过与仅作检测报告的分类,请参阅 Anti-Bot Defenses。如果该服务商无法在此 endpoint 上绕过,请将调用迁移至 POST /api/browser/POST /api/auto/

noResponse 偏高

出口节点完全未收到响应,因此无法获取目标的任何状态信息。请调大 maxTries,调大 timeout_ms,并检查该 URL 是否可以从公网正常解析。

other 偏高

目标已响应,但未命中上述任何分类。请比对 total_time 与您的 timeout_ms:目标响应耗时超出预算时会归入此类。

浏览器 Profile 轮换

当目标网站拒绝 FourA 发送的浏览器特征时,Proxy 会停止使用该特征,并尝试 公共 profile 目录中的其他系列。这不会消耗额外的尝试次数:轮换只会改变重试时发送的内容,而不会决定是否进行重试。

profilesTried 用于查看该过程的具体发生情况。仅包含一条记录表示每次发送的 request 均与定义完全一致。包含多条记录则表示执行了轮换,但网站拒绝了其中的每一次尝试,这与完全未轮换的情况截然不同。

成功的 Proxy response 中,仅当轮换选择了非您指定的浏览器时,才会出现 profile 字段:

{
  "status": 200,
  "data": "<!doctype html>...",
  "proxy": "A1B2C3",
  "profile": "...",
  "total": 4.108
}

该值是来自 GET /api/profiles 的目录 ID。缺失表示 request 完全按原样发出。存在则表示最终生效的浏览器并非你输入的浏览器,因此在后续调用中应将该 ID 作为 profile 传回,而不是重放失败的 ID。控制台 Playground 会通过 Carry 自动为你处理此操作。

在你的 request 中显式指定的 profilebrowserosversion 绝不会被覆盖。携带自定义 User-AgentCookie header 的 request 也不会被覆盖,因为 clearance 已与获取它时所用的签名绑定。

在代码中读取

import requests

API = "https://eu.api.foura.ai"
H = {"X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json"}

r = requests.post(f"{API}/api/proxy/", headers=H, json={
    "maxTries": 5,
    "request": {
        "method": "GET",
        "url": "https://example.com/product/42",
        "validate": {"data": {"accept": ["Add to cart"]}},
    },
}).json()

if "error" in r:
    rep = r.get("attemptReport", {})
    print(rep.get("summary", r["error"]))

    if rep.get("contentRejected", 0) > rep.get("total", 0) / 2:
        # The pages arrived. The validate rule is what threw them away.
        raise SystemExit("validate.data did not match the real page")
    if rep.get("defense", 0):
        print("bot check met:", ", ".join(rep.get("vendors", [])))

相关内容

更新于: 2026年8月31日