Réponse rapide
Que signifient les codes d'erreur de WebReaderAPI, et lesquels dois-je réessayer ?
Les erreurs WebReaderAPI se répartissent en trois groupes, et aucune n'est jamais facturée. Les problèmes de requête (invalid_url, invalid_api_key, insufficient_credits) nécessitent une correction de votre côté. Les décisions du site (blocked_by_robots, blocked_by_site_policy, target_refused) sont définitives. Les échecs temporaires (fetch_failed, fetch_timeout, concurrency_limit) peuvent être réessayés avec un backoff. Les crédits commencent à 0,07 $US/1K pages, payés en crypto sans KYC.
Quels codes d'erreur l'API peut-elle renvoyer ?
Le corps est toujours {"error": {"code", "message"}}, plus target_status lorsque le refus provient du site.
| HTTP | code | Cause | Réessayer ? |
|---|---|---|---|
| 400 | invalid_url | Ni http ni https, trop long, ou une adresse privée, locale ou de métadonnées cloud | Non : corrigez l'URL |
| 400 | invalid_parameter | Un paramètre a une valeur hors de sa liste ou de sa plage | Non : corrigez la requête |
| 401 | invalid_api_key | Clé absente, erronée ou révoquée | Non : vérifiez la clé |
| 402 | insufficient_credits | Votre solde n'a plus de crédits | Après l'achat d'un pack |
| 403 | blocked_by_robots | Le robots.txt du site interdit la page pour WebReaderAPI ou pour tous les robots | Non : définitif |
| 403 | blocked_by_site_policy | Le site déclare Content-Signal: ai-input=no | Non : définitif |
| 422 | unsupported_content | Un type que nous ne lisons pas (vidéo, archive) ou un fichier trop volumineux | Non |
| 429 | concurrency_limit | Trop de vos requêtes tournent en même temps | Oui, avec backoff |
| 502 | target_refused | Le site a refusé : 403, 429, une page de challenge ou un mur de connexion ; son statut est dans target_status | Non : définitif |
| 502 | fetch_failed | Erreur DNS, TLS ou de connexion, ou le site a répondu 404 ou 410 | Une ou deux fois, avec backoff |
| 504 | fetch_timeout | Le site n'a pas répondu dans les timeout | Oui, avec backoff ou un timeout plus long |
Pourquoi les refus liés au robots.txt et au site sont-ils définitifs ?
Parce qu'ils relèvent de la décision du propriétaire du site, et WebReaderAPI est conçu pour la respecter. Nous respectons robots.txt : notre crawler s'identifie comme WebReaderAPI, suit les règles écrites pour lui ou pour tous les robots, et ne lit pas une page qu'un site interdit. Les pages refusées ne sont pas facturées. Si un site déclare Content-Signal: ai-input=no dans son robots.txt, nous ne lisons pas ses pages pour vous.
Quand un site refuse une requête autrement, par un 403, un 429, une page de challenge ou un formulaire de connexion, vous recevez target_refused et le statut du site dans target_status. Si un site refuse notre requête, nous renvoyons sa réponse : nous ne résolvons jamais de CAPTCHA, ne nous connectons pas à des comptes et ne cherchons pas à contourner un blocage ou un paywall. Réessayer la même URL une minute plus tard donnera généralement la même réponse : traitez donc ces codes comme un résultat, pas comme un échec : journalisez-les et passez à la suite.
Comment vérifier pourquoi une page a été refusée ?
Ouvrez le /robots.txt du site et cherchez un groupe qui nomme WebReaderAPI, ou le groupe * qui s'applique à tous les robots. Une ligne Disallow qui correspond au chemin explique un blocked_by_robots. Une ligne comme Content-Signal: ai-input=no explique un blocked_by_site_policy.
Nous conservons une copie de chaque robots.txt pendant 24 heures au maximum. Si un site vient d'ouvrir une section aux robots, la modification peut prendre jusqu'à un jour pour s'appliquer. Les propriétaires de sites peuvent voir comment notre crawler se comporte, et comment l'autoriser ou le bloquer, sur notre page crawler.
Comment réessayer les échecs temporaires ?
Réessayez uniquement concurrency_limit, fetch_timeout et, une ou deux fois, fetch_failed. Attendez avant chaque nouvelle tentative et doublez l'attente à chaque fois, avec un léger jitter aléatoire pour que les workers parallèles ne réessaient pas ensemble. Arrêtez après 4 ou 5 tentatives.
Pour concurrency_limit, la meilleure solution est d'envoyer moins de requêtes à la fois : Chaque compte a une limite du nombre de pages lues en simultané ; votre dashboard l'affiche, et le support peut l'augmenter. Pour fetch_timeout, augmentez timeout jusqu'à 60 secondes avant de réessayer un site lent. Les tentatives ne coûtent rien, car une tentative échouée n'est jamais facturée.
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"))
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);
}
}
Et les erreurs au sein d'un batch ou d'un crawl ?
Un job ne s'arrête pas parce qu'une page échoue. Cette page apparaît dans results comme {url, error} avec les mêmes codes que ci-dessus, et elle ne coûte rien. Un crawl qui rencontre blocked_by_robots sur une section lit simplement le reste du site.
À la fin d'un job, comptez les codes d'erreur. Beaucoup de target_refused sur un même site signifient que le site n'accepte pas les lecteurs automatisés ; beaucoup de fetch_timeout signifient qu'il est lent, et un timeout plus long dans les options du job peut aider.
Erreurs : questions fréquentes
Suis-je facturé pour une page bloquée par robots.txt ?
Non. 1 crédit = 1 page lue sans navigateur ; 2 crédits quand la page nécessite un navigateur pour exécuter JavaScript. Une requête qui échoue, ou qu'un robots.txt de site refuse, n'est pas facturée. Il en va de même pour toutes les autres erreurs de cette page.
Pouvez-vous lire une page qui bloque votre crawler si je vous le demande ?
Non. Nous respectons le choix du propriétaire du site, et la politique est la même pour tous les clients. Si vous êtes propriétaire du site, ajoutez une règle pour WebReaderAPI dans votre robots.txt ; la page du crawler montre comment faire.
Pourquoi est-ce que j'obtiens fetch_failed pour une page qui existe ?
Le site était peut-être indisponible, lent à accepter une connexion TLS ou renvoyait un 404 aux clients automatisés. Réessayez une ou deux fois avec un backoff. Si l'échec persiste, vérifiez l'adresse dans un navigateur.
Que doit dire mon agent à l'utilisateur après un refus ?
Quelque chose de simple, par exemple que le site n'autorise pas la lecture automatisée de cette page. Laissez ensuite le modèle utiliser d'autres sources. Renvoyer le code d'erreur comme résultat de l'outil suffit pour la plupart des modèles.