Warum das Fetch-Tool deines KI-Agenten mit einer leeren Seite zurückkommt

Leere Hülle, Cookie-Wall, Login-Redirect oder abgeschnittene Seite: wie du sie in 30 Zeilen Python auseinanderhältst, mit echten Messungen vom 19. Oktober 2026.

„Ich konnte die Seite nicht lesen” ist beim Modell ein einziger Satz, und darunter stecken mindestens fünf verschiedene Probleme. Die Lösung hängt komplett davon ab, welches du erwischt hast, also stell zuerst die Diagnose.

Warum sieht das Fetch-Tool eines Agenten nichts Brauchbares?

Die meisten eingebauten Fetch-Tools machen dasselbe: ein einfacher HTTP-GET, dann HTML in Text umgewandelt, dann ein Längenlimit. Kein JavaScript läuft, keine Cookies werden behalten, niemand klickt auf „Akzeptieren”. Anthropics eigene Dokumentation zum Web-Fetch-Tool sagt, Stand Oktober 2026, dass es keine Websites unterstützt, die dynamisch mit JavaScript gerendert werden, und dass Inhalte über max_content_tokens abgeschnitten werden.

Fünf Situationen fallen für das Modell also in dasselbe „nichts Brauchbares” zusammen:

Fall Was der Server geschickt hat Signal, das du prüfen kannst
Clientseitig gerenderte App Eine HTML-Hülle, Inhalt wird später per Skript aufgebaut Unter 500 Zeichen sichtbarer Text, ein <noscript>, das nach JavaScript fragt, oder HTML-Bytes weit größer als der Text
Consent-Wall Eine Cookie- oder Datenschutz-Zwischenseite Redirect auf einen consent.-Host, kurze Seite voller „cookie” und „consent”
Login-Wall Ein Anmeldeformular Redirect auf /login oder /signin, ein Passwortfeld
Blockiert Ein Fehler oder eine Challenge 401, 403, 429 oder 503
Zu lang Die vollständige Seite Sichtbarer Text größer als das Budget des Tools

Warum Rendering mehr kostet und wie du es von Hand erkennst, steht in JavaScript-Rendering beim Web-Scraping. In diesem Beitrag geht es darum, die Prüfung im Code zu machen, innerhalb des Agenten.

Eine Diagnosefunktion zum Ausführen

Einfaches requests und BeautifulSoup, kein API-Key, keine Kosten:

import re
import requests
from bs4 import BeautifulSoup

HEADERS = {"User-Agent": "page-diagnostic/1.0", "Accept": "text/html"}
CONSENT = re.compile(r"cookie|consent|gdpr|privacy choices", re.I)
LOGIN = re.compile(r"/(login|log-in|signin|sign-in|auth)\b", re.I)

def diagnose(url, budget=12_000):
    r = requests.get(url, headers=HEADERS, timeout=30)
    soup = BeautifulSoup(r.text, "html.parser")
    noscript = " ".join(n.get_text(" ", strip=True) for n in soup.find_all("noscript"))
    for tag in soup(["script", "style", "noscript", "template"]):
        tag.decompose()
    text = soup.get_text(" ", strip=True)

    if r.status_code in (401, 403, 429, 503):
        verdict = "blocked"
    elif LOGIN.search(r.url) or soup.find("input", {"type": "password"}):
        verdict = "login_wall"
    elif "consent" in r.url or (len(text) < 3_000 and CONSENT.search(text)):
        verdict = "consent_wall"
    elif len(text) < 500 and ("javascript" in noscript.lower() or len(r.content) > 50 * len(text)):
        verdict = "client_rendered"
    elif len(text) > budget:
        verdict = "too_long"
    else:
        verdict = "ok"
    return {"status": r.status_code, "final_url": r.url, "html_bytes": len(r.content),
            "text_chars": len(text), "verdict": verdict, "text": text}

Die Schwellenwerte sind Heuristiken. Stimm sie auf deinen eigenen Traffic ab, aber die Reihenfolge zählt: Prüf erst die billigen, eindeutigen Signale (Status, URL), dann die unscharfen (Textlänge).

Was sie auf vier öffentlichen Seiten gemeldet hat

Ausgeführt am 19. Oktober 2026 von einem Rechner in Italien:

URL Status HTML-Bytes Sichtbarer Text Verdict
python.org/about/ 200 41.239 4.598 Zeichen ok
crates.io/crates/serde 200 5.406 32 Zeichen client_rendered
it.yahoo.com 200, weitergeleitet auf consent.yahoo.com 112.226 2.471 Zeichen consent_wall
en.wikipedia.org/wiki/World_War_II 200 2.074.721 196.628 Zeichen too_long

Jede lieferte 200. Eine reine Statuscode-Prüfung hätte alle vier als Erfolg gewertet. crates.io schickt eine 5-KB-Hülle, deren einziger Text der Titel und eine <noscript>-Zeile ist, die nach JavaScript fragt. Die italienische Yahoo-Startseite leitet Besucher ohne Cookies auf eine Consent-Seite weiter; uk.yahoo.com, auf dieselbe Weise abgerufen, tat das nicht.

Was machst du mit welchem Verdict?

ok: Nimm den einfachen Fetch. Python.org hat nichts gekostet und nichts weiter gebraucht.

client_rendered: Render die Seite. Die Web-Scraping-API von FastAF macht das mit dynamic=true für 5 Credits statt 1, oder mit render_js: true im scrape_page-Tool des MCP. Auf crates.io lieferte ein statischer Aufruf 0 Zeichen Markdown, ein gerenderter 4.939. Sei aber ehrlich zu dir, was die Schwankung angeht: Wir haben am 19. Oktober sechs gerenderte Aufrufe auf diese Seite gemacht, vier kamen mit 2.313 bis 4.939 Zeichen zurück und zwei leer, weil die App noch nicht fertig geladen hatte. Diese zwei wurden damals abgerechnet; nachdem wir das für diesen Beitrag gemessen hatten, haben wir die Regel geändert, und ein leeres Ergebnis liefert jetzt 422 target_unavailable und kostet nichts. Ein Render kann trotzdem eine dünne Hülle zurückgeben, also prüf die Textlänge und setz wait (0 bis 35.000 ms) für langsame Apps.

consent_wall: Rendering hilft nicht. Auch der Fetch von FastAF auf it.yahoo.com bekam die Consent-Seite (3.697 Zeichen, alles Datenschutzhinweis). Niemand sollte in einem automatisierten Lauf im Namen einer Website Bedingungen akzeptieren. Melde es, oder hol die Information aus einer Quelle, die dafür gemacht ist, etwa einer News-Suche.

login_wall und blocked: Stopp und sag es. Inhalte hinter einem Login sind nicht öffentlich, und Bot-Schutz ist die Entscheidung des Seitenbetreibers. FastAF liefert nur öffentliche Seiten aus: Ein Request auf https://example.com/login liefert 400 blocked_target in 64 ms und kostet nichts. Die Regeln stehen in der Acceptable Use Policy.

too_long: Fordere Markdown mit einem Budget an. Das MCP-Tool scrape_page lieferte auf der Wikipedia-Seite die ersten 12.000 von 597.765 Markdown-Zeichen mit truncated: true, für 1 Credit in 1,3 Sekunden. Genau darum geht es bei dem Flag: Das Modell weiß, dass es einen Ausschnitt gelesen hat, nicht die ganze Seite.

Eine Fetch-Strategie für Agenten

Erst der billige Weg, Rendering nur, wenn die Diagnose danach verlangt, und ein hartes Limit pro Lauf:

import os, time, requests

def read_page(url, state, max_chars=12_000):
    d = diagnose(url, budget=max_chars)
    if d["verdict"] in ("blocked", "login_wall", "consent_wall"):
        return {"error": f"{d['verdict']}: page not readable, tell the user"}
    if d["verdict"] in ("ok", "too_long"):
        t = d["text"]
        return {"text": t[:max_chars], "truncated": len(t) > max_chars}
    if state["renders_left"] <= 0:
        return {"error": "page needs rendering, render budget for this run is spent"}
    for _ in range(3):
        r = requests.get("https://api.fastaf.co/v1/scrape",
            params={"url": url, "formats": "markdown", "dynamic": "true", "wait": 3000},
            headers={"Authorization": f"Bearer {os.environ['FASTAF_API_KEY']}"}, timeout=90)
        if r.status_code != 429:
            break
        time.sleep(int(r.headers.get("Retry-After", 3)))
    if r.status_code != 200:
        return {"error": r.json().get("error", {}).get("code", "scrape_failed")}
    state["renders_left"] -= 1
    if len(r.text) < 500:
        return {"error": "rendered page still empty, tell the user"}
    return {"text": r.text[:max_chars], "truncated": len(r.text) > max_chars}

Unser letzter Lauf am 19. Oktober, mit renders_left auf 1, kostete insgesamt 5 Credits. Python.org und Wikipedia kamen aus dem kostenlosen einfachen Fetch (Wikipedia auf 12.000 von 196.628 Zeichen gekürzt und markiert), crates.io/crates/serde wurde in 6,3 Sekunden zu 2.508 Zeichen gerendert, crates.io/crates/tokio bekam „budget spent” und Yahoo consent_wall. Fehler kommen als Tool-Ergebnisse zurück, damit das Modell dem Nutzer sagen kann, was passiert ist, statt zu raten. Alle Parameter stehen in den Docs.

Kurzversion

Ein leerer Fetch ist ein Symptom, keine Diagnose. Prüf Status, Redirect-Ziel, Länge des sichtbaren Texts und <noscript>, bevor du irgendetwas ausgibst. Einfacher Fetch für statische Seiten, Rendering für 5 Credits nur bei clientseitig gerenderten öffentlichen Seiten, ein Render-Limit pro Lauf und ein ehrliches „Das kann ich nicht lesen” bei Consent-Walls, Logins und Blockaden. Abgeschnittene Seiten brauchen ein Budget und ein Flag, keinen größeren Kontext. Hol dir 500 kostenlose Credits und probier die Diagnose an den Seiten aus, an denen dein Agent immer wieder scheitert.