Pourquoi l’outil fetch de votre agent IA revient avec une page vide
Coquille vide, mur de cookies, redirection vers la connexion ou page tronquée : comment les distinguer en 30 lignes de Python, avec de vraies mesures du 19 octobre 2026.

« Je n’ai pas pu lire cette page » : une seule phrase du modèle, et au moins cinq problèmes différents en dessous. La solution dépend entièrement de celui que vous rencontrez, alors commencez par le diagnostic.
Pourquoi l’outil fetch d’un agent ne voit-il rien d’utile ?
La plupart des outils fetch intégrés font la même chose : un simple GET HTTP, puis la conversion du HTML en texte, puis un plafond de longueur. Aucun JavaScript ne s’exécute, aucun cookie n’est conservé, personne ne clique sur « accepter ». La documentation d’Anthropic pour son propre outil web fetch indique, en octobre 2026, qu’il ne prend pas en charge les sites rendus dynamiquement en JavaScript, et que le contenu au-delà de max_content_tokens est tronqué.
Cinq situations se réduisent donc au même « rien d’utile » pour le modèle :
| Cas | Ce que le serveur a envoyé | Signal vérifiable |
|---|---|---|
| Application rendue côté client | Une coquille HTML, le contenu étant construit ensuite par des scripts | Moins de 500 caractères de texte visible, un <noscript> qui demande JavaScript, ou des octets HTML bien plus nombreux que le texte |
| Mur de consentement | Une page interstitielle cookies ou confidentialité | Redirection vers un hôte consent., page courte remplie de « cookie » et « consent » |
| Mur de connexion | Un formulaire de connexion | Redirection vers /login ou /signin, un champ mot de passe |
| Bloqué | Une erreur ou un challenge | 401, 403, 429 ou 503 |
| Trop long | La page complète | Texte visible plus long que le budget de l’outil |
Pour comprendre pourquoi le rendu coûte plus cher et comment le repérer à la main, voir le rendu JavaScript en web scraping. Cet article porte sur la vérification en code, à l’intérieur de l’agent.
Une fonction de diagnostic prête à l’emploi
Du simple requests et BeautifulSoup, sans clé API, sans coût :
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}
Les seuils sont des heuristiques. Ajustez-les sur votre propre trafic, mais l’ordre compte : vérifiez les signaux peu coûteux et sans ambiguïté (statut, URL) avant les signaux flous (longueur du texte).
Ce qu’elle a renvoyé sur quatre pages publiques
Exécution le 19 octobre 2026 depuis une machine en Italie :
| URL | Statut | Octets HTML | Texte visible | Verdict |
|---|---|---|---|---|
| python.org/about/ | 200 | 41 239 | 4 598 caractères | ok |
| crates.io/crates/serde | 200 | 5 406 | 32 caractères | client_rendered |
| it.yahoo.com | 200, redirigé vers consent.yahoo.com | 112 226 | 2 471 caractères | consent_wall |
| en.wikipedia.org/wiki/World_War_II | 200 | 2 074 721 | 196 628 caractères | too_long |
Toutes ont renvoyé 200. Une simple vérification du code de statut les aurait toutes considérées comme un succès. crates.io envoie une coquille de 5 Ko dont le seul texte est le titre et une ligne <noscript> qui demande JavaScript. La page d’accueil italienne de Yahoo redirige un visiteur sans cookies vers une page de consentement ; uk.yahoo.com, récupéré de la même façon, ne l’a pas fait.
Que faire de chaque verdict ?
ok : utilisez le fetch simple. Python.org n’a rien coûté et n’a besoin de rien d’autre.
client_rendered : faites le rendu. L’API de web scraping de FastAF s’en charge avec dynamic=true pour 5 crédits au lieu de 1, ou render_js: true sur l’outil MCP scrape_page. Sur crates.io, un appel statique a renvoyé 0 caractère de markdown ; un appel avec rendu en a renvoyé 4 939. Soyez toutefois honnête avec vous-même sur la variabilité : nous avons fait six appels avec rendu sur cette page le 19 octobre, quatre sont revenus avec 2 313 à 4 939 caractères et deux sont revenus vides, parce que l’application n’avait pas fini de charger. Ces deux-là ont été facturés à ce moment-là ; après les avoir mesurés pour cet article, nous avons changé la règle, et un résultat vide renvoie désormais 422 target_unavailable et ne coûte rien. Un rendu peut encore renvoyer une coquille maigre : vérifiez donc la longueur du texte, et réglez wait (de 0 à 35 000 ms) pour les applications lentes.
consent_wall : le rendu n’y change rien. Le fetch de it.yahoo.com par FastAF a lui aussi obtenu la page de consentement (3 697 caractères, entièrement consacrés à l’avis de confidentialité). Personne ne devrait accepter des conditions au nom d’un site dans une exécution automatisée. Signalez-le, ou allez chercher l’information auprès d’une source conçue pour cela, comme une recherche d’actualités.
login_wall et blocked : arrêtez-vous et dites-le. Un contenu derrière une connexion n’est pas public, et la protection anti-bot relève de la décision du propriétaire du site. FastAF ne sert que des pages publiques : une requête vers https://example.com/login renvoie 400 blocked_target en 64 ms et ne coûte rien. Les règles figurent dans la politique d’utilisation acceptable.
too_long : demandez du markdown avec un budget. L’outil MCP scrape_page sur la page Wikipedia a renvoyé les 12 000 premiers des 597 765 caractères de markdown avec truncated: true, pour 1 crédit en 1,3 seconde. Tout l’intérêt est dans ce flag : le modèle sait qu’il a lu une tranche, pas la page entière.
Une stratégie de fetch pour les agents
D’abord la voie économique, le rendu seulement quand le diagnostic le demande, et un plafond strict par exécution :
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}
Notre dernière exécution du 19 octobre, avec renders_left réglé à 1, a coûté 5 crédits au total. Python.org et Wikipedia sont venus du fetch simple gratuit (Wikipedia coupé à 12 000 caractères sur 196 628 et signalé comme tel), crates.io/crates/serde a été rendu en 2 508 caractères en 6,3 secondes, crates.io/crates/tokio a reçu « budget spent » et Yahoo a reçu consent_wall. Les erreurs reviennent sous forme de résultats d’outil, pour que le modèle puisse dire à l’utilisateur ce qui s’est passé au lieu de deviner. Tous les paramètres sont dans la documentation.
En bref
Un fetch vide est un symptôme, pas un diagnostic. Vérifiez le statut, la cible de la redirection, la longueur du texte visible et le <noscript> avant de dépenser quoi que ce soit. Le fetch simple pour les pages statiques, le rendu à 5 crédits uniquement pour les pages publiques rendues côté client, un plafond de rendus par exécution, et un « je ne peux pas lire ceci » honnête pour les murs de consentement, les connexions et les blocages. Les pages tronquées ont besoin d’un budget et d’un flag, pas d’un contexte plus grand. Obtenez 500 crédits gratuits et essayez le diagnostic sur les pages où votre agent échoue sans cesse.