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.