Um padrão comum em conversas de suporte: alguém migra para proxies residenciais, a taxa de bloqueio melhora e, ainda assim, um subconjunto obstinado de destinos continua recusando as requisições. O IP está limpo, a geografia está correta, e as requisições continuam sendo desafiadas. Quase sempre a resposta está nos headers, porque o endereço leva você até a porta, mas os headers decidem se o que entra parece um navegador.
O erro por trás da maioria dos problemas com headers é pensar neles como uma lista de valores a serem configurados corretamente. Não é isso. É um conjunto de afirmações que precisam concordar entre si e com tudo mais na sua conexão, e uma requisição construída com partes incompatíveis é mais suspeita do que uma sem nenhum esforço.
Consistência supera qualquer valor individual
Comece por aqui, porque isso reformula tudo o que vem depois. Um servidor não avalia o seu User-Agent isoladamente. Ele vê um conjunto: a string do UA, os client hints que a acompanham, o conjunto de headers e sua ordem, o valor de Accept-Language, o handshake TLS por trás de tudo, e o IP de onde tudo isso chegou. Navegadores reais produzem conjuntos internamente consistentes porque o mesmo software gerou tudo.
Scrapers produzem conjuntos inconsistentes por acidente. Um UA de Chrome chegando com o conjunto de headers de uma biblioteca HTTP em Python, um UA de Windows sobre uma fingerprint TLS que pertence a uma ferramenta Linux, uma saída nos EUA enviando Accept-Language: de-DE, ou um navegador declarado que nunca solicita os recursos que um navegador solicitaria. Nenhum desses sinais isoladamente diz “bot”, mas a contradição diz, e contradições são muito mais fáceis de detectar de forma confiável do que qualquer sinal isolado. Essa é a mesma lógica do pareamento entre dispositivo e rede nos antidetect browsers: cada camada precisa contar a mesma história.
Portanto, o objetivo não é a string de UA mais convincente. É uma requisição em que o UA, os headers, a fingerprint TLS e o endereço de saída descrevem todos o mesmo visitante plausível.
O que um navegador real realmente envia
Se você definir apenas o User-Agent, você já está sendo inconsistente, porque nenhum navegador envia um UA e nada mais. Uma requisição de página de um Chrome moderno carrega, no mínimo, um conjunto como este:
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.9
Accept-Encoding: gzip, deflate, br, zstd
sec-ch-ua: "Chromium";v="141", "Not?A_Brand";v="24", "Google Chrome";v="141"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Upgrade-Insecure-Requests: 1
Três coisas ali vale a pena entender, e não apenas copiar.
Os client hints precisam corresponder ao UA. A lista de marcas do sec-ch-ua, o sec-ch-ua-platform e o sec-ch-ua-mobile são uma reafirmação estruturada do que a string do UA já declara. Se o seu UA diz Chrome no Windows e o seu hint de plataforma diz macOS, ou se a versão na lista de marcas não coincide com a versão no UA, você se contradisse em dois headers adjacentes. Não enviar nenhum client hint enquanto afirma ser um Chrome recente também é uma incompatibilidade, já que um Chrome real os envia.
Os headers Sec-Fetch descrevem o contexto. Eles informam ao servidor que tipo de requisição é esta: uma navegação de nível superior, a busca de um subrecurso, um XHR de mesma origem. O carregamento de uma página é Dest: document, Mode: navigate, Site: none quando aberta diretamente, ou same-origin quando segue um link interno. Um XHR para uma API é Dest: empty, Mode: cors. Errar isso é revelador justamente porque esses valores são automáticos em um navegador e fáceis de esquecer em um script.
Accept-Encoding é uma afirmação que você precisa honrar. Anuncie br e zstd apenas se o seu cliente realmente conseguir descomprimi-los. Algumas bibliotecas anunciam codificações que depois não conseguem tratar, o que produz erros ou um fallback diferente do que um navegador negociaria.
Combine o Accept-Language com o país de saída
Este caso é específico do trabalho com proxies e é a incompatibilidade autoinfligida mais comum. Se você sai por um IP residencial alemão e envia Accept-Language: en-US, você descreveu um visitante cujo navegador está configurado para inglês americano, mas está em uma conexão residencial alemã. Isso acontece na vida real, mas é raro o suficiente para ser um sinal, e, na prática, isso pode mudar o que você recebe de volta: muitos sites servem conteúdo com base nesse header, então um job de coleta com segmentação geográfica pode obter o idioma errado enquanto parece estar funcionando.
Vincule o idioma à saída, idealmente no mesmo lugar em que você escolhe o país, para que os dois nunca se desalinhem:
import requests
MARKETS = {
"us": "en-US,en;q=0.9",
"de": "de-DE,de;q=0.9,en;q=0.8",
"fr": "fr-FR,fr;q=0.9,en;q=0.8",
"br": "pt-BR,pt;q=0.9,en;q=0.8",
}
UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36")
def fetch(url, country):
proxy = f"http://customer-USERNAME-country-{country}:PASSWORD@p.shifter.io:443"
headers = {
"User-Agent": UA,
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,"
"image/avif,image/webp,*/*;q=0.8",
"Accept-Language": MARKETS[country], # follows the exit, always
"Accept-Encoding": "gzip, deflate, br",
"sec-ch-ua": '"Chromium";v="141", "Not?A_Brand";v="24", '
'"Google Chrome";v="141"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"', # agrees with the UA
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Upgrade-Insecure-Requests": "1",
}
return requests.get(url, headers=headers,
proxies={"http": proxy, "https": proxy}, timeout=20)
A mesma disciplina se aplica a timezone e locale quando você está controlando um navegador, casos em que esses valores são observáveis separadamente e também devem corresponder à saída, o que se torna mais rigoroso quando você trabalha com segmentação em nível de cidade.
Não gire o User-Agent de forma aleatória
Este é um conselho que circula amplamente e causa mais mal do que bem. Escolher um UA aleatório por requisição produz um padrão que nenhuma população real apresenta: um IP que é Chrome no Windows, depois Safari em um Mac, depois Firefox no Linux, em menos de um minuto. Pior, se você mantém uma sticky session para que um único endereço atenda a um fluxo de várias etapas, trocar o UA no meio do fluxo significa que o mesmo visitante aparentemente trocou de dispositivo entre clicar em uma busca e visualizar um resultado.
O modelo coerente é uma identidade por sessão. Escolha um UA plausível, mantenha-o durante toda a vida daquela sessão, e mantenha tudo o mais consistente com ele. Se você quiser variedade em toda a sua frota, varie entre sessões, e não entre requisições, e varie em proporções realistas, e não de forma uniforme entre todos os navegadores que já existiram. Mantenha as versões atualizadas também: um UA que declara uma versão de navegador de três anos atrás já é, por si só, anômalo, já que instalações reais são atualizadas.
Headers são apenas uma camada
Vale a pena ser honesto sobre o limite aqui. Headers perfeitos não fazem um cliente Python parecer o Chrome, porque a camada abaixo ainda é diferente. O handshake TLS e as configurações de HTTP/2 que o seu cliente produz formam uma fingerprint própria, e um UA de Chrome sobre uma fingerprint que indica Python é exatamente a contradição discutida no início. Essa incompatibilidade é o tema de fingerprinting de TLS e HTTP/2, e é por isso que alguns destinos fortemente protegidos permanecem fora de alcance para clientes HTTP simples, independentemente do trabalho feito com os headers, o que é um dos motivos para recorrer a um navegador headless.
A ordem dos headers importa pelo mesmo motivo. Navegadores emitem headers em uma ordem estável; muitas bibliotecas HTTP os emitem em ordem alfabética ou na ordem de inserção, o que é outra forma de o conjunto revelar sua origem mesmo quando todos os valores estão corretos. Alguns clientes permitem controlar a ordenação, e, quando isso importa, reproduzir a ordem de um navegador real vale o esforço. O catálogo mais amplo desses sinais está em as fingerprints que podem bloquear a extração de dados, e as versões comuns autoinfligidas estão em erros que disparam detecção.
Uma checklist rápida
Envie um conjunto completo de headers de navegador, não apenas um User-Agent. Faça os client hints concordarem com o UA em marca, versão, plataforma e flag de dispositivo móvel. Defina os headers Sec-Fetch para descrever o tipo real de requisição. Vincule o Accept-Language ao país de saída e mantenha-os no mesmo trecho de código. Anuncie apenas as codificações que você consegue decodificar. Mantenha uma identidade por sessão, em vez de girar por requisição, e mantenha as versões do UA atualizadas. Depois, verifique se a sua fingerprint TLS concorda com o navegador que você afirma ser, porque essa é a camada que os headers não conseguem corrigir.
Se um destino ainda recusar você depois de tudo isso, o problema se deslocou para outro lugar: ritmo, reputação de IP, ou sinais comportamentais, que são o conjunto mais amplo abordado em evitando bloqueios e fazendo scraping de sites fortemente protegidos.
Conclusão
Os headers são a segunda metade da identidade que o seu proxy inicia. Um endereço residencial limpo torna a conexão discreta; um conjunto coerente de headers torna a requisição discreta, e coerência é o jogo todo. Cada afirmação precisa concordar com todas as outras: os client hints com o UA, o idioma com o país de saída, os metadados de fetch com o tipo de requisição, as codificações com suas capacidades reais, e o conjunto todo com a fingerprint TLS por trás dele. Uma identidade por sessão, mantida estável, supera qualquer rotação inteligente, sempre.
Essa camada de conexão é o que os proxies residenciais oferecem, endereços reais de qualidade residencial com segmentação por país e cidade, para que a geografia que os seus headers afirmam seja a geografia pela qual você realmente está saindo, cobrados por GB, de modo que ajustar as requisições para buscar menos custa menos.