El módulo urllib.error reúne las excepciones utilizadas por urllib.request cuando falla una operación de red. Distinguir un error HTTP de un problema de DNS, timeout, certificado inválido, conexión rechazada o descarga incompleta permite construir clientes más confiables, logs más útiles y políticas de retry que no empeoran una incidencia.
Esta guía se concentra en el tratamiento de excepciones del cliente estándar. Para crear requests, headers, proxies y descargas con límites, consulta urllib.request en Python. Para separar y validar componentes de URLs, revisa urllib.parse en Python.
La jerarquía básica
URLError es la excepción base y deriva de OSError. HTTPError deriva de URLError. Por eso importa el orden de los bloques except: captura HTTPError antes de URLError, o el manejador más general ocultará el caso específico.
from urllib.error import HTTPError, URLError
from urllib.request import urlopen
try:
with urlopen("https://example.com/recurso", timeout=10) as respuesta:
datos = respuesta.read(100_000)
except HTTPError as error:
print("Estado HTTP:", error.code)
except URLError as error:
print("Fallo de transporte:", error.reason)
Un estado 404 o 500 significa que llegó una respuesta HTTP válida, aunque informe un fallo. Un URLError puro puede representar resolución de nombre, conexión rechazada, timeout, TLS u otra etapa anterior a la respuesta.
Comprender URLError
El atributo reason puede ser una cadena u otra excepción. Evita tomar decisiones comparando únicamente mensajes. Inspecciona el tipo subyacente cuando la diferencia sea relevante.
import socket
from urllib.error import URLError
from urllib.request import urlopen
try:
urlopen("https://example.invalid", timeout=5)
except URLError as error:
if isinstance(error.reason, socket.timeout):
print("La conexión agotó el tiempo")
else:
print(type(error.reason).__name__, error.reason)
Según la plataforma y el camino interno, un timeout puede aparecer como TimeoutError, socket.timeout o una excepción anidada. Las pruebas deben reflejar el entorno real, sin convertir textos inestables en un contrato rígido.
HTTPError es excepción y respuesta
HTTPError expone url, code, reason, headers y un objeto similar a archivo en fp. El propio error también puede leerse como la respuesta. Así es posible extraer un cuerpo JSON o texto enviado por el servidor.
import json
from urllib.error import HTTPError
from urllib.request import Request, urlopen
request = Request("https://api.example.com/items/999")
try:
with urlopen(request, timeout=10) as respuesta:
payload = json.load(respuesta)
except HTTPError as error:
cuerpo = error.read(64_000)
tipo = error.headers.get_content_type()
if tipo == "application/json":
detalle = json.loads(cuerpo.decode("utf-8"))
else:
detalle = cuerpo.decode("utf-8", errors="replace")
print(error.code, detalle)
Limita siempre el cuerpo de error. Un servidor remoto no confiable puede devolver megabytes o mantener la conexión ocupada. Un estado de error no hace que el contenido sea seguro para logs o HTML.
Clasificar estados
Una política práctica puede agrupar respuestas:
- 400–499 suelen indicar problemas de request, autenticación, autorización o recurso.
- 500–599 suelen indicar un fallo temporal o interno del servidor.
- 429 pide reducir el ritmo y puede incluir
Retry-After. - 401 y 403 no deben provocar repeticiones infinitas con las mismas credenciales.
No repitas automáticamente cualquier estado. Reenviar un POST no idempotente puede duplicar pedidos, cobros, mensajes o registros.
Retries selectivos con backoff
import time
from urllib.error import HTTPError, URLError
from urllib.request import urlopen
REPETIBLES = {429, 500, 502, 503, 504}
def descargar(url: str, intentos: int = 3) -> bytes:
for indice in range(intentos):
try:
with urlopen(url, timeout=10) as respuesta:
return respuesta.read(1_000_000)
except HTTPError as error:
if error.code not in REPETIBLES or indice == intentos - 1:
raise
except URLError:
if indice == intentos - 1:
raise
time.sleep(2 ** indice)
raise RuntimeError("flujo imposible")
En producción, añade jitter aleatorio para evitar que muchos clientes repitan al mismo tiempo. Respeta Retry-After cuando corresponda e impone un presupuesto total de tiempo. Varias tentativas no deberían consumir cada una todo el plazo de usuario.
Un timeout no es un límite completo
El argumento timeout limita operaciones bloqueantes, pero un cliente robusto también restringe tamaño, redirects y duración total. Leer hasta el final sin límite puede tardar mucho después de establecer la conexión.
Cuando el usuario controla la URL, añade defensas contra SSRF: esquemas y puertos permitidos, validación DNS e IP, control de redirects y límites de respuesta. El manejo de excepciones no valida el destino.
Fallos de TLS
Los problemas de certificado suelen llegar mediante URLError con una excepción SSL en reason. No los soluciones desactivando la validación.
import ssl
from urllib.error import URLError
try:
# llamada HTTPS
pass
except URLError as error:
if isinstance(error.reason, ssl.SSLCertVerificationError):
print("No fue posible validar el certificado")
raise
Corrige el reloj, la cadena de confianza, el hostname o la CA. La guía de ssl en Python explica contextos seguros y validación de certificados.
ContentTooShortError
ContentTooShortError se relaciona con urlretrieve() cuando el contenido recibido es menor que el Content-Length esperado. Su atributo content conserva los datos descargados, pero deben considerarse incompletos.
from urllib.error import ContentTooShortError
from urllib.request import urlretrieve
try:
ruta, headers = urlretrieve(
"https://example.com/archivo.zip",
"archivo.zip",
)
except ContentTooShortError as error:
print("Descarga truncada:", len(error.content))
raise
No proceses silenciosamente un archivo parcial. Elimina o aísla el destino temporal, repite según la política y verifica hash o firma cuando exista. Para ubicaciones temporales seguras, usa tempfile en Python.
Cuerpos con encoding desconocido
El servidor puede declarar charset, omitirlo o informar uno incorrecto. Para diagnóstico, usa el charset declarado cuando sea adecuado y un fallback con reemplazo. Una decodificación UTF-8 estricta dentro del manejador puede lanzar una segunda excepción y ocultar el problema original.
def leer_error(error: HTTPError, limite: int = 64_000) -> str:
datos = error.read(limite)
charset = error.headers.get_content_charset() or "utf-8"
return datos.decode(charset, errors="replace")
Logs sin filtrar secretos
Registra método, host, ruta normalizada, estado, duración e identificador de correlación. No registres tokens, headers Authorization, cookies, contraseñas en URLs ni cuerpos completos con datos personales.
El atributo url puede incluir una query sensible. Enmascara parámetros antes del log. Limita también el texto de reason, porque contenido influido externamente puede alcanzar sistemas operativos.
Fallo esperado frente a bug
Captura únicamente las excepciones que puedes tratar. Un except Exception alrededor de todo el flujo puede transformar un defecto de programación en una advertencia de red. Mantén pequeña la región del try.
try:
respuesta = urlopen(request, timeout=10)
except (HTTPError, URLError) as error:
manejar_fallo_red(error)
else:
with respuesta:
procesar(respuesta)
Los errores de procesar() ya no quedan clasificados incorrectamente como fallos de transporte.
APIs de CLI y bibliotecas
Una herramienta de línea de comandos puede convertir fallos conocidos en mensajes breves y códigos de salida estables, conservando detalles en modo verbose. Una biblioteca reutilizable normalmente debe propagar el error original o envolverlo con raise ErrorDominio(...) from error para mantener la causa.
Probar sin depender de internet
Ejecuta un servidor HTTP local que produzca 404, 429, 500, redirects, respuestas lentas y cuerpos truncados. La guía de socketserver en Python ayuda a preparar integraciones controladas. Los mocks son rápidos, pero una prueba local de protocolo descubre errores de framing y lectura.
Incluye pruebas para el orden de except, límites de cuerpo, retry solo en operaciones elegibles, conservación de causas y eliminación de secretos.
Errores comunes
Los fallos frecuentes son capturar URLError antes que HTTPError, repetir toda respuesta, desactivar TLS, leer cuerpos ilimitados, comparar textos inestables, registrar credenciales, aceptar archivos truncados, repetir POST sin protección de idempotencia y capturar excepciones demasiado amplias.
Conclusión
urllib.error separa respuestas HTTP, fallos de transporte y descargas incompletas. Esa distinción mejora mensajes, métricas y decisiones de recuperación. Usa HTTPError para examinar estado, headers y cuerpo; inspecciona URLError.reason para conocer la causa; y trata los datos de ContentTooShortError como incompletos y no confiables.
Consulta la documentación oficial de urllib.error y HTTP Semantics. Un cliente seguro combina timeouts, límites, validación de destino, retries selectivos y logs sin secretos.







