Pon las credenciales en la URL del proxy y pásala como meta["proxy"]:
import scrapy
PROXY = "http://user:pass@proxy.example.com:8080"
class ExampleSpider(scrapy.Spider):
name = "example"
async def start(self):
yield scrapy.Request("https://example.com/", meta={"proxy": PROXY})
def parse(self, response):
self.logger.info("%s via %s", response.status, response.meta["proxy"])HttpProxyMiddleware está activado por defecto (HTTPPROXY_ENABLED = True). Traslada el usuario y la contraseña de la URL a una cabecera Proxy-Authorization (documentación), así que response.meta["proxy"] contiene la URL sin credenciales.
En nuestro laboratorio esto autenticó peticiones https:// y http:// contra un proxy local que exige autenticación Basic. Todo lo que sigue se registró el 7 de octubre de 2026 con Scrapy 2.19.0, Twisted 26.4.0 y Python 3.14.4.
Tres detalles antes de los errores:
- Codifica la contraseña con porcentajes.
p@ss:w/rdfuncionó comop%40ss%3Aw%2Frd. Sin codificar, la petición falló conValueError: Port could not be cast to integer value as 'w'. - Las peticiones creadas en un callback no heredan el proxy. Una petición creada con
response.follow()salió directa. Una redirección conservó el proxy. Definemeta["proxy"]en cada petición, o en un downloader middleware como se muestra más abajo. - Usa
async def start(). Un spider que solo definíastart_requests()se abrió, no envió nada y se cerró confinish_reason: finishedy sin ningún aviso. Scrapy 2.16.0 eliminó el método: «is removed and no longer called» (notas de la versión). Lo sustituyeSpider.start(). Scrapy no llama a start_requests() explica la migración, incluidos los spider middlewares.
Rotación de proxies: define meta["proxy"] en cada petición
La rotación es un downloader middleware que se ejecuta antes que HttpProxyMiddleware (orden 750) y fija una URL de proxy con credenciales:
from itertools import cycle
PROXIES = cycle([
"http://user:pass@proxy-a.example.com:8080",
"http://user:pass@proxy-b.example.com:8080",
])
class RotateProxyMiddleware:
def process_request(self, request):
request.meta["proxy"] = next(PROXIES)DOWNLOADER_MIDDLEWARES = {"myproject.middlewares.RotateProxyMiddleware": 740}En el laboratorio, una petición redirigida pasó dos veces por este middleware, fue a dos proxies y se autenticó en ambos.
La versión que falla fija una URL sin credenciales y escribe la cabecera a mano:
request.meta["proxy"] = next(PROXIES) # "http://proxy-a.example.com:8080"
request.headers["Proxy-Authorization"] = "Basic dXNlcjpwYXNz"La primera petición llegó al proxy A con la cabecera. El destino respondió 301 y la petición redirigida volvió a pasar por los middlewares. La rotación eligió el proxy B y puso la cabecera otra vez, pero el proxy B recibió la petición sin Proxy-Authorization y respondió 407. Una petición https:// reintentada perdió la cabecera de la misma forma.
HttpProxyMiddleware registra con qué URL de proxy se envió una cabecera puesta a mano. Cuando una pasada posterior sobre la misma petición tiene otra URL de proxy sin credenciales, borra la cabecera. Las notas de la versión 2.6.2 explican el motivo: los middlewares de rotación cambiaban el proxy y dejaban la cabecera antigua, de modo que las credenciales de un proxy iban a otro. Desde la 2.7.1, una cabecera puesta a mano se conserva solo «for as long as that proxy URL remains the same». Las credenciales de la URL siempre la sustituyen.
Dos resultados más del mismo código:
- En la primera pasada la cabecera aún no está ligada a un proxy. Un middleware que cambió
meta["proxy"]de A a B antes de que se ejecutaraHttpProxyMiddlewareenvió la cabecera puesta a mano al proxy B. - Sin ningún proxy, la cabecera se eliminó antes de que la petición llegara al destino.
Variables de entorno: HTTP_PROXY se usa por defecto
Scrapy lee las variables de proxy cuando arranca el rastreo, y no hace falta ningún ajuste para activarlo. Con HTTP_PROXY=http://user:pass@127.0.0.1:8888 y sin meta["proxy"], la petición del laboratorio pasó por el proxy y se autenticó.
HTTP_PROXYyhttp_proxyen minúsculas se aplicaron a las URLhttp://, yHTTPS_PROXYa las URLhttps://. Con soloHTTP_PROXYdefinida, una peticiónhttps://salió directa.ALL_PROXYse ignoró.NO_PROXY=127.0.0.1envió la petición directa.meta["proxy"]tuvo prioridad sobre las variables e ignoróNO_PROXY, como dice la documentación.meta={"proxy": None}envió una petición directa con la variable definida.HTTPPROXY_ENABLED = Falsehizo que Scrapy dejara de leer las variables. También desactivó el tratamiento de las credenciales:meta["proxy"] = "http://user:pass@127.0.0.1:8888"lanzó entoncesValueError: invalid hostname: user:pass@127.0.0.1para una URLhttp://y elTunnelError407 parahttps://.
Para ignorar el entorno y que meta["proxy"] siga funcionando, sustituye el middleware por una subclase que descarta lo que leyó:
from scrapy.downloadermiddlewares.httpproxy import HttpProxyMiddleware
class ExplicitProxyMiddleware(HttpProxyMiddleware):
def __init__(self, auth_encoding="latin-1"):
super().__init__(auth_encoding)
self.proxies = {}DOWNLOADER_MIDDLEWARES = {
"scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware": None,
"myproject.middlewares.ExplicitProxyMiddleware": 750,
}proxies no es un atributo documentado, así que vuelve a probarlo después de actualizar. aiohttp hace lo contrario: ignora las variables salvo que pases trust_env=True (Proxies en aiohttp 3.14). Variables de entorno de proxy compara otros clientes.
TunnelError: Could not open CONNECT tunnel with proxy
scrapy.core.downloader.handlers.http11.TunnelError: Could not open CONNECT tunnel with proxy 127.0.0.1:8888 [{'status': 407, 'reason': b'Proxy Authentication Required'}]El proxy rechazó la petición CONNECT de una URL https://. El mensaje fue idéntico sin credenciales y con una contraseña incorrecta, y no contiene las credenciales, así que no te dice cuál de los dos casos tienes. Revisa la URL de meta["proxy"] en la petición que falla y después sigue el orden de comprobaciones de Corrige el error de proxy 407.
TunnelErrorestá enRETRY_EXCEPTIONS. Con la configuración por defecto, el proxy del laboratorio recibió elCONNECTrechazado tres veces. El registro mostróRetrying <GET https://127.0.0.1:8443/> (failed 1 times), después(failed 2 times)y despuésGave up retrying. Con"dont_retry": Trueenmeta, el proxy recibió uno.handle_httpstatus_listno cambia nada aquí. No hay respuesta, solo la excepción, así que trátala en unerrback.
Ignoring response 407: HTTP status code is not handled or not allowed
Ignoring response <407 http://127.0.0.1:8080/>: HTTP status code is not handled or not allowedPara una URL http://, el 407 del proxy es una respuesta normal y HttpErrorMiddleware la descarta. El callback no se llamó nunca, esta línea INFO fue la única señal y el rastreo se cerró con finish_reason: finished. Con un errback, la línea no se registra y el errback recibe scrapy.spidermiddlewares.httperror.HttpError: Ignoring non-200 response. Scrapy no reintentó la petición.
Para recibir la respuesta en el callback, permite ese estado en la petición:
yield scrapy.Request(url, meta={"proxy": PROXY, "handle_httpstatus_list": [407]})El atributo del spider handle_httpstatus_list = [407] y el ajuste HTTPERROR_ALLOWED_CODES = [407] hicieron lo mismo (HttpErrorMiddleware). El callback recibió entonces el estado 407 con Proxy-Authenticate: Basic realm="lab". Sin ninguno de ellos, las estadísticas del rastreo cuentan las respuestas descartadas en httperror/response_ignored_status_count/407.
UnsupportedURLSchemeError: Unsupported scheme: b'socks5'
scrapy.exceptions.UnsupportedURLSchemeError: Unsupported scheme: b'socks5'El download handler por defecto de Scrapy no admite SOCKS. Con meta["proxy"] = "socks5://127.0.0.1:1080", una petición http:// lanzó este error una vez, sin reintentos. socks5h:// lanzó Unsupported scheme: b'socks5h', y una URL SOCKS en HTTP_PROXY lanzó el mismo error.
Una petición https:// no lo lanzó. Scrapy envió CONNECT 127.0.0.1:8443 como HTTP en claro al puerto SOCKS, el servidor SOCKS del laboratorio cerró la conexión y Scrapy esperó el timeout de descarga tres veces antes de informar DownloadTimeoutError: Getting https://127.0.0.1:8443/ took longer than 15.0 seconds. El laboratorio fijó DOWNLOAD_TIMEOUT en 15. El valor por defecto es 180.
Hay dos salidas:
- Usa un endpoint de proxy HTTP si tu proveedor tiene uno. Entonces se aplica todo lo anterior.
- Cambia a
HttpxDownloadHandler. Viene con Scrapy, y la documentación indica que admite SOCKS5 y lo marca como experimental y todavía no recomendado para producción.
python -m pip install "scrapy[httpx]"DOWNLOAD_HANDLERS = {
"http": "scrapy.core.downloader.handlers._httpx.HttpxDownloadHandler",
"https": "scrapy.core.downloader.handlers._httpx.HttpxDownloadHandler",
}Con este handler, socks5:// devolvió 200 para ambos esquemas. Para http://localhost:8080/ el servidor SOCKS registró un nombre de dominio, así que el handler envía el nombre de host al proxy. socks5h:// también funcionó. socks5 vs socks5h muestra qué hacen otros clientes con los dos esquemas. Sus errores se leen distinto: un CONNECT rechazado apareció como scrapy.exceptions.DownloadConnectionRefusedError: 407 Proxy Authentication Required.
Reintentos: RetryMiddleware repite las peticiones POST
RetryMiddleware reintentó un POST igual que reintenta un GET. En el laboratorio, un POST pasó por el proxy hasta un destino que leyó la petición y cerró la conexión sin responder. Scrapy registró Retrying <POST http://127.0.0.1:8081/jobs> (failed 1 times) y lo envió de nuevo. El destino recibió el mismo cuerpo dos veces y el callback vio un solo 201.
Para una petición que no es seguro repetir, pon "dont_retry": True en meta (RetryMiddleware). El destino recibió entonces un solo POST y el errback recibió scrapy.exceptions.DownloadFailedError. Reintentos con proxy: una respuesta perdida, dos trabajos creados muestra qué cambia una clave de idempotencia.
Cada reintento es otra petición a través del proxy. ¿Cuántos GB de proxies necesitas? mide cuánto consume una descarga de solo HTML. Si quieres ayuda de diagnóstico dentro de un agente de programación, Proxy Toolkit MCP acepta descripciones de errores saneadas. Mantén las credenciales fuera de sus argumentos.
Cómo se probó
En Ubuntu 26.04.1 con Python 3.14.4, Scrapy 2.19.0, Twisted 26.4.0 y httpx2 2.13.1, todo en 127.0.0.1: python -m http.server como destinos HTTP y HTTPS, tres proxies que exigen autenticación Basic y registran cada petición con los nombres de sus cabeceras, un servidor SOCKS5 que registra cada destino y un destino que pierde la respuesta al primer POST. Cada uno de los 62 casos ejecutó scrapy runspider en un intérprete nuevo con un entorno limpio. No se usó ninguna cuenta de proxy. En Python 3.15.0, pip install scrapy falla sin un compilador de C porque brotli no tenía wheel cp315 el 14 de octubre de 2026; las soluciones probadas están en Python 3.15 para web scraping.
El archivo del laboratorio contiene el ejecutor, los casos, el proxy, el servidor SOCKS5, el README y los resultados registrados.
No se probó: macOS y Windows, autenticación de proxy Digest y NTLM, proxies SOCKS con autenticación, proxies a los que se llega por TLS, HTTP/2 y el download handler de Scrapy basado en aiohttp.
ipvolt es un servicio de proxies para desarrolladores que todavía no está abierto; únete a la lista de acceso anticipado y recibirás un solo correo cuando abra.
Fuentes y lecturas adicionales
Referencias técnicas usadas para esta guía. Consulta la documentación de tu versión instalada y la configuración compatible de tu proveedor.
- Scrapy documentation: HttpProxyMiddleware and HTTPPROXY_ENABLED
- Scrapy documentation: Request.meta special keys (proxy, dont_retry, handle_httpstatus_list)
- Scrapy documentation: HttpErrorMiddleware and HTTPERROR_ALLOWED_CODES
- Scrapy documentation: RetryMiddleware
- Scrapy documentation: Spider.start()
- Scrapy documentation: HttpxDownloadHandler
- Scrapy release notes: 2.16.0, start_requests() removed
- Scrapy release notes: 2.6.2, Proxy-Authorization security fix
- Scrapy release notes: 2.7.1, explicit Proxy-Authorization header