Perché il tool di fetch del tuo agente AI torna con una pagina vuota

Guscio vuoto, muro dei cookie, redirect al login o pagina tagliata a metà: come distinguerli in 30 righe di Python, con misure reali del 19 ottobre 2026.

“Non sono riuscito a leggere quella pagina” è una sola frase del modello, e sotto ci sono almeno cinque problemi diversi. La soluzione dipende interamente da quale ti è capitato, quindi prima si fa la diagnosi.

Perché il tool di fetch di un agente non vede niente di utile?

La maggior parte dei tool di fetch integrati fa la stessa cosa: una semplice GET HTTP, poi la conversione dell’HTML in testo, poi un tetto sulla lunghezza. Non gira JavaScript, non si conservano cookie, nessuno clicca “accetta”. La documentazione di Anthropic per il suo tool di web fetch dice, a ottobre 2026, che non supporta i siti renderizzati dinamicamente con JavaScript, e che il contenuto oltre max_content_tokens viene troncato.

Così cinque situazioni diverse si riducono, per il modello, allo stesso “niente di utile”:

Caso Cosa ha mandato il server Segnale che puoi controllare
App renderizzata lato client Un guscio HTML, con il contenuto costruito dopo dagli script Meno di 500 caratteri di testo visibile, un <noscript> che chiede JavaScript, o byte di HTML molto più numerosi del testo
Muro del consenso Una pagina intermedia su cookie o privacy Redirect a un host consent., pagina corta piena di “cookie” e “consent”
Muro del login Un modulo di accesso Redirect a /login o /signin, un campo password
Bloccato Un errore o una challenge 401, 403, 429 o 503
Troppo lunga La pagina intera Testo visibile più grande del budget del tool

Per capire perché il rendering costa di più e come riconoscerlo a mano, vedi rendering JavaScript nello scraping. Questo articolo parla di fare il controllo nel codice, dentro l’agente.

Una funzione di diagnosi da usare subito

requests e BeautifulSoup e basta, senza chiave API e senza costi:

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}

Le soglie sono euristiche. Tarale sul tuo traffico, ma l’ordine conta: prima i segnali economici e inequivocabili (status, URL), poi quelli sfumati (lunghezza del testo).

Cosa ha restituito su quattro pagine pubbliche

Eseguita il 19 ottobre 2026 da una macchina in Italia:

URL Status Byte di HTML Testo visibile Verdetto
python.org/about/ 200 41.239 4.598 caratteri ok
crates.io/crates/serde 200 5.406 32 caratteri client_rendered
it.yahoo.com 200, redirect a consent.yahoo.com 112.226 2.471 caratteri consent_wall
en.wikipedia.org/wiki/World_War_II 200 2.074.721 196.628 caratteri too_long

Tutte e quattro hanno restituito 200. Un controllo sul solo status code le avrebbe date tutte per riuscite. crates.io manda un guscio da 5 KB il cui unico testo è il titolo più una riga <noscript> che chiede JavaScript. La homepage italiana di Yahoo reindirizza chi arriva senza cookie a una pagina di consenso; uk.yahoo.com, scaricata allo stesso modo, no.

Cosa fai con ciascun verdetto?

ok: usa il fetch semplice. Python.org non è costata niente e non ha avuto bisogno d’altro.

client_rendered: renderizzala. La web scraping API di FastAF lo fa con dynamic=true per 5 crediti invece di 1, oppure con render_js: true sul tool scrape_page dell’MCP. Su crates.io una chiamata statica ha restituito 0 caratteri di markdown; una chiamata renderizzata ne ha restituiti 4.939. Però sii onesto con te stesso sulla variabilità: il 19 ottobre abbiamo fatto sei chiamate renderizzate a quella pagina, quattro sono tornate con un numero di caratteri fra 2.313 e 4.939 e due sono tornate vuote, perché l’app non aveva finito di caricarsi. Quelle due al momento sono state addebitate; dopo averlo misurato per questo articolo abbiamo cambiato la regola, e ora un risultato vuoto restituisce 422 target_unavailable e non costa nulla. Un render può comunque restituire un guscio scarno, quindi controlla la lunghezza del testo, e imposta wait (da 0 a 35.000 ms) per le app lente.

consent_wall: il rendering non aiuta. Anche il fetch di FastAF su it.yahoo.com ha ottenuto la pagina di consenso (3.697 caratteri, tutti informativa sulla privacy). Nessuno dovrebbe accettare termini al posto di un sito in un’esecuzione automatica. Segnalalo, oppure prendi l’informazione da una fonte pensata per questo, come una ricerca di notizie.

login_wall e blocked: fermati e dillo. Il contenuto dietro un login non è pubblico, e la protezione anti-bot è una decisione del proprietario del sito. FastAF serve solo pagine pubbliche: una richiesta per https://example.com/login restituisce 400 blocked_target in 64 ms e non costa nulla. Le regole sono nella policy di uso accettabile.

too_long: chiedi il markdown con un budget. Il tool MCP scrape_page sulla pagina di Wikipedia ha restituito i primi 12.000 di 597.765 caratteri di markdown con truncated: true, per 1 credito in 1,3 secondi. Il punto è proprio il flag: il modello sa di aver letto una fetta, non la pagina intera.

Una strategia di fetch per agenti

Prima la strada economica, il rendering solo quando la diagnosi lo chiede, e un tetto rigido per esecuzione:

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}

La nostra esecuzione finale del 19 ottobre, con renders_left impostato a 1, è costata 5 crediti in tutto. Python.org e Wikipedia sono arrivate dal fetch semplice gratuito (Wikipedia tagliata a 12.000 di 196.628 caratteri e segnalata), crates.io/crates/serde è stata renderizzata in 2.508 caratteri in 6,3 secondi, crates.io/crates/tokio ha ricevuto “budget spent” e Yahoo consent_wall. Gli errori tornano come risultati del tool, così il modello può dire all’utente cosa è successo invece di tirare a indovinare. I parametri completi sono nella documentazione.

La versione breve

Un fetch vuoto è un sintomo, non una diagnosi. Controlla status, destinazione del redirect, lunghezza del testo visibile e <noscript> prima di spendere qualsiasi cosa. Fetch semplice per le pagine statiche, rendering a 5 crediti solo per le pagine pubbliche renderizzate lato client, un tetto ai render per esecuzione, e un onesto “non riesco a leggerla” per muri del consenso, login e blocchi. Le pagine troncate hanno bisogno di un budget e di un flag, non di un contesto più grande. Prendi 500 crediti gratis e prova la diagnosi sulle pagine su cui il tuo agente continua a fallire.