curl retorna 403, mas o navegador funciona: encontre a peça que falta

O navegador carrega. curl recebe 403. A diferença entre essas duas solicitações é sempre finita e sempre identificável — aqui está como dividi-la em cinco minutos.

Você cola uma URL no Chrome e a página carrega. Você cola a mesma URL no curl e recebe 403 Forbidden. Nada sobre o recurso mudou entre esses dois segundos, então a diferença está inteiramente na solicitação — e uma solicitação é algo finito e inspecionável. Este guia é um procedimento de bissecção: reproduza exatamente o que o navegador enviou, depois remova peças até que o 403 volte. O que você removeu por último é a sua resposta. Os suspeitos, na ordem em que geralmente são culpados, são User-Agent, Referer, cookies, impressão digital TLS e JavaScript.

Passo 0: prove que as solicitações realmente são diferentes

Antes de teorizar, veja o que o curl realmente envia. Com -v você vê a linha de solicitação, cada cabeçalho e o handshake TLS. Uma solicitação padrão do curl é surpreendentemente simples — tipicamente Host, User-Agent: curl/8.x e Accept: */*. Um navegador envia uma dúzia a mais.

# what you send, what you get back, and the TLS details
curl -v -o /dev/null https://target.example/page

# just the response headers, quickly
curl -sS -o /dev/null -D - https://target.example/page

Leia os cabeçalhos de resposta tão cuidadosamente quanto a linha de status. Um deles resolve a questão diretamente no caso mais comum: Vary: User-Agent significa que o servidor serve deliberadamente respostas diferentes dependendo de quem você afirma ser. Em um caso bem documentado no Stack Overflow, curl -f contra um host Apache 2.4.38 simples retornou 403 enquanto wget buscou o arquivo idêntico com 200 — e a resposta bem-sucedida carregava exatamente esse cabeçalho Vary: User-Agent. Passar -A 'Wget/1.21.2' para o curl corrigiu instantaneamente. O proprietário do site havia colocado o user agent do curl na lista negra após abuso; nada mais sobre a solicitação importava.

Enquanto você está lendo a saída: curl: (22) A URL solicitada retornou erro: 403 não é um problema separado. O código de saída 22 é o que -f/--fail faz com qualquer erro HTTP — a flag suprime o corpo e falha no comando. Remova -f temporariamente para que você possa realmente ler a página de bloqueio, que geralmente nomeia o sistema que o parou.

Passo 1: Copiar como cURL, a resposta de 30 segundos

Ambos os principais navegadores podem fornecer a solicitação exata que acabaram de fazer. Abra o DevTools, vá para a aba Network, clique com o botão direito na solicitação e escolha "Copiar como cURL". O Chrome oferece isso desde a versão 26 e o Firefox desde a 31, e a saída inclui todos os cabeçalhos, todos os cookies e o referer. Cole no seu terminal: se retornar 200, seu problema está definitivamente na forma da solicitação, e o passo 2 encontra qual parte.

Um detalhe desperdiça muito tempo aqui. Se a URL redireciona, o painel Network limpa na navegação e você copia a solicitação errada. Marque "Preserve log" no Chrome ou "Persistent Logs" no Firefox primeiro, para que você possa ver tanto a solicitação que redirecionou quanto a que finalmente serviu o conteúdo. Cadeias de redirecionamento importam: em um conhecido tópico do Unix Stack Exchange, o servidor verificou o Referer, depois passou por um 302 para um local que não verificava nada — o que fez a falha parecer aleatória até que toda a cadeia fosse visível.

Comparação lado a lado de uma solicitação do navegador Chrome e uma solicitação padrão do curl mostrando contagem de cabeçalhos, cookies, impressão digital TLS e suporte a JavaScript
Cada 403 nesse cenário se esconde dentro dessa lacuna. Feche-a uma coluna de cada vez.

Passo 2: divida os cabeçalhos

Comece com o comando "Copiar como cURL" que funciona e delete cabeçalhos um de cada vez, executando novamente após cada exclusão. A primeira exclusão que traz o 403 de volta nomeia seu culpado. Na prática, é quase sempre um de quatro.

curl -sS -o /dev/null -w '%{http_code}\n' \
  -A 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36' \
  -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' \
  -H 'Accept-Language: en-GB,en;q=0.9' \
  -e 'https://target.example/' \
  -b 'session=abc123; consent=1' \
  -L \
  'https://target.example/page'

Dois detalhes finais para este nível. Coloque a URL entre aspas: uma string de consulta contendo & ou um token de acesso é corrompida pelo seu shell, caso contrário, e o 403 resultante não tem nada a ver com o servidor. E se você estiver depurando a partir de PHP ou Node em vez do shell, replique o mesmo conjunto de cabeçalhos lá — os padrões do libcurl dentro do PHP diferem dos da ferramenta de linha de comando, o que é por isso que a solicitação idêntica pode passar em um terminal e falhar em código. Nossas receitas de proxy curl cobrem a sintaxe de flags na íntegra.

Passo 3: quando cabeçalhos idênticos ainda retornam 403

Se uma cópia byte a byte dos cabeçalhos do navegador ainda falhar, a decisão foi tomada antes que seus cabeçalhos fossem analisados. Duas camadas estão abaixo deles.

Impressão digital TLS. Seu ClientHello — conjuntos de cifras, extensões, preferências de curva, ALPN, além do quadro de configurações HTTP/2 que se segue — é hash para um valor JA3 ou JA4. curl construído contra OpenSSL produz um que nenhum navegador jamais produz, e sistemas anti-bot o comparam com seu User-Agent alegado. Alegar ser o Chrome enquanto faz handshake como OpenSSL é uma contradição que eles foram construídos para detectar. A solução é um cliente que reproduz handshakes de navegador: curl-impersonate na linha de comando, ou curl_cffi do Python.

# pip install curl_cffi
from curl_cffi import requests

proxy = "http://USER:PASS@gate.quantumproxies.io:8000"

r = requests.get(
    "https://target.example/page",
    impersonate="chrome",          # browser ClientHello + HTTP/2 settings
    proxies={"http": proxy, "https": proxy},
    timeout=20,
)
print(r.status_code, r.headers.get("content-type"))

Seu IP. O navegador que funciona geralmente está na sua conexão doméstica enquanto o curl roda em um VPS. ASNs de hospedagem são publicados e pré-avaliados, então a mesma solicitação de um endereço residencial é julgada de forma diferente antes mesmo de ser lida. Trocar a saída é uma mudança de uma linha com proxies residenciais — mais de 90 milhões de IPs em mais de 200 países, HTTP e SOCKS5 em todos os planos — e é a maneira mais rápida de descartar a rede. A mecânica da camada de impressão digital está em impressão digital TLS JA3/JA4.

Elimine a rede com proxies residenciais

Árvore de decisão do curl 403 para uma resposta 200: copiar como cURL, dividir cabeçalhos, imitar TLS, depois renderizar ou usar uma API de scraper
Quatro passos, cada um um teste. Pare assim que o status mudar para 200 — não suba mais do que o necessário.

Passo 4: a página precisa de um navegador, não de um cliente

Às vezes, o 403 não é um julgamento sobre você — é o modo de falha de um desafio que você nunca tentou. Uma discussão pública no GitHub sobre verificadores de links atingindo npmjs.com coloca isso claramente: curl não pode produzir uma solução de desafio válida, então a solicitação é bloqueada com um 403. O servidor emite um pequeno problema JavaScript, espera um momento pela resposta e recusa qualquer coisa que não possa executá-lo. Nenhum conjunto de cabeçalhos, nenhuma impressão digital e nenhum IP passa em um teste que requer execução de código.

Nesse ponto, você tem três opções honestas: dirigir um navegador real e pagar o custo, encontrar o endpoint JSON que a própria página chama (geralmente sentado na mesma aba Network que você já abriu), ou entregar a URL a um serviço que renderiza sob demanda. A Scraper API da QuantumProxies faz a última — TLS em nível de navegador, saídas residenciais, renderização de JavaScript apenas onde uma página precisa, e markdown, JSON ou HTML bruto de volta de uma solicitação. Se o que você recebe é uma página vazia em vez de uma proibida, esse é um diagnóstico diferente: veja por que seu scraper retorna uma página vazia. E se a página de bloqueio carregar um Cloudflare Ray ID, vá para Cloudflare erro 1020 em vez disso.

Perguntas frequentes

Por que o curl recebe 403 quando meu navegador não?

Porque o curl envia cerca de três cabeçalhos, sem cookies, sem referer e uma impressão digital TLS não de navegador, enquanto seu navegador envia uma dúzia de cabeçalhos, um jar de cookies e um handshake do Chrome. O servidor está recusando a solicitação, não o recurso. Reproduza a solicitação exata do navegador com "Copiar como cURL", depois remova cabeçalhos um por um para descobrir qual diferença importa.

Por que o wget tem sucesso onde o curl recebe 403?

Quase sempre o User-Agent. Alguns servidores colocam o UA do curl na lista negra especificamente após abuso, enquanto deixam o do wget em paz — um caso documentado mostrou um cabeçalho de resposta Vary: User-Agent confirmando que o servidor ramifica com base nele, e curl -A 'Wget/1.21.2' restaurou o 200. wget também envia Accept-Encoding e Connection por padrão, o que ocasionalmente também importa.

Como configuro um User-Agent no curl?

Use -A 'string', ou o equivalente -H 'User-Agent: string'. Prefira uma string de navegador completa e atual em vez de um Mozilla/5.0 truncado, que alguns servidores agora rejeitam precisamente porque nenhum navegador real envia apenas dois tokens. Emparelhe com valores correspondentes de Accept e Accept-Language para que todo o conjunto permaneça coerente.

O que significa o erro 22 do curl?

O código de saída 22 é produzido por -f/--fail sempre que o servidor retorna um erro HTTP, e a mensagem cita o status — comumente 403. É uma flag de relatório, não uma falha distinta. Remova -f para ver o corpo da resposta, que geralmente explica o bloqueio muito melhor do que o código de saída faz.

Um proxy pode corrigir um curl 403?

Ele corrige o subconjunto causado por reputação de IP ou geografia — um grande subconjunto quando seu script é executado em um host na nuvem e seu navegador não. Ele não corrigirá um referer ausente, um cookie ausente ou um desafio JavaScript. Teste os cabeçalhos primeiro, já que não custam nada, depois mude o IP de saída para isolar a camada de rede.

Não há mistério aqui, apenas uma lacuna: o navegador enviou uma solicitação e você enviou outra. Copie a do navegador, reduza até que quebre, e você sempre encontrará a peça que importava — geralmente um cabeçalho, às vezes uma impressão digital, ocasionalmente um desafio que precisa de um navegador real para responder.

Busque qualquer página com a Scraper API