Por que a ferramenta de fetch do seu agente de IA volta com uma página vazia
Shell vazio, muro de cookies, redirecionamento para login ou página cortada: como diferenciar cada caso em 30 linhas de Python, com medições reais de 19 de outubro de 2026.

“Não consegui ler essa página” é uma frase do modelo e pelo menos cinco problemas diferentes por baixo. A solução depende inteiramente de qual deles você encontrou, então diagnostique primeiro.
Por que a ferramenta de fetch de um agente não vê nada útil?
A maioria das ferramentas de fetch embutidas faz a mesma coisa: um GET HTTP simples, depois a conversão do HTML em texto, depois um limite de tamanho. Nenhum JavaScript roda, nenhum cookie é guardado, ninguém clica em “aceitar”. A própria documentação da Anthropic para a sua ferramenta de web fetch diz, em outubro de 2026, que ela não suporta sites renderizados dinamicamente com JavaScript, e que o conteúdo acima de max_content_tokens é truncado.
Então cinco situações diferentes viram o mesmo “nada útil” para o modelo:
| Caso | O que o servidor mandou | Sinal que você pode checar |
|---|---|---|
| App renderizado no cliente | Um shell HTML, com o conteúdo montado depois por scripts | Menos de 500 caracteres de texto visível, <noscript> pedindo JavaScript, ou bytes de HTML muito maiores que o texto |
| Muro de consentimento | Uma página intermediária de cookies ou privacidade | Redirecionamento para um host consent., página curta cheia de “cookie” e “consent” |
| Muro de login | Um formulário de login | Redirecionamento para /login ou /signin, um input de senha |
| Bloqueado | Um erro ou um desafio | 401, 403, 429 ou 503 |
| Longo demais | A página inteira | Texto visível maior que o orçamento da ferramenta |
Para entender por que a renderização custa mais e como identificá-la na mão, veja renderização de JavaScript em web scraping. Este post trata de fazer essa checagem em código, dentro do agente.
Uma função de diagnóstico que você pode rodar
requests puro e BeautifulSoup, sem chave de API, sem custo:
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}
Os limiares são heurísticas. Ajuste-os com o seu próprio tráfego, mas a ordem importa: cheque os sinais baratos e sem ambiguidade (status, URL) antes dos nebulosos (tamanho do texto).
O que ela reportou em quatro páginas públicas
Rodada em 19 de outubro de 2026 a partir de uma máquina na Itália:
| URL | Status | Bytes de HTML | Texto visível | Veredito |
|---|---|---|---|---|
| python.org/about/ | 200 | 41.239 | 4.598 caracteres | ok |
| crates.io/crates/serde | 200 | 5.406 | 32 caracteres | client_rendered |
| it.yahoo.com | 200, redirecionado para consent.yahoo.com | 112.226 | 2.471 caracteres | consent_wall |
| en.wikipedia.org/wiki/World_War_II | 200 | 2.074.721 | 196.628 caracteres | too_long |
Todas devolveram 200. Uma checagem só de status code teria considerado as quatro um sucesso. O crates.io manda um shell de 5 KB cujo único texto é o título e uma linha de <noscript> pedindo JavaScript. A home italiana do Yahoo redireciona um visitante sem cookies para uma página de consentimento; o uk.yahoo.com, baixado do mesmo jeito, não fez isso.
O que fazer com cada veredito?
ok: use o fetch simples. O python.org não custou nada e não precisou de mais nada.
client_rendered: renderize. A API de web scraping do FastAF faz isso com dynamic=true por 5 créditos em vez de 1, ou com render_js: true na ferramenta scrape_page do MCP. No crates.io uma chamada estática devolveu 0 caracteres de markdown; uma chamada renderizada devolveu 4.939. Mas seja honesto consigo mesmo sobre a variação: fizemos seis chamadas renderizadas a essa página em 19 de outubro, quatro voltaram com 2.313 a 4.939 caracteres e duas voltaram vazias, porque o app não tinha terminado de carregar. Essas duas foram cobradas na hora; depois de medir isso para este post mudamos a regra, e agora um resultado vazio devolve 422 target_unavailable e não custa nada. Uma renderização ainda pode devolver um shell magro, então cheque o tamanho do texto, e defina wait (de 0 a 35.000 ms) para apps lentos.
consent_wall: renderizar não ajuda. O fetch do FastAF para it.yahoo.com também pegou a página de consentimento (3.697 caracteres, tudo aviso de privacidade). Ninguém deveria aceitar termos em nome de um site numa execução automatizada. Reporte o caso, ou busque a informação numa fonte feita para isso, como uma busca de notícias.
login_wall e blocked: pare e diga isso. Conteúdo atrás de login não é público, e a proteção contra bots é decisão do dono do site. O FastAF só serve páginas públicas: uma requisição para https://example.com/login devolve 400 blocked_target em 64 ms e não custa nada. As regras estão na política de uso aceitável.
too_long: peça markdown com orçamento. A ferramenta MCP scrape_page na página da Wikipedia devolveu os primeiros 12.000 de 597.765 caracteres de markdown com truncated: true, por 1 crédito em 1,3 segundo. A flag é o ponto principal: o modelo sabe que leu um pedaço, não a página inteira.
Uma estratégia de fetch para agentes
Primeiro o caminho barato, renderização só quando o diagnóstico pede, e um teto rígido por execução:
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}
A nossa execução final em 19 de outubro, com renders_left em 1, custou 5 créditos no total. O python.org e a Wikipedia vieram do fetch simples e gratuito (a Wikipedia cortada em 12.000 de 196.628 caracteres e sinalizada), o crates.io/crates/serde foi renderizado em 2.508 caracteres em 6,3 segundos, o crates.io/crates/tokio recebeu “budget spent” e o Yahoo recebeu consent_wall. Os erros voltam como resultados da ferramenta, para que o modelo possa contar ao usuário o que aconteceu em vez de chutar. Os parâmetros completos estão na documentação.
Resumindo
Um fetch vazio é um sintoma, não um diagnóstico. Cheque o status, o destino do redirecionamento, o tamanho do texto visível e o <noscript> antes de gastar qualquer coisa. Fetch simples para páginas estáticas, renderização a 5 créditos só para páginas públicas renderizadas no cliente, um limite de renderizações por execução, e um “não consigo ler isto” honesto para muros de consentimento, logins e bloqueios. Páginas truncadas precisam de um orçamento e de uma flag, não de um contexto maior. Pegue 500 créditos grátis e teste o diagnóstico nas páginas em que o seu agente vive falhando.