API 错误

如何处理来自 FourA API 的错误。

错误响应格式

API 会针对所有错误返回扁平化的 JSON 对象。不存在嵌套的 error 对象。当故障包含机器可读的代码时,它会作为顶级字段返回:达到套餐限额时为 reason,在没有可用出口的代理调用中为 code。

{
  "error": "Invalid API key"
}

部分错误在顶层包含额外字段,例如 status、service、retryAfter、current 或 limits:

{
  "error": "Rate limit exceeded",
  "status": 429,
  "service": "single",
  "retryAfter": 5,
  "current": { "concurrency": 12, "rpm": 3000 },
  "limits": { "maxConcurrency": 500, "maxRpm": 3000 }
}

跟踪 request

每个 API response(无论成功还是错误)都包含一个带有该调用 UUID 的 X-FourA-Request-Id header。但 FourA 完全无法读取的 body(格式错误的 JSON,或超过 100 KB 的 body)除外:这类 request 会在分配 ID 之前被拒绝。请在您的端记录该 ID。如果您需要向支持团队咨询特定 request 的处理情况,该 ID 能让我们快速找到对应记录。

curl -i -X POST https://eu.api.foura.ai/api/single/ \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"method": "GET", "url": "https://example.com"}'
# HTTP/1.1 200 OK
# X-FourA-Request-Id: 9f1c4e6c-7b2a-4d3e-8a1f-2c9d8e4a3b15
# Content-Type: application/json
# ...

错误类型

400: Bad Request

请求体缺少必填字段、包含无效值,或指定了 API 拒绝获取的目标。

{
  "error": "Invalid request body format"
}

同样的 400 错误也涵盖 SSRF 防护。如果您的 url 解析为私有、环回或其他保留 IP 地址段(RFC 5735、RFC 6598、IPv6 保留块),该 request 会在离开 FourA 网络之前被拒绝:

{
  "error": "Refusing to fetch <target>: target resolves to a private or reserved IP range. The FourA scraping API only forwards requests to public internet hosts."
}

<target> 是地址,或者是主机名及其解析到的地址。无法解析或不是 http:// 或 https:// 的 URL 同样会返回 400。

无法查询的主机名不会被直接拒绝。与 FourA 无法访问的任何目标一样,调用将返回 HTTP 200 以及 status: 0 和原因(could not resolve <host>: <reason>),且不会计费。

请求体中格式错误的 JSON 会在读取任何字段之前以相同方式被拒绝:

{
  "error": "Invalid JSON in request body"
}

proxy 和 ignoreProxies 字段有其对应的 400 错误。两者均接收早期 response 返回的不透明 proxy ID,因此传入任何其他内容均会导致解码失败:

消息 原因
Invalid proxy format proxy 的值不是 FourA 分发的 proxy ID。传入原始 proxy 地址会导致此错误。
Invalid ignoreProxies format ignoreProxies 中的某个条目不是 proxy ID。
Proxy not found ID 能够正常解码,但已不再解析到活跃出口。请选择新的出口。
Managed exit: this proxy id cannot be pinned to a request 出口存在,但 FourA 不会为指定 request 保持其开启状态。当您的套餐中没有剩余的高级流量时,传入高级出口的 ID 会导致此错误。请复用其返回时所在的 session,或通过 POST /api/proxy/ 发起调用并使用系统分配的出口。

解决方法: 检查 request 是否包含所有必填字段,URL 是否使用 http:// 或 https://,主机名是否解析为公共地址,以及任何 proxy 值是否为从早期 response 中逐字复制的 ID。

这些属于 client_error 结果:request 未离开 FourA,因此未产生任何费用扣除。

401: Unauthorized

您的 API key 缺失或无效。

缺少 key:

{
  "error": "Missing API key. Include X-API-Key header."
}

无效的 key:

{
  "error": "Invalid API key"
}

解决方法: 确认 X-API-Key header 包含有效密钥。如有需要,可从 控制台生成新密钥。

403: 当前套餐不支持

调用请求了当前套餐未包含的 endpoint 或参数。response 会设置 X-FourA-Limit,并在 body 的 reason 字段返回相同代码:

{
  "error": "The browser endpoint is not included in your plan (Free). Upgrade to use it.",
  "reason": "plan_limit_feature",
  "documentation": "https://foura.ai/prices"
}

若 endpoint 不包含在当前套餐中,或在无地理定位功能的套餐中使用 exitCountries,reason 会返回 plan_limit_feature;若在无高级出口节点的套餐中使用 exitClass: premium,则返回 plan_limit_premium。error 字符串会指明具体的 endpoint 或参数。

来自 FourA 的 403 绝不代表目标站点的问题:系统根本未尝试连接目标。目标站点返回的 403 会以 HTTP 200 形式返回,并在响应 body 内包含 status: 403。

解决方法: 移除该参数、调用套餐包含的 endpoint,或升级套餐。此处不会设置 Retry-After,因为重试等待不会改变结果。本次请求未产生费用:结果为 rate_limit,系统仅对 success 计费。

413: Payload Too Large

JSON request body 超出 FourA 允许的最大限制 (100 KB)。响应并非 JSON 格式且不包含 X-FourA-Request-Id,因为请求 body 在被读取前即已被拒绝。

解决方法: 发送体积更小的 data 负载。本次请求未产生费用。

429: Rate Limited

有两种不同的检查会返回 429,且它们包含的字段不同。

套餐自带的限制。 响应会设置 X-FourA-Limit header 指明触发了哪项限制,并在 body 的 reason 字段中附带相同的错误代码:

{
  "error": "Concurrency limit reached: your plan allows 50 simultaneous proxy request(s). Retry when an in-flight request finishes.",
  "reason": "plan_limit_concurrency",
  "documentation": "https://foura.ai/prices",
  "limit": 50,
  "in_flight": 51,
  "retry_after_seconds": 1
}

reason 为 plan_limit_concurrency、plan_limit_rate、plan_limit_browser_daily、plan_limit_credits 或 plan_limit_bandwidth 之一。当等待有助于恢复时,等待时长位于 retry_after_seconds 以及 Retry-After header 中,绝不在 retryAfter 中。plan_limit_browser_daily 两者皆不包含,因为额度将在 UTC 午夜恢复,而非按秒倒计时。未产生任何消耗:结果为 rate_limit,且仅对 success 计费。

平台的共享额度。 无 X-FourA-Limit header,等待时长位于 retryAfter:

{
  "error": "Rate limit exceeded",
  "status": 429,
  "service": "single",
  "retryAfter": 5,
  "current": { "concurrency": 12, "rpm": 3000 },
  "limits": { "maxConcurrency": 500, "maxRpm": 3000 }
}

current 与 limits 描述的是覆盖所有流量的服务状态,而非您的账户状态。此处的拒绝意味着 FourA 处于繁忙状态。

解决方法: 根据 response 中携带的 Retry-After、retry_after_seconds 或 retryAfter 进行等待。遇到并发或 rate limit 时,请限制保持打开的 request 数量,而不是重新发送被拒绝的批次。遇到每日或计费周期限制时,请停止运行。有关所有字段的说明请参阅 Rate Limits,有关模式请参阅 Run Requests in Parallel。

500: Server Error

我们端发生了错误。

解决方法: 短暂延迟后重试 request。如果错误持续存在,请查看 状态页面,或附带失败 response 中的 X-FourA-Request-Id 联系技术支持。

502: Upstream Unavailable

FourA 已连接到自身引擎,但无法使用该响应。

{
  "error": "Upstream unavailable",
  "details": "..."
}

解决办法: 进行短暂退避后重试。这是我们服务端的问题,因此您无需承担任何费用:结果为 service_error,且仅对 success 计费。

504: Upstream Timeout

引擎未能在该 request 的时间预算内完成。

{
  "error": "Upstream timeout",
  "details": "the backend did not finish inside the time budget for this request"
}

504 错误与执行耗时有关,与您的 key、参数或 proxy 无关。目标站点响应慢、冷启动验证求解以及大型页面是常见原因。

解决方法: 调高 request 中的 timeout_ms(Single 最高支持 120000,Browser 最高支持 120000,Auto 最高支持 180000),或重试。FourA 会等待您声明的配额时间加上额外的小幅缓冲,因此增加时间确实可以提供更多处理时间。

503: Service Disabled or At Capacity

503 表示服务因维护暂时不可用,或者平台的并发配额已满。两种情况都包含相同的键:error、status、service、retryAfter、current 和 limits。请通过 error 字符串区分它们,而不是通过存在哪些字段。

{
  "error": "Service disabled",
  "status": 503,
  "service": "single",
  "retryAfter": 60,
  "current": { "concurrency": 0, "rpm": 0 },
  "limits": { "maxConcurrency": 500, "maxRpm": 3000 }
}

Service disabled 表示维护中,且两个计数器的 current 均显示为 0,因为 request 在进行任何计量之前就已被拒绝。Service at capacity 是并发形式,其中 current 包含平台的实际使用量。有关该结构,请参见 Rate Limits。

**解决方法:**等待 retryAfter 秒,然后重试。状态页面列出了处于活动状态的维护窗口。

第三种 503 形式不包含 retryAfter。这意味着当您的调用到达时,endpoint 背后的引擎正在重启:

{
  "error": "Backend service unavailable",
  "backend_status": 503
}

请在 1 到 2 秒后重试。

从 /api/auto/ 读取失败信息

只要梯级运行,POST /api/auto/ 就会返回 HTTP 200,即使每个梯级都失败也是如此。实际结果位于 body 中:

{
  "status": 403,
  "error": "exit blocked by the target defense",
  "attempts": 7,
  "meta": { "rung": "fail", "solved": false, "attempts": 7, "credits": 47 }
}

status 是目标最后返回的状态,或者在没有任何尝试成功连接目标时为 502(如果时间预算先耗尽则为 504)。Auto 无法接受的 request 字段(例如小于 5000 或大于 180000 的 timeout_ms)也会以相同方式返回:在进行任何尝试之前且不产生费用,返回 HTTP 200,其中包含 "status": 400 并在 error 中说明原因。

因此不要根据 Auto 的传输状态进行分支判断。请改为从 body 中读取 status 和 error。来自 /api/auto/ 的真正非 200 状态意味着 FourA 在阶梯策略启动前拒绝了调用,或者无法完成调用:400(格式错误的 JSON,或私有/保留的目标地址)、401、413、502、503 或 504。无论限制来自您还是平台,都会在 200 响应中返回,其具体状态包含在 body 内。

当某个站点连续多次 Auto 调用失败时,Auto 会在一段时间内直接返回而不进行尝试:"error": "target temporarily unservable, retry later"、"status": 503 以及以秒为单位的 retryAfter。这不会产生任何费用;请等待 retryAfter 秒。

某个子调用达到套餐限制时同样返回 HTTP 200。Body 本身即为拒绝信息,包含其 reason,加上 status 和 meta,且 response 携带与直接拒绝相同的 X-FourA-Limit header:

{
  "status": 429,
  "error": "Credit limit reached: 75,000 of 75,000 credits used this billing period. Upgrade your plan or wait for the reset.",
  "reason": "plan_limit_credits",
  "documentation": "https://foura.ai/prices",
  "used": 75000,
  "hard_stop": 75000,
  "retry_after_seconds": 86400,
  "resets_at": "2026-10-01T00:00:00.000Z",
  "meta": { "rung": "fail", "solved": false, "attempts": 1, "credits": 0 }
}

哪些限制会终止阶梯策略、哪些限制仅关闭单个层级,详见 Smart Fetch (Auto)。

200 OK 内部的目标端失败

并非所有失败都会体现为非 2xx HTTP 状态码。当目标返回 HTTP 200,但 FourA 的响应包含 error(例如您的 validate 规则拒绝了响应体),或者响应体是 FourA 能够识别的验证页面时,结果为 application_error。当目标返回非 2xx 且您的 validate 规则不接受时,结果为 application_fail,响应体将原样透传。

这两种情况均不计费,仅 success 会计费。当 FourA 的所有浏览器均处于忙碌状态时,Browser 也可能返回带有 "error": "No available browser slot" 的 HTTP 200。这不会计费,请在数秒后重试。Outcomes 参考文档涵盖了完整的分类说明。

通过您固定的 proxy 发起的 Single 调用也可能在响应体旁返回带有 "error": "The exit gave the same answer for <n> different sites" 的 HTTP 200。FourA 检测到该出口向无关站点返回了相同的页面,因此该页面属于出口节点自身,而非您请求的页面。其结果为 application_error 且不计费。请从 POST /api/proxy/ 获取新的出口,系统会自动跳过此类出口。

响应编码

FourA 会将响应体自动解码为 UTF-8。如果目标服务器返回 windows-1251、gbk、shift_jis、iso-8859-* 或在 Content-Type header 与 HTML <meta charset> 标签中声明的任何其他字符集,您都会在 data(single、proxy)或 body(browser)字段中收到标准的 UTF-8 字符串。

对于二进制负载(图片、protobuf、原始音频),请在请求中设置 returnBuffer: true。此时 Single 和 Proxy 会将 data 作为包含原始字节的对象返回,即 {"type": "Buffer", "data": [<byte values>]},且不应用任何字符集转码。

重试策略

实用的重试策略:

import time
import requests

# Plan limits that no short wait will clear: stop the run instead of retrying.
STOP_ON = {
    "plan_limit_feature",
    "plan_limit_premium",
    "plan_limit_browser_daily",
    "plan_limit_credits",
    "plan_limit_bandwidth",
}

def make_request(url, payload, api_key, max_retries=3):
    for attempt in range(max_retries):
        resp = requests.post(
            url,
            headers={"X-API-Key": api_key, "Content-Type": "application/json"},
            json=payload,
        )
        if resp.status_code == 200:
            return resp.json()

        body = resp.json() if resp.headers.get("content-type", "").startswith("application/json") else {}
        request_id = resp.headers.get("X-FourA-Request-Id", "?")

        limit = resp.headers.get("X-FourA-Limit")
        if limit in STOP_ON:
            raise RuntimeError(f"{limit} (request {request_id}): {body.get('error')}")

        # Plan limits send Retry-After and retry_after_seconds; platform limits send retryAfter.
        header = resp.headers.get("Retry-After")
        retry_after = (
            int(header) if header and header.isdigit()
            else body.get("retry_after_seconds") or body.get("retryAfter") or 2 ** attempt
        )

        if resp.status_code in (429, 503):
            time.sleep(retry_after)
            continue
        if resp.status_code >= 500:   # 500, 502, 503, 504 are all ours to fix
            time.sleep(2 ** attempt)
            continue

        # 400/401/403/404 won't fix themselves
        raise RuntimeError(f"{resp.status_code} (request {request_id}): {body.get('error')}")

    raise RuntimeError(f"Exhausted {max_retries} retries")

Proxy 失败附带报告

耗尽尝试次数的 POST /api/proxy/ 调用会以包含错误封包的 HTTP 200 返回,而非 HTTP 错误码。错误字符串很短且格式始终一致,因此旁边会附带一个包含计数信息的 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
}

在错误旁记录 attemptReport.summary,即可了解出口节点是被封禁、失效,还是返回了被您自定义 validate 规则拒绝的页面。字段参考以及针对各项计数的处理方法:Proxy Request 耗尽重试次数的原因。

相关内容

更新于: 2026年9月30日