Un patrón habitual en las conversaciones de soporte: alguien cambia a proxies residenciales, la tasa de bloqueo mejora y, aun así, un subconjunto obstinado de objetivos sigue rechazándolos. La IP está limpia, la geografía es correcta y las solicitudes siguen siendo desafiadas. Casi siempre la respuesta está en las cabeceras, porque la dirección te lleva hasta la puerta y las cabeceras deciden si lo que entra parece un navegador.
El error que subyace a la mayoría de los problemas de cabeceras es pensar en ellas como una lista de valores que hay que configurar correctamente. No lo es. Es un conjunto de afirmaciones que deben concordar entre sí y con todo lo demás sobre tu conexión, y una solicitud construida con piezas incompatibles resulta más sospechosa que una hecha sin ningún esfuerzo.
La coherencia vence a cualquier valor individual
Empecemos por aquí, porque esto replantea todo lo que sigue. Un servidor no evalúa tu User-Agent de forma aislada. Ve un conjunto: la cadena UA, los client hints que la acompañan, el conjunto de cabeceras y su orden, el valor de Accept-Language, el handshake TLS subyacente y la IP desde la que llegó todo. Los navegadores reales producen conjuntos internamente coherentes porque el mismo software generó todo.
Los scrapers producen conjuntos incoherentes por accidente. Un UA de Chrome que llega con el conjunto de cabeceras de una librería HTTP de Python, un UA de Windows sobre una huella TLS que pertenece a una herramienta de Linux, una salida en EE. UU. enviando Accept-Language: de-DE, o un navegador declarado que nunca solicita los recursos que un navegador solicitaría. Ninguno de estos elementos por sí solo dice “bot”, pero la contradicción sí lo dice, y las contradicciones son mucho más fáciles de detectar de forma fiable que cualquier señal aislada. Es la misma lógica que el emparejamiento de dispositivo y red en los navegadores antidetección: cada capa tiene que contar la misma historia.
Así que el objetivo no es la cadena UA más convincente. Es una solicitud en la que el UA, las cabeceras, la huella TLS y la dirección de salida describen todos al mismo visitante plausible.
Lo que realmente envía un navegador real
Si solo configuras User-Agent, ya eres incoherente, porque ningún navegador envía un UA y nada más. Una solicitud moderna de Chrome para una página lleva, como mínimo, un conjunto similar a 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
Hay tres cosas ahí que merece la pena entender en lugar de copiar sin más.
Los client hints deben coincidir con el UA. La lista de marcas de sec-ch-ua, sec-ch-ua-platform y sec-ch-ua-mobile son una reformulación estructurada de lo que la cadena UA ya afirma. Si tu UA dice Chrome en Windows y tu client hint de plataforma dice macOS, o la versión en la lista de marcas no coincide con la versión del UA, te has contradicho en dos cabeceras adyacentes. No enviar ningún client hint mientras se declara un Chrome reciente también es una incoherencia, ya que un Chrome real los envía.
Las cabeceras Sec-Fetch describen el contexto. Indican al servidor qué tipo de solicitud es esta: una navegación de nivel superior, la obtención de un subrecurso, una XHR del mismo origen. La carga de una página es Dest: document, Mode: navigate, Site: none si se abre directamente o same-origin si se sigue un enlace interno. Una XHR a una API es Dest: empty, Mode: cors. Equivocarse en esto delata precisamente porque son automáticas en un navegador y fáciles de olvidar en un script.
Accept-Encoding es una afirmación que debes cumplir. Anuncia br y zstd solo si tu cliente puede realmente descomprimirlos. Algunas librerías anuncian codificaciones que luego no logran gestionar, lo que produce errores o una alternativa que difiere de lo que negociaría un navegador.
Haz coincidir Accept-Language con tu país de salida
Este punto es específico del trabajo con proxies y es la incoherencia autoinfligida más común. Si sales a través de una IP residencial alemana y envías Accept-Language: en-US, has descrito a un visitante cuyo navegador está configurado en inglés americano y que se encuentra en una conexión doméstica alemana. Eso ocurre en la vida real, pero es lo bastante inusual como para ser una señal, y más prácticamente puede cambiar lo que recibes de vuelta: muchos sitios sirven contenido según esa cabecera, así que un trabajo de recolección con segmentación geográfica puede obtener el idioma equivocado aunque parezca funcionar.
Vincula el idioma a la salida, idealmente en el mismo lugar donde eliges el país, para que los dos nunca puedan desincronizarse:
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)
La misma disciplina se aplica a la zona horaria y la configuración regional cuando estás manejando un navegador, donde estos valores son observables por separado y también deberían coincidir con la salida, lo cual se vuelve más exigente cuando trabajas con segmentación a nivel de ciudad.
No rotes el User-Agent de forma aleatoria
Este es un consejo que circula ampliamente y hace más daño que bien. Elegir un UA aleatorio por cada solicitud produce un patrón que ninguna población real presenta: una IP que es Chrome en Windows, luego Safari en un Mac, luego Firefox en Linux, todo en un minuto. Peor aún, si mantienes una sesión persistente para que una misma dirección sirva un flujo de varios pasos, cambiar el UA a mitad del flujo implica que el mismo visitante aparentemente cambió de dispositivo entre hacer clic en la búsqueda y ver un resultado.
El modelo coherente es una identidad por sesión. Elige un UA plausible, mantenlo durante toda la vida de esa sesión y mantén todo lo demás coherente con él. Si quieres variedad en tu flota, varía entre sesiones en lugar de entre solicitudes, y varía en proporciones realistas en lugar de de manera uniforme entre todos los navegadores que hayan existido jamás. Mantén también las versiones actualizadas: un UA que declara una versión de navegador de hace tres años es en sí mismo anómalo, ya que las instalaciones reales se actualizan.
Las cabeceras son solo una capa
Vale la pena ser honestos sobre el límite de todo esto. Unas cabeceras perfectas no hacen que un cliente Python parezca Chrome, porque la capa subyacente sigue siendo diferente. El handshake TLS y los ajustes de HTTP/2 que produce tu cliente forman una huella propia, y un UA de Chrome sobre una huella que dice Python es exactamente la contradicción de la que hablábamos al principio. Esa incoherencia es el tema de las huellas TLS y HTTP/2, y es la razón por la que algunos objetivos fuertemente protegidos siguen fuera de alcance para clientes HTTP simples independientemente del trabajo con cabeceras, lo cual es uno de los casos para recurrir a un navegador headless.
El orden de las cabeceras importa por la misma razón. Los navegadores emiten las cabeceras en un orden estable; muchas librerías HTTP las emiten en orden alfabético o por orden de inserción, lo cual es otra forma en que el conjunto puede delatar su origen incluso cuando todos los valores son correctos. Algunos clientes permiten controlar el orden y, cuando importa, hacerlo coincidir con el de un navegador real merece el esfuerzo. El catálogo más amplio de estas señales está en las huellas que pueden bloquear la extracción de datos, y las versiones autoinfligidas más comunes están en los errores que activan la detección.
Una lista de comprobación breve
Envía un conjunto completo de cabeceras de navegador, no solo un User-Agent. Haz que los client hints concuerden con el UA en marca, versión, plataforma e indicador móvil. Configura las cabeceras Sec-Fetch para que describan el tipo real de solicitud. Vincula Accept-Language al país de salida y mantenlos en la misma ruta de código. Anuncia solo las codificaciones que puedas decodificar. Mantén una identidad por sesión en lugar de rotar por solicitud, y mantén actualizadas las versiones del UA. Después, comprueba si tu huella TLS concuerda con el navegador que dices ser, porque esa es la capa que las cabeceras no pueden arreglar.
Si un objetivo sigue rechazándote después de todo esto, el problema se ha trasladado a otro lugar: ritmo de solicitudes, reputación de IP o señales de comportamiento, que son el conjunto más amplio tratado en cómo evitar bloqueos y cómo raspar sitios fuertemente protegidos.
La conclusión
Las cabeceras son la segunda mitad de la identidad que inicia tu proxy. Una dirección residencial limpia hace que la conexión pase desapercibida; un conjunto de cabeceras coherente hace que la solicitud pase desapercibida, y la coherencia es el juego entero. Cada afirmación tiene que concordar con todas las demás: los client hints con el UA, el idioma con el país de salida, los metadatos de fetch con el tipo de solicitud, las codificaciones con tus capacidades reales, y el conjunto completo con la huella TLS subyacente. Una identidad por sesión, mantenida de forma estable, vence a cualquier rotación ingeniosa.
Esa capa de conexión es lo que proporcionan los proxies residenciales, direcciones reales de nivel doméstico con segmentación por país y ciudad, de modo que la geografía que declaran tus cabeceras sea la geografía por la que realmente sales, facturados por GB para que ajustar las solicitudes y obtener menos datos te cueste menos.