Risposta rapida
Cosa significano i codici di errore di WebReaderAPI, e quali conviene riprovare?
Gli errori WebReaderAPI si dividono in tre gruppi, e nessuno viene mai addebitato. I problemi di richiesta (invalid_url, invalid_api_key, insufficient_credits) richiedono una correzione da parte tua. Le decisioni del sito (blocked_by_robots, blocked_by_site_policy, target_refused) sono definitive. I fallimenti temporanei (fetch_failed, fetch_timeout, concurrency_limit) possono essere riprovati con backoff. I crediti partono da 0,07 USD/1K pagine, pagati in criptovalute senza KYC.
Quali codici di errore può restituire l'API?
Il body è sempre {"error": {"code", "message"}}, più target_status quando il rifiuto è arrivato dal sito.
| HTTP | code | Causa | Riprovare? |
|---|---|---|---|
| 400 | invalid_url | Non http o https, troppo lungo, oppure un indirizzo privato, locale o di metadata cloud | No: correggi l'URL |
| 400 | invalid_parameter | Un parametro ha un valore fuori dal suo elenco o intervallo | No: correggi la richiesta |
| 401 | invalid_api_key | Chiave mancante, errata o revocata | No: controlla la chiave |
| 402 | insufficient_credits | Il tuo saldo non ha più crediti | Dopo aver acquistato un pack |
| 403 | blocked_by_robots | Il robots.txt del sito vieta la pagina a WebReaderAPI o a tutti i robot | No: definitivo |
| 403 | blocked_by_site_policy | Il sito dichiara Content-Signal: ai-input=no | No: definitivo |
| 422 | unsupported_content | Un tipo che non leggiamo (video, archivio) o un file troppo grande | No |
| 429 | concurrency_limit | Troppe delle tue richieste sono in esecuzione nello stesso momento | Sì, con backoff |
| 502 | target_refused | Il sito ha rifiutato: 403, 429, una pagina di challenge o un login wall; il suo stato è in target_status | No: definitivo |
| 502 | fetch_failed | Errore DNS, TLS o di connessione, oppure il sito ha risposto 404 o 410 | Una o due volte, con backoff |
| 504 | fetch_timeout | Il sito non ha risposto entro timeout | Sì, con backoff o un timeout più lungo |
Perché robots.txt e i rifiuti del sito sono definitivi?
Perché sono una decisione del proprietario del sito e WebReaderAPI è costruito per rispettarla. Rispettiamo robots.txt: il nostro crawler si identifica come WebReaderAPI, segue le regole scritte per lui o per tutti i robot e non legge una pagina che un sito vieta. Le pagine rifiutate non vengono addebitate. Se un sito dichiara Content-Signal: ai-input=no nel suo robots.txt, non leggiamo le sue pagine per te.
Quando un sito rifiuta una richiesta in un altro modo, con un 403, un 429, una pagina di challenge o un modulo di login, ricevi target_refused e lo stato del sito in target_status. Se un sito rifiuta la nostra richiesta, restituiamo la sua risposta: non risolviamo mai CAPTCHA, non accediamo agli account e non tentiamo di aggirare un blocco o un paywall. Riprovare lo stesso URL un minuto dopo di solito dà la stessa risposta, quindi tratta questi codici come un risultato, non come un errore: registrali e vai avanti.
Come faccio a controllare perché una pagina è stata rifiutata?
Apri il /robots.txt del sito e cerca un gruppo che nomina WebReaderAPI, oppure il gruppo * che vale per tutti i robot. Una riga Disallow che corrisponde al percorso spiega un blocked_by_robots. Una riga come Content-Signal: ai-input=no spiega un blocked_by_site_policy.
Conserviamo una copia di ogni robots.txt per un massimo di 24 ore. Se un sito ha appena aperto una sezione ai robot, la modifica può richiedere fino a un giorno per essere applicata. I proprietari dei siti possono leggere come si comporta il nostro crawler e come consentirlo o bloccarlo nella nostra pagina del crawler.
Come devo riprovare i fallimenti temporanei?
Riprova solo concurrency_limit, fetch_timeout e, una o due volte, fetch_failed. Aspetta prima di ogni nuovo tentativo e raddoppia l'attesa ogni volta, con un po' di jitter casuale così i worker paralleli non riprovano insieme. Fermati dopo 4 o 5 tentativi.
Per concurrency_limit, la soluzione migliore è inviare meno richieste contemporaneamente: Ogni account ha un limite di pagine lette contemporaneamente; la tua dashboard lo mostra e il supporto può aumentarlo. Per fetch_timeout, aumenta timeout fino a 60 secondi prima di riprovare con un sito lento. I tentativi non costano nulla, perché un tentativo fallito non viene mai addebitato.
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);
}
}
E per gli errori dentro un batch o un crawl?
Un lavoro non si ferma perché una pagina fallisce. Quella pagina compare in results come {url, error} con gli stessi codici di cui sopra, e non costa nulla. Un crawling che incontra blocked_by_robots su una sezione legge semplicemente il resto del sito.
Alla fine di un lavoro, conta i codici di errore. Molti target_refused su un sito significano che il sito non accetta lettori automatizzati; molti fetch_timeout significano che è lento, e un timeout più lungo nelle opzioni del lavoro può aiutare.
Errori: domande frequenti
Mi viene addebitata una pagina bloccata da robots.txt?
No. 1 credito = 1 pagina letta senza browser; 2 crediti quando la pagina richiede un browser per eseguire JavaScript. Una richiesta che fallisce, o che il robots.txt di un sito rifiuta, non viene addebitata. Lo stesso vale per ogni altro errore in questa pagina.
Puoi leggere una pagina che blocca il tuo crawler se te lo chiedo?
No. Rispettiamo la scelta del proprietario del sito, e la policy è la stessa per ogni cliente. Se il sito è tuo, aggiungi una regola per WebReaderAPI nel tuo robots.txt; la pagina del crawler mostra come.
Perché ricevo fetch_failed per una pagina che esiste?
Il sito potrebbe essere stato giù, lento ad accettare una connessione TLS o restituire 404 ai client automatizzati. Riprova una o due volte con backoff. Se continua a fallire, controlla l'indirizzo in un browser.
Cosa dovrebbe dire il mio agente all'utente dopo un rifiuto?
Qualcosa di semplice, come che il sito non consente la lettura automatizzata di quella pagina. Poi lascia che il modello usi altre fonti. Restituire il codice di errore come risultato del tool basta per la maggior parte dei modelli.