El módulo http.client implementa el lado cliente de HTTP y HTTPS en un nivel inferior a urllib.request. Expone conexiones, solicitudes y respuestas directamente, permitiendo controlar rutas, cabeceras, cuerpos, streaming, reutilización, túneles CONNECT y contextos TLS.
Este control es útil para aprender HTTP, probar servidores, construir clientes específicos y diagnosticar integraciones. Para APIs normales, una biblioteca superior suele ser más productiva. Esta guía cubre el ciclo correcto de HTTPSConnection, lecturas limitadas, JSON, archivos, conexiones persistentes, proxies, errores y seguridad.
Cuándo usar http.client
Úsalo cuando necesites comportamiento HTTP/1.1 explícito, handlers propios o cero dependencias. Para URLs completas, redirects, cookies y autenticación, consulta urllib.request en Python. Las aplicaciones grandes se benefician de clientes con pooling y timeouts más completos.
Primera conexión HTTPS
import http.client
connection = http.client.HTTPSConnection(
"www.python.org",
timeout=10,
)
try:
connection.request(
"GET",
"/",
headers={
"Host": "www.python.org",
"Accept": "text/html",
"User-Agent": "MiHerramienta/1.0",
},
)
response = connection.getresponse()
print(response.status, response.reason)
body = response.read(200_000)
finally:
connection.close()
El constructor recibe hostname y puerto, no una URL completa. El target de la solicitud suele ser una ruta absoluta como /docs?page=1. Configura siempre timeout.
HTTPS y SSLContext
HTTPSConnection valida certificado y hostname por defecto. Para una CA privada, proporciona un contexto seguro.
import ssl
context = ssl.create_default_context(cafile="empresa-ca.pem")
connection = http.client.HTTPSConnection(
"api-interna.example",
timeout=10,
context=context,
)
No uses un contexto no verificado para evitar fallos. Corrige la cadena. La guía de ssl en Python explica TLS seguro.
Ciclo request y getresponse
Envía la solicitud, obtiene la respuesta y lee o cierra por completo esa respuesta antes de enviar otra en la misma conexión.
connection.request("GET", "/primero")
first = connection.getresponse()
first_data = first.read()
connection.request("GET", "/segundo")
second = connection.getresponse()
second_data = second.read()
Los bytes pendientes impiden encuadrar correctamente la siguiente respuesta. Los cuerpos grandes deben consumirse en bloques hasta EOF.
Lecturas limitadas
MAX_BYTES = 5 * 1024 * 1024
def read_limited(response, maximum=MAX_BYTES) -> bytes:
chunks = []
total = 0
while chunk := response.read(64 * 1024):
total += len(chunk)
if total > maximum:
response.close()
raise ValueError("La respuesta superó el límite")
chunks.append(chunk)
return b"".join(chunks)
Content-Length ayuda, pero cuenta los bytes reales. Para descargas, escribe en un archivo temporal.
Cabeceras
response = connection.getresponse()
content_type = response.getheader("Content-Type")
content_length = response.getheader("Content-Length")
all_headers = response.getheaders()
getheader() une valores repetidos con comas. Esto no es correcto para todos los campos, especialmente Set-Cookie. Usa getheaders() cuando necesites conservar ocurrencias.
Enviar JSON
import json
payload = json.dumps({"name": "Ejemplo"}).encode("utf-8")
headers = {
"Content-Type": "application/json",
"Accept": "application/json",
"Content-Length": str(len(payload)),
}
connection.request("POST", "/items", body=payload, headers=headers)
response = connection.getresponse()
raw = read_limited(response, 500_000)
Para un cuerpo bytes, la biblioteca puede calcular Content-Length. Si lo declaras, debe coincidir exactamente.
Subir archivos
Cuando el cuerpo es un archivo o iterable sin tamaño, la biblioteca usa normalmente transferencia chunked.
with open("archivo.bin", "rb") as file:
connection.request(
"PUT",
"/upload",
body=file,
headers={"Content-Type": "application/octet-stream"},
)
response = connection.getresponse()
response.read()
Algunos servidores antiguos no aceptan chunked. Proporciona una longitud correcta cuando sea necesario. Un archivo parcialmente leído no puede repetirse sin reposicionarlo.
Streaming de descarga
from pathlib import Path
connection.request("GET", "/download")
response = connection.getresponse()
if response.status != 200:
response.read(20_000)
raise RuntimeError(f"HTTP {response.status}")
with Path("download.tmp").open("wb") as output:
total = 0
while chunk := response.read(64 * 1024):
total += len(chunk)
if total > 100 * 1024 * 1024:
response.close()
raise ValueError("Archivo demasiado grande")
output.write(chunk)
Verifica SHA-256 antes de mover el archivo. Consulta hashlib en Python.
Estados HTTP
http.client no lanza automáticamente una excepción por 404 o 500. Inspecciona response.status.
if 200 <= response.status < 300:
data = read_limited(response)
elif response.status == 404:
response.read(20_000)
raise LookupError("Recurso no encontrado")
else:
response.read(20_000)
raise RuntimeError(f"El servidor devolvió {response.status}")
Redirects, 429, Retry-After y autenticación también quedan bajo responsabilidad de la aplicación.
Conexiones persistentes
HTTP/1.1 permite reutilización, reduciendo handshakes TCP y TLS. Reutiliza solo después de consumir la respuesta. No compartas una conexión simultáneamente entre hilos sin coordinación estricta.
Si ocurre RemoteDisconnected antes de una operación idempotente, cierra y reconecta. No repitas POST ciegamente porque el servidor puede haberlo procesado.
Solicitudes HEAD
connection.request("HEAD", "/archivo.zip")
response = connection.getresponse()
print(response.status, response.getheader("Content-Length"))
response.read()
HEAD no tiene cuerpo, pero completa el ciclo. Los metadatos no sustituyen los límites reales de un GET posterior.
Túnel CONNECT
proxy = http.client.HTTPSConnection("proxy.example", 8443, timeout=10)
proxy.set_tunnel(
"www.python.org",
443,
headers={"Host": "www.python.org:443"},
)
proxy.request("GET", "/")
response = proxy.getresponse()
Protege credenciales del proxy y valida el certificado del destino. Desde Python 3.12, CONNECT usa HTTP/1.1 y get_proxy_response_headers() permite inspeccionar las cabeceras del proxy.
API paso a paso
putrequest(), putheader(), endheaders() y send() exponen etapas inferiores. Úsalas solo si request() no basta; es fácil crear headers o chunked inválidos.
Excepciones y estado incierto
try:
connection.request("GET", "/")
response = connection.getresponse()
data = read_limited(response)
except (TimeoutError, OSError, http.client.HTTPException) as error:
connection.close()
raise RuntimeError("Fallo HTTP") from error
Excepciones importantes: IncompleteRead, BadStatusLine, LineTooLong, ResponseNotReady y RemoteDisconnected. Cierra la conexión cuando el framing sea incierto.
Debug
set_debuglevel(1) imprime detalles en stdout. Úsalo solo en desarrollo controlado, porque puede mostrar Authorization y otros datos sensibles.
Protección SSRF
Si host y puerto proceden del usuario, valida DNS e IP antes de conectar. Bloquea loopback, redes privadas, link-local y metadata de nube. Las conexiones directas también sufren SSRF y DNS rebinding.
Cuándo elegir una API superior
Usa urllib.request o un cliente externo cuando necesites redirects, cookies, parsing, autenticación y pools. http.client es apropiado para control fino y aprendizaje.
La guía para integrar APIs con Python muestra un flujo completo.
Errores comunes
Los fallos habituales son omitir timeout, pasar una URL completa como ruta, no consumir respuesta, compartir conexión sin seguridad, leer sin límite, desactivar TLS, repetir POST tras fallo y activar debug con credenciales.
Buenas prácticas
Usa HTTPS validado, timeout, cuerpos limitados, streaming y cierre garantizado. Consume o cierra cada respuesta. Reutiliza solo conexiones sanas, distingue métodos idempotentes y restringe hosts externos cuando sea posible.
Conclusión
http.client ofrece acceso directo al cliente HTTP/1.1 de Python. Controla conexiones, requests, respuestas, streaming y proxies, pero deja redirects, estados, retries y límites a tu código. Úsalo cuando ese control justifique la responsabilidad adicional.
Consulta la documentación oficial de http.client y el RFC 9112 sobre HTTP/1.1.







