Szybka odpowiedź
Co oznaczają kody błędów WebReaderAPI i które warto ponowić?
Błędy WebReaderAPI występują w trzech grupach i żaden nigdy nie jest rozliczany. Problemy z zapytaniem (invalid_url, invalid_api_key, insufficient_credits) wymagają poprawki po Twojej stronie. Decyzje witryny (blocked_by_robots, blocked_by_site_policy, target_refused) są ostateczne. Przejściowe awarie (fetch_failed, fetch_timeout, concurrency_limit) można ponowić z backoffem. Kredyty zaczynają się od 0,07 USD/1K stron, płatne kryptowalutą bez KYC.
Które kody błędów może zwrócić API?
Treść to zawsze {"error": {"code", "message"}}, plus target_status, gdy odmowa pochodziła z witryny.
| HTTP | code | Przyczyna | Ponowić? |
|---|---|---|---|
| 400 | invalid_url | Nie jest to http ani https, adres jest zbyt długi albo to prywatny, lokalny lub chmurowy adres metadanych | Nie: popraw URL |
| 400 | invalid_parameter | Parametr ma wartość poza swoją listą lub zakresem | Nie: popraw zapytanie |
| 401 | invalid_api_key | Klucz brakuje, jest błędny lub unieważniony | Nie: sprawdź klucz |
| 402 | insufficient_credits | Na Twoim saldzie nie ma już kredytów | Po zakupie pakietu |
| 403 | blocked_by_robots | robots.txt strony nie zezwala na tę stronę dla WebReaderAPI ani dla wszystkich robotów | Nie: ostatecznie |
| 403 | blocked_by_site_policy | Strona deklaruje Content-Signal: ai-input=no | Nie: ostatecznie |
| 422 | unsupported_content | Typ, którego nie odczytujemy (wideo, archiwum), albo plik zbyt duży | Nie |
| 429 | concurrency_limit | Zbyt wiele Twoich zapytań działa jednocześnie | Tak, z backoffem |
| 502 | target_refused | Strona odmówiła: 403, 429, strona wyzwania lub ściana logowania; jej status jest w target_status | Nie: ostatecznie |
| 502 | fetch_failed | Błąd DNS, TLS lub połączenia albo strona odpowiedziała 404 lub 410 | Raz lub dwa razy, z backoffem |
| 504 | fetch_timeout | Strona nie odpowiedziała w ciągu timeout | Tak, z backoffem lub dłuższym timeoutem |
Dlaczego robots.txt i odmowy strony są ostateczne?
Bo to decyzja właściciela strony, a WebReaderAPI jest zbudowany tak, by ją respektować. Respektujemy robots.txt: nasz crawler identyfikuje się jako WebReaderAPI, przestrzega reguł napisanych dla niego lub dla wszystkich robotów i nie czyta strony, której witryna zabrania. Odrzucone strony nie są obciążane opłatą. Jeśli witryna zadeklaruje Content-Signal: ai-input=no w swoim robots.txt, nie czytamy jej stron dla Ciebie.
Gdy strona odrzuca zapytanie w inny sposób — przez 403, 429, stronę wyzwania lub formularz logowania — otrzymujesz target_refused i własny status strony w target_status. Jeśli witryna odrzuci nasze zapytanie, zwracamy jej odpowiedź: nigdy nie rozwiązujemy CAPTCHA, nie logujemy się do kont ani nie próbujemy obejść blokady lub paywalla. Ponowna próba tego samego URL-a minutę później zwykle da tę samą odpowiedź, więc traktuj te kody jako wynik, a nie błąd: zapisz je w logach i idź dalej.
Jak sprawdzić, dlaczego strona została odrzucona?
Otwórz /robots.txt strony i poszukaj grupy, która wymienia WebReaderAPI, albo grupy *, która dotyczy wszystkich robotów. Linia Disallow pasująca do ścieżki wyjaśnia blocked_by_robots. Linia taka jak Content-Signal: ai-input=no wyjaśnia blocked_by_site_policy.
Trzymamy kopię każdego robots.txt przez maksymalnie 24 godziny. Jeśli strona właśnie otworzyła sekcję dla robotów, zmiana może zacząć obowiązywać dopiero po upływie dnia. Właściciele stron mogą przeczytać, jak zachowuje się nasz crawler i jak go dozwolić lub zablokować, na naszej stronie o crawlerze.
Jak ponawiać przejściowe błędy?
Ponawiaj tylko concurrency_limit, fetch_timeout i raz lub dwa razy fetch_failed. Przed każdą nową próbą odczekaj i za każdym razem podwajaj czas oczekiwania, z odrobiną losowego jittera, żeby równolegli workerzy nie ponawiali jednocześnie. Zatrzymaj się po 4 lub 5 próbach.
W przypadku concurrency_limit lepszym rozwiązaniem jest wysyłanie mniejszej liczby zapytań naraz: Każde konto ma limit liczby stron czytanych jednocześnie; Twój dashboard go pokazuje, a support może go podnieść. W przypadku fetch_timeout zwiększ timeout do 60 sekund, zanim ponowisz próbę dla wolnej strony. Ponowienia nic nie kosztują, bo nieudana próba nigdy nie jest rozliczana.
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);
}
}
Co z błędami wewnątrz batcha lub crawlu?
Zadanie nie zatrzymuje się, bo jedna strona zawiodła. Ta strona pojawia się w results jako {url, error} z tymi samymi kodami co powyżej i nic nie kosztuje. Crawling, który napotka blocked_by_robots w sekcji, po prostu czyta resztę witryny.
Na końcu zadania policz kody błędów. Wiele target_refused w jednej witrynie oznacza, że witryna nie akceptuje zautomatyzowanych czytników; wiele fetch_timeout oznacza, że jest wolna, a dłuższy timeout w opcjach zadania może pomóc.
Błędy: najczęstsze pytania
Czy płacę za stronę blokowaną przez robots.txt?
Nie. 1 kredyt = 1 odczytana strona bez przeglądarki; 2 kredyty, gdy strona wymaga przeglądarki do wyrenderowania JavaScript. Zapytanie, które się nie powiedzie lub które zostanie odrzucone przez robots.txt witryny, nie jest obciążane opłatą. To samo dotyczy każdego innego błędu na tej stronie.
Czy możecie odczytać stronę, która blokuje wasz crawler, jeśli o to poproszę?
Nie. Respektujemy wybór właściciela witryny, a polityka jest taka sama dla każdego klienta. Jeśli jesteś właścicielem witryny, dodaj regułę dla WebReaderAPI w swoim robots.txt; strona crawlera pokazuje jak.
Dlaczego dostaję fetch_failed dla strony, która istnieje?
Witryna mogła być niedostępna, wolno akceptować połączenie TLS lub zwracać 404 zautomatyzowanym klientom. Ponów próbę raz lub dwa z backoffem. Jeśli nadal się nie udaje, sprawdź adres w przeglądarce.
Co mój agent powinien powiedzieć użytkownikowi po odmowie?
Coś prostego, np. że witryna nie pozwala na zautomatyzowane odczytywanie tej strony. Następnie pozwól modelowi skorzystać z innych źródeł. Zwrócenie kodu błędu jako wyniku narzędzia wystarcza większości modeli.