¿Error ProxyError de Requests en una API privada? Comprueba primero el entorno
Requests fusiona la configuración de proxy, autenticación y CA del entorno. Lee lo que ve el proceso, excluye el host privado y mantén activa la verificación TLS.
La VPN está activa, el panel interno se abre en el navegador y el script de Python sigue lanzando un error de proxy. En esa imagen no hay nada mal con el túnel: el intérprete lee una dirección de proxy de su propio entorno y envía allí la petición. Este manual lee el entorno que el proceso ve realmente, aplica la exclusión más estrecha y solo entonces considera una política de sesión explícita. Cada paso termina con un comando cuya salida decide si continúas.
Lee el entorno que el proceso ve realmente
La configuración de proxy del escritorio, las exportaciones del shell y las líneas Environment= de una unidad de systemd son tres fuentes distintas, y Requests solo lee la última que hereda. Ejecuta el diagnóstico de abajo en el mismo shell, contenedor o unidad que ejecuta el script fallido, no en tu terminal interactiva. Una variable exportada en tu shell de inicio de sesión es invisible para un proceso de contenedor, y esa brecha explica muchos informes de un proxy que aparece de la nada. Los dos comandos imprimen los mismos datos con distinto nivel de detalle.
env | grep -i -E 'proxy|requests_ca_bundle|curl_ca_bundle'
python3 -c "import os;[print(k,'=',v) for k,v in sorted(os.environ.items()) if 'proxy' in k.lower()]"
Se leen ambas grafías de cada variable, y las entradas duplicadas se aplican en el orden en que el proceso las recibió, así que un HTTP_PROXY obsoleto puede anular el valor que acabas de exportar. Las variables del paquete de CA también importan aquí, porque cambian a qué resuelve verify sin aparecer en ningún lugar del código. Si la salida está vacía, el entorno no es tu problema y puedes dejar de leer esta sección. Si enumera un proxy que no reconoces, encuentra el archivo o la unidad que lo define antes de cambiar nada.
Pregunta a Requests qué haría con una URL concreta. Esta llamada muestra los valores combinados de proxy, verify y cert para esa petición sin enviar un paquete.
python3 -c "import requests;s=requests.Session();print(s.merge_environment_settings('https://api.corp.example/status',{},None,None,None))"
Una entrada proxies no vacía para el host privado es tu respuesta, y el valor verify impreso te dice en qué paquete de CA confiaría la petición. Si ambos parecen correctos, el fallo está más adelante en la pila y deberías saltar a la sección de verificación. No adivines a partir del código: la fusión ocurre por petición, por eso una sesión puede comportarse distinto para dos hosts.
Excluye el host privado antes de desactivar nada
Cuando el proxy es realmente necesario para otros destinos, la corrección estrecha es una exclusión para este host. Añade a la lista existente en lugar de reemplazarla, porque borrar NO_PROXY rompe cualquier otro destino interno que dependiera de él. Los comandos de abajo añaden la entrada y luego preguntan a Requests si se aplica la omisión.
export NO_PROXY="${NO_PROXY:+$NO_PROXY,}api.corp.example,.corp.example"
python3 -c "import requests;print(requests.utils.should_bypass_proxies('https://api.corp.example/status',None))"
La coincidencia es por sufijo de host, así que .corp.example cubre todo el dominio mientras que api.corp.example cubre un solo nombre. Un asterisco solo desactiva el proxy para todos los hosts y casi nunca es lo que quiere una tarea de producción. Que se respete una entrada CIDR depende de las versiones de Requests y urllib3 de la imagen, así que prueba la entrada exacta en lugar de suponer que funciona. Comprueba el resultado de should_bypass_proxies para la URL real, no para un nombre de host que hayas escrito de memoria.
Mantén las dos capas separadas en tu cabeza. Evitar el proxy HTTP no evita la VPN, porque la decisión del proxy ocurre en la biblioteca y la del túnel en la tabla de enrutamiento del núcleo.
Desactiva trust_env solo como política de sesión deliberada
Si todo el trabajo debe ignorar la configuración ambiental, construye la sesión explícitamente y registra la decisión en el código. Eso es una política, no un parche, y pertenece a un diff que un revisor pueda ver. El ejemplo de abajo mantiene un tiempo de espera, una ruta de CA explícita y una comprobación de estado.
import requests
with requests.Session() as session:
session.trust_env = False
response = session.get(
"https://api.corp.example/status",
timeout=(5, 20),
verify="/etc/ssl/certs/corp-ca.pem",
)
response.raise_for_status()
Desactivar trust_env cambia tres comportamientos a la vez, y solo uno de ellos tiene que ver con proxies. Requests deja de leer credenciales de .netrc, y las variables REQUESTS_CA_BUNDLE y CURL_CA_BUNDLE dejan de afectar a verify, que entonces recurre al almacén de certificados incluido. Si tu organización termina TLS con su propia CA, ese respaldo provoca un error de verificación que parece no tener relación con el cambio que acabas de hacer. Pasar verify explícitamente es lo que mantiene separados los dos efectos.
Lo que no deberías hacer es establecer verify=False para silenciar el error resultante. Eso cambia una configuración de proxy incorrecta por una conexión no verificada, y oculta el problema de certificado que la exclusión debía revelar.
Verifica la capa que falla y mantén una reversión
Tres fallos se parecen en un registro y no tienen nada en común: un proxy que no alcanza el destino lanza ProxyError o devuelve 502, un problema de confianza lanza SSLError, y un rechazo de la aplicación devuelve 401 o 403. Reproduce la petición fuera de Requests para ver cuál tienes. Los comandos de abajo quitan el proxy, prueban la cadena de certificados y luego repiten la llamada a través de la biblioteca.
curl -sv --noproxy '*' https://api.corp.example/status -o /dev/null
openssl s_client -connect api.corp.example:443 -servername api.corp.example </dev/null
python3 -c "import requests;r=requests.get('https://api.corp.example/status',timeout=10);print(r.status_code)"
Si la llamada con curl tiene éxito y la llamada de Python falla, el entorno sigue siendo la diferencia. Si ambas fallan en la etapa TLS, el proxy nunca fue el problema y lo es la cadena de certificados. Si ambas devuelven 401, la petición llegó a la aplicación y el trabajo restante es la autenticación. Anotar cuál de los tres has observado ahorra una hora a la siguiente persona.
La reversión es una línea en cualquier dirección: quita la exclusión o vuelve a poner trust_env. Guarda el valor anterior de NO_PROXY antes de añadir algo, y prefiere un script envoltorio o una entrada Environment= a nivel de unidad a una exportación que solo viva en el historial de tu terminal. Ambos cambios son visibles en el entorno del proceso, así que un reinicio suele devolverte al comportamiento antiguo.
Termina confirmando cuatro cosas: la configuración combinada para la URL privada no muestra ningún proxy, el certificado valida contra la CA prevista, la petición devuelve el estado que esperas y el cambio está confirmado en un lugar reproducible. Un script en verde con un entorno sin explicar es una corrección que vuelve a romperse en la siguiente máquina. Los comandos de este manual son los que hay que pegar en el ticket.