# 错误、robots.txt 拒绝和重试

> 所有 WebReaderAPI 错误码：robots.txt 拒绝、站点拒绝、超时和积分错误，哪些应重试，以及为什么这些错误从不计费。

- URL: https://webreaderapi.com/zh/docs/errors/
- 最后更新: 2026-10-22
- 发布者: WebReaderAPI (https://webreaderapi.com)

每个错误都有 HTTP 状态码、稳定的 `code` 和可读的 `message`。它们都不会消耗积分。有些值得重试；来自站点的拒绝是最终结果，我们绝不会绕过它们。

**快速回答 — WebReaderAPI 错误码是什么意思，哪些应该重试？**

**WebReaderAPI** 错误分为三组，且都不会计费。请求问题（`invalid_url`、`invalid_api_key`、`insufficient_credits`）需要你在自己这边修复。站点决定（`blocked_by_robots`、`blocked_by_site_policy`、`target_refused`）是最终结果。临时故障（`fetch_failed`、`fetch_timeout`、`concurrency_limit`）可以用退避策略重试。积分起步于 US$0.07/千页，使用加密货币支付，无需 KYC。

## API 可以返回哪些错误码？

响应体始终是 `{"error": {"code", "message"}}`，如果拒绝来自站点，还会包含 `target_status`。

| HTTP | `code` | 原因 | 重试？ |
| --- | --- | --- | --- |
| 400 | `invalid_url` | 不是 http 或 https、网址过长，或是私有、本地或云元数据地址 | 不重试：修正 URL |
| 400 | `invalid_parameter` | 某个参数的值超出了其可选列表或范围 | 不重试：修正请求 |
| 401 | `invalid_api_key` | 密钥缺失、错误或已被撤销 | 不重试：检查 API 密钥 |
| 402 | `insufficient_credits` | 你的余额已无可用积分 | 购买积分包后可重试 |
| 403 | `blocked_by_robots` | 该站点的 robots.txt 禁止 WebReaderAPI 或所有机器人访问该页面 | 不重试：最终结果 |
| 403 | `blocked_by_site_policy` | 站点声明了 `Content-Signal: ai-input=no` | 不重试：最终结果 |
| 422 | `unsupported_content` | 我们不读取的类型（视频、压缩包）或文件过大 | 否 |
| 429 | `concurrency_limit` | 同一时间运行的请求过多 | 可以，退避后重试 |
| 502 | `target_refused` | 站点拒绝：403、429、验证挑战页或登录墙；其状态码在 `target_status` 中 | 不重试：最终结果 |
| 502 | `fetch_failed` | DNS、TLS 或连接错误，或站点返回 404 或 410 | 重试一两次，并退避 |
| 504 | `fetch_timeout` | 站点在 `timeout` 内未响应 | 可以，退避后重试或延长超时 |

## 为什么 robots.txt 和站点拒绝属于最终结果？

因为这是站点所有者的决定，而 WebReaderAPI 的设计就是遵循它。我们遵守 robots.txt：我们的爬虫以 WebReaderAPI 标识自身，遵循专为其或为所有爬虫编写的规则，不读取网站禁止的页面。被拒绝的页面不扣费。如果网站在其 robots.txt 中声明 Content-Signal: ai-input=no，我们不会为你读取其页面。

当站点以其他方式拒绝请求，例如 403、429、验证挑战页或登录表单时，你会得到 `target_refused`，并在 `target_status` 中看到站点自己的状态码。如果网站拒绝我们的请求，我们会返回其答复：我们从不破解 CAPTCHA、不登录账户，也不试图绕过封锁或付费墙。一分钟后重试同一 URL 通常得到相同结果，因此请把这些状态码当作结果而非失败：记录后继续。

## 如何排查页面为什么被拒绝？

打开站点的 `/robots.txt`，查找指名 `WebReaderAPI` 的分组，或适用于所有机器人的 `*` 分组。匹配该路径的 `Disallow` 行可解释 `blocked_by_robots`。诸如 `Content-Signal: ai-input=no` 的行则可解释 `blocked_by_site_policy`。

我们会保留每份 robots.txt 的副本，最长 24 小时。如果站点刚刚向机器人开放某个版块，变更最多需要一天才能生效。站点所有者可在[我们的爬虫页面](https://webreaderapi.com/zh/bot/)上了解我们爬虫的行为方式，以及如何允许或屏蔽它。

## 临时性失败应如何重试？

只重试 `concurrency_limit`、`fetch_timeout`，以及重试一两次 `fetch_failed`。每次重试前先等待，并让等待时间每次翻倍，再加一点随机抖动，避免并行 worker 同时重试。重试 4 或 5 次后停止。

对于 `concurrency_limit`，更好的办法是减少同时发送的请求数：每个账户都有同时读取页面数的上限；你的控制台会显示该上限，支持团队可将其提高。对于 `fetch_timeout`，在重试慢站点前将 `timeout` 提高到最多 60 秒。重试不产生费用，因为失败的尝试不计费。

*Python：只重试那些可能变化的错误*

```python
import os
import random
import time

import requests

API = "https://webreaderapi.com/api/v1/read"
HEADERS = {"Authorization": "Bearer " + os.environ["WEBREADER_API_KEY"]}
RETRY = {"concurrency_limit", "fetch_timeout", "fetch_failed"}
FINAL = {"blocked_by_robots", "blocked_by_site_policy", "target_refused", "unsupported_content"}


def read_page(url, attempts=5, **options):
    wait = 1.0
    for attempt in range(1, attempts + 1):
        resp = requests.get(API, headers=HEADERS, params={"url": url, **options}, timeout=75)
        if resp.status_code == 200:
            return resp.json()
        error = resp.json().get("error", {})
        code = error.get("code", "unknown")
        if code in FINAL:
            return {"url": url, "skipped": code, "target_status": error.get("target_status")}
        if code not in RETRY or attempt == attempts or (code == "fetch_failed" and attempt >= 3):
            raise RuntimeError(f"{code}: {error.get('message')}")
        time.sleep(wait + random.uniform(0, wait / 2))
        wait = min(wait * 2, 30)
    raise RuntimeError("unreachable")


if __name__ == "__main__":
    print(read_page("https://example.com/blog/first-post", max_chars=20000).get("title"))

```

*Node.js：同样的规则，配合指数退避*

```javascript
const RETRY = new Set(['concurrency_limit', 'fetch_timeout', 'fetch_failed']);
const FINAL = new Set(['blocked_by_robots', 'blocked_by_site_policy', 'target_refused', 'unsupported_content']);
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

export async function readPage(url, options = {}, attempts = 5) {
  let wait = 1000;
  for (let attempt = 1; attempt <= attempts; attempt++) {
    const params = new URLSearchParams({ url, ...options });
    const res = await fetch(`https://webreaderapi.com/api/v1/read?${params}`, {
      headers: { Authorization: `Bearer ${process.env.WEBREADER_API_KEY}` },
    });
    const body = await res.json();
    if (res.ok) return body;
    const code = body.error?.code ?? 'unknown';
    if (FINAL.has(code)) return { url, skipped: code, target_status: body.error.target_status };
    if (!RETRY.has(code) || attempt === attempts) throw new Error(`${code}: ${body.error?.message}`);
    await sleep(wait + Math.random() * (wait / 2));
    wait = Math.min(wait * 2, 30000);
  }
}

```

## 批量或整站抓取 (crawl) 中的错误怎么办？

单个页面失败不会让整个任务停止。该页面会以 `{url, error}` 的形式出现在 `results` 中，并带有与上面相同的错误代码，且不产生任何费用。在一次整站抓取 (crawl) 中，如果某个分区遇到 `blocked_by_robots`，它会直接读取网站的其余部分。

任务结束时，统计错误代码。同一网站上出现大量 `target_refused`，说明该网站不接受自动化读取；出现大量 `fetch_timeout`，说明网站较慢，在任务选项中设置更长的 `timeout` 可能会有帮助。

## 错误：常见问题

### 被 robots.txt 阻止的页面会收费吗？

不会。1 积分 = 读取 1 个页面且无需浏览器；当页面需要浏览器渲染 JavaScript 时为 2 积分。失败的请求，或被网站 robots.txt 拒绝的请求，不扣费。本页上的其他所有错误同样如此。

### 如果我要求，你们能读取阻止你们爬虫的页面吗？

不能。我们尊重网站所有者的选择，且该政策对所有客户一致。如果你拥有该网站，请在 robots.txt 中为 `WebReaderAPI` 添加一条规则；[爬虫页面](https://webreaderapi.com/zh/bot/)说明了具体做法。

### 为什么一个存在的页面会返回 fetch_failed？

网站可能当时宕机、接受 TLS 连接缓慢，或对自动化客户端返回 404。请用退避策略重试一两次。如果持续失败，请在浏览器中检查该地址。

### 在被拒绝后，我的 agent 应该告诉用户什么？

简单说明即可，例如该网站不允许对该页面进行自动化读取。然后让模型使用其他来源。对大多数模型来说，把错误代码作为工具结果返回就足够了。

---

WebReaderAPI: WebReaderAPI 是一款面向 AI agents 的网页读取 API，可将任何公开网页转换为干净的 Markdown，用加密货币支付，无需上传身份证件。预付费积分低至 US$0.07/千页，永不过期，且遵守 robots.txt。无需 KYC — 无身份证、无自拍、无证件。加密货币支付。
