Somos Geonode y vendemos proxies, por lo que esta es una guía para utilizar nuestro tipo de producto con un cliente concreto. La frase que merece la pena leer aunque te saltes el resto: comprueba la dirección de salida tras la configuración, ya que una configuración de proxy que no funciona no genera ningún error. SuperAgent enviará sin problemas tu solicitud directamente, devolverá un 200 y no te dará ninguna indicación de que se haya eludido el proxy. La sección de verificación consta de cuatro líneas y marca la diferencia entre saberlo y darlo por sentado.
Ten en cuenta también que SuperAgent funciona también en navegadores, donde nada de esto es aplicable: no se le puede indicar a un navegador que utilice un proxy desde JavaScript, por lo que todo lo que aquí se explica es exclusivo de Node.
El problema de la terminología
Aclaremos esto primero, porque da lugar a errores reales.
En SuperAgent, .agent() sin argumentos crea una copia de SuperAgent que conserva las cookies. La documentación lo deja claro: «En Node, SuperAgent no guarda las cookies por defecto, pero puedes utilizar el método .agent() para crear una copia de SuperAgent que guarde las cookies. Cada copia tiene un almacén de cookies independiente».
const agent = request.agent();
await agent.post("/login").send({ user, pass });
await agent.get("/cookied-page"); // session cookie carried over
Ese agente también tiene valores por defecto: «Los métodos de solicitud habituales invocados en el agente se utilizarán como valores por defecto para todas las solicitudes realizadas por ese agente».
Por su parte, .agent(httpAgent) con un argumento establece el http.Agent de Node para la solicitud, que es donde reside la compatibilidad con el proxy.
Mismo nombre de método, dos funciones distintas, que solo se diferencian en si se pasa algún argumento. Si has estado leyendo sobre los agentes SuperAgent y sobre los agentes proxy en la misma sesión, vale la pena aclarar esto antes de escribir nada.
Opción 1: Un agente proxy
Es el enfoque más recomendable, y el motivo es el mantenimiento.
import request from "superagent";
import { HttpsProxyAgent } from "https-proxy-agent";
const agent = new HttpsProxyAgent("http://myuser:mypass@proxy.example.com:9000");
const res = await request
.get("https://api.example.com/items")
.agent(agent);
Para SOCKS, cambia el paquete:
import { SocksProxyAgent } from "socks-proxy-agent";
const agent = new SocksProxyAgent("socks5h://proxy.example.com:1080");
Ten en cuenta que es socks5h
en lugar de socks5
. La variante h
resuelve los nombres de host en el proxy en lugar de hacerlo localmente, lo que evita que las consultas DNS se dirijan a tu propio resolutor mientras tu tráfico sale por otra parte —una fuga que, de forma silenciosa, anula el sentido de utilizar un proxy para la geolocalización.
Además, proxy-agent
gestiona cualquier protocolo que especifique la URL, lo cual resulta útil cuando el proxy proviene de la configuración:
import { ProxyAgent } from "proxy-agent";
const agent = new ProxyAgent(); // reads http_proxy / https_proxy / no_proxy
¿Por qué estos paquetes en lugar de la extensión SuperAgent? Los tres se mantienen de forma activa. Según el registro de npm de septiembre de 2026, proxy-agent
está en la versión 8.0.2, https-proxy-agent
en la 9.1.0 y socks-proxy-agent
en la 10.1.0, todas publicadas en junio de 2026.
Ruta dos: superagent-proxy
La extensión diseñada específicamente para este fin, y la advertencia que conlleva.
import request from "superagent";
import superagentProxy from "superagent-proxy";
superagentProxy(request);
const res = await request
.get("https://api.example.com/items")
.proxy("http://myuser:mypass@proxy.example.com:9000");
Su archivo README la describe como una extensión de «la clase Request de superagent con una función .proxy(uri)», y señala que «cuenta con el respaldo del módulo proxy-agent».
La API es más elegante que pasar un agente. La llamada .proxy(uri) se lee mejor en una cadena y acepta URI «HTTP, HTTPS o SOCKS», delegando la selección del protocolo a proxy-agent.
La salvedad es la fecha de lanzamiento. superagent-proxy se encuentra en la versión 3.0.0, publicada en septiembre de 2021 —tiene aproximadamente cinco años en el momento de escribir este artículo—, mientras que su dependencia subyacente proxy-agent ha seguido publicando nuevas versiones. No está obsoleta ni deja de funcionar, pero es una envoltura ligera que no ha evolucionado mientras que lo que envuelve sí lo ha hecho.
La consecuencia práctica: si ya lo utilizas y te funciona, no hay urgencia. Para el código nuevo, utilizar un agente proxy directamente supone una línea más y elimina una capa obsoleta de tu árbol de dependencias; y, dado que la extensión es precisamente una envoltura de ese agente, no pierdes nada más que la sintaxis.
Comprueba que realmente ha funcionado
Las cuatro líneas más importantes.
const res = await request
.get("https://api.ipify.org?format=json")
.agent(agent);
console.log(res.body);
Ejecútalo con el agente y sin él. Si la dirección no cambia, el proxy no está en la ruta. SuperAgent no muestra ningún error, ninguna advertencia ni ninguna solicitud fallida cuando esto ocurre: el tráfico simplemente pasa directamente.
Tres razones por las que una configuración suele no surtir efecto:
Has llamado a .agent() sin ningún argumento, creando una copia de SuperAgent que persiste en la cookie en lugar de configurar un agente HTTP. Se trata de una trampa terminológica que produce exactamente este síntoma.
Has aplicado el agente a la solicitud equivocada. El encadenamiento de SuperAgent se realiza por solicitud, por lo que un agente establecido en una llamada no se aplica a la siguiente. Para garantizar un comportamiento coherente, envuelve la creación de la solicitud en una función.
El tipo de agente no coincide con el destino. Un «HttpsProxyAgent» gestiona destinos HTTPS; un destino HTTP simple puede necesitar la variante HTTP. «proxy-agent» evita este problema eligiendo por ti.
En el caso de los proxies con segmentación geográfica, la comprobación de la dirección no es suficiente. Verifica por el resultado: solicita algo que varíe realmente según la región y confirma que la respuesta ha cambiado. Si un servicio de búsqueda indica el país correcto mientras que tu API devuelve los datos de tu región de origen, significa que la segmentación no está llegando donde debe, lo cual es el patrón de «fallo silencioso» que describimos en por qué es importante probar los proxies.
Tiempos de espera a través de un proxy
El modelo de tiempos de espera de SuperAgent es excepcionalmente bueno, y merece la pena utilizarlo correctamente cuando un proxy añade latencia.
La documentación describe dos ajustes. req.timeout({deadline: ms})
— o req.timeout(ms)
— «establece un plazo para que se complete toda la solicitud (incluidas todas las subidas, redireccionamientos y el tiempo de procesamiento del servidor). Si la respuesta no se descarga por completo en ese tiempo, la solicitud se abortará». Y req.timeout({response: ms})
«establece el tiempo máximo de espera para que llegue el primer byte desde el servidor, pero no limita la duración total de la descarga».
El propio consejo de la documentación sobre el dimensionamiento es directamente relevante para las solicitudes mediante proxy: «El tiempo de espera de la respuesta debería ser al menos unos segundos más largo que el tiempo que tarda el servidor en responder, ya que también incluye el tiempo necesario para realizar la consulta DNS, las conexiones TCP/IP y TLS, y el tiempo para enviar los datos de la solicitud».
A través de un proxy, cada una de esas fases conlleva un mayor coste. Una salida residencial añade una latencia real por solicitud, y eso se debe a la distancia, no a un fallo.
const res = await request
.get("https://api.example.com/items")
.agent(agent)
.timeout({ response: 15000, deadline: 60000 });
La documentación recomienda utilizar ambos, y la razón es la misma que en cualquier otro lugar: un tiempo de espera de respuesta detecta un servidor que nunca responde, mientras que un plazo límite detecta uno que responde y luego se ralentiza. Ninguna de las dos opciones por sí sola cubre ambos casos.
Establece los valores basándote en mediciones realizadas a través del proxy, en lugar de por costumbre, o provocarás fallos que parecerán deberse a un proxy defectuoso, cuando en realidad se trata simplemente de una latencia que no habías tenido en cuenta.
Gestión de errores
El comportamiento predeterminado de SuperAgent difiere del de la mayoría de los clientes, y esto es importante en este contexto.
La documentación es muy clara: «SuperAgent considera que las respuestas 4xx y 5xx (así como las respuestas 3xx no gestionadas) son errores por defecto». Añade que «esta información de estado estará disponible a través de err.status», y que dichos errores «también contienen un campo err.response».
Por lo tanto, un fallo en la autenticación del proxy se presenta como un rechazo en lugar de como una respuesta:
try {
const res = await request.get(url).agent(agent).timeout({ deadline: 30000 });
return res.body;
} catch (err) {
if (err.status === 407) throw new Error("Proxy rejected credentials");
if (err.status === 401) throw new Error("Target requires authentication");
if (!err.status) throw new Error(`Network error: ${err.code} ${err.message}`);
throw err;
}
Merece la pena tener en cuenta la distinción entre 407 y 401. Un 407 significa que el proxy te ha bloqueado y que nunca se ha llegado al destino; un 401 significa que el proxy ha funcionado y que el destino solicita credenciales. Son situaciones distintas, con soluciones diferentes, y es muy fácil confundirlas cuando ambas aparecen como errores lanzados.
Un error sin err.status significa que no llegó ninguna respuesta HTTP, lo que apunta a la conexión y no a la autenticación de nadie. ECONNREFUSED significa que no hay nada a la escucha en la dirección del proxy; ETIMEDOUT significa que los paquetes están desapareciendo.
Para tratar algunos códigos de error como éxitos —interpretando un 404 como datos en lugar de como un fallo—, utiliza .ok():
.ok(res => res.status < 500)
Reintentos: con precaución
SuperAgent cuenta con una función de reintento integrada, con una restricción documentada que conviene respetar.
const res = await request.get(url).agent(agent).retry(2);
La documentación explica que .retry() «reintentará automáticamente las solicitudes si fallan de forma transitoria o debido a una conexión a Internet inestable», aceptando un número opcional de reintentos (por defecto, 1) y una función de devolución de llamada que se invoca «antes de cada reintento». La llamada de retorno «puede devolver true/false para controlar si la solicitud debe reintentarse (aunque siempre se aplica el número máximo de reintentos)».
Y la restricción, expresada claramente en la documentación: utiliza .retry() «solo con solicitudes que sean idempotentes».
A través de un proxy, esto es más importante de lo habitual, por una razón concreta. Un tiempo de espera agotado no es prueba de fallo: la solicitud puede haber llegado a su destino y haberse completado con éxito, mientras que la respuesta se perdió en el camino de vuelta. Hay un salto adicional en el que eso puede ocurrir. Reintentar un POST en esa situación puede duplicar una escritura, y ninguna configuración de reintentos lo hace seguro. Cuando la operación sea importante, utiliza una clave de idempotencia si la API ofrece una.
La llamada de retorno también es el lugar adecuado para evitar reintentar un fallo de autenticación, ya que un 407 con credenciales incorrectas producirá un 407 en cada intento:
.retry(3, (err, res) => {
if (res?.status === 407 || res?.status === 401) return false;
return true;
})
Un envoltorio que merece la pena escribir
El encadenamiento de SuperAgent se realiza por solicitud, lo que significa que es fácil olvidarse de la configuración del proxy en la única llamada que importa. Envolver la creación de la solicitud resuelve ese problema y te ofrece un lugar donde colocar el resto de los valores por defecto.
import request from "superagent";
import { ProxyAgent } from "proxy-agent";
const agent = process.env.PROXY_URL ? new ProxyAgent(process.env.PROXY_URL) : undefined;
const UA = "AcmeBot/1.0 (+https://acme.example.com/bot)";
function req(method, url) {
const r = request[method](url)
.set("User-Agent", UA)
.timeout({ response: 15000, deadline: 60000 })
.retry(2, (err, res) => {
if (res?.status === 407 || res?.status === 401) return false;
if (res?.status === 429) return false; // honour the rate limit instead
return true;
});
return agent ? r.agent(agent) : r;
}
export const get = url => req("get", url);
export const post = url => req("post", url);
export async function verifyExit() {
const res = await get("https://api.ipify.org?format=json");
console.log(`Exit address: ${res.body.ip}`);
return res.body.ip;
}
Las cinco opciones que hay ahí son deliberadas.
El proxy es opcional y proviene del entorno. Sin PROXY_URL, el agente es undefined y las solicitudes van directamente, lo que hace que el desarrollo local y la producción se comporten de forma predecible sin ramificaciones en el código de tu aplicación. No aparece ninguna credencial en el código fuente.
ProxyAgent sin ningún argumento de constructor se interpretaría como http_proxy y similares si prefirieras una configuración basada en el entorno; pasar la URL explícitamente deja clara la fuente de información, lo que suele ser más valioso.
El agente de usuario es honesto e incluye una URL de contacto. No cuesta nada y cambia lo que ocurre cuando el administrador de un sitio web se fija en ti.
Los reintentos excluyen los estados en los que volver a intentarlo no tiene sentido o resulta descortés. Un 407 por credenciales incorrectas seguirá siendo un 407 en todo momento; un 429 es una indicación de que hay que reducir la velocidad, y volver a intentarlo convierte un límite temporal en uno más prolongado.
El archivo «verifyExit()» se exporta y se ejecuta al iniciar el sistema. Seis líneas que convierten un proxy que se omite en silencio en una entrada en los registros —lo cual es el único tema recurrente del funcionamiento de los proxies en todos los clientes, y lo único que ninguna biblioteca hará por ti—.
«.connect()» no es un proxy
Merece la pena señalarlo porque parece uno, pero no lo es.
SuperAgent ofrece un método «.connect()» que, según la documentación, permite «ignorar la resolución de DNS y dirigir todas las solicitudes a una dirección IP específica». Admite una asignación, que incluye una opción alternativa «*»:
const res = await request.get("http://redir.example.com:555")
.connect({
"redir.example.com": "127.0.0.1",
"www.example.com": false,
"mapped.example.com": { host: "127.0.0.1", port: 8080 },
"*": "proxy.example.com",
});
La documentación señala que «las solicitudes mantendrán su encabezado Host con el valor original», y que .connect(undefined) desactiva esta función.
Se trata de una redirección de host, no de un proxy. Cambia la dirección a la que se establece la conexión, pero deja la solicitud sin modificar: no hay ningún túnel CONNECT, ni protocolo de proxy, ni autenticación de proxy. Existe con fines de prueba, y la documentación la incluye en la sección «Pruebas en localhost» por una buena razón.
La línea "*": "proxy.example.com" del ejemplo oficial es la fuente de la confusión. Utiliza .connect() para dirigir las solicitudes a un servidor de pruebas local; utiliza un agente para un proxy real.
Preguntas frecuentes
¿Cómo se utiliza un proxy con SuperAgent?
Pasa un agente proxy a .agent(): crea un HttpsProxyAgent o un SocksProxyAgent con la URL de tu proxy y pásalo a la solicitud. Como alternativa, utiliza la extensión superagent-proxy, que añade el método .proxy(uri), aunque ese paquete no se ha publicado desde 2021.
¿Cuál es la diferencia entre .agent() con y sin argumentos?
Sin argumentos, crea una copia de SuperAgent con persistencia de cookies, con su propio archivo JAR y opciones predeterminadas. Con un argumento, establece la propiedad http.Agent de Node para esa solicitud, que es como se aplica la compatibilidad con el proxy. El nombre compartido genera una gran confusión.
¿Se sigue manteniendo superagent-proxy?
No está obsoleto, pero la versión 3.0.0 data de septiembre de 2021, mientras que su dependencia subyacente proxy-agent ha seguido publicando versiones, la más reciente en junio de 2026. Para código nuevo, utilizar un agente proxy directamente evita un envoltorio obsoleto a cambio de una línea adicional.
¿Por qué no funciona mi proxy de SuperAgent?
Lo más habitual es que se haya llamado a .agent() sin ningún argumento, lo que crea un «cookie jar» en lugar de configurar un proxy. Comprueba también que el agente se haya aplicado a la solicitud correcta, ya que el encadenamiento de SuperAgent se realiza por llamada. Verifícalo solicitando un servicio que muestre tu dirección: un proxy no utilizado no produce ningún error.
¿Respeta SuperAgent las variables de entorno HTTP_PROXY?
Por sí solo, no. El paquete proxy-agent lee http_proxy, https_proxy y no_proxy, por lo que crear un ProxyAgent() sin argumentos y pasárselo a .agent() te proporciona un comportamiento basado en el entorno.
¿Cómo configuro los tiempos de espera para las solicitudes proxy?
Utiliza ambos ajustes: .timeout({ response: 15000, deadline: 60000 }). El tiempo de espera de la respuesta limita la espera del primer byte, mientras que el plazo límite limita toda la solicitud. Determina sus valores a partir de mediciones realizadas a través del proxy, ya que una salida residencial añade latencia real tanto a las fases de DNS como a las de conexión y TLS.
¿Cómo distingo un error de proxy de un error del destino?
Por el código de estado. SuperAgent considera los códigos 4xx y 5xx como errores, así que captura y lee err.status: un 407 significa que el proxy te ha rechazado y nunca se ha llegado al destino, mientras que un 401 significa que el proxy ha funcionado y el destino solicita credenciales. Si no aparece ningún «err.status», significa que no se ha recibido ninguna respuesta HTTP.
¿Puedo utilizar .connect() como proxy?
No. Redirige las solicitudes a una IP específica manteniendo el encabezado «Host» original, lo cual es una asignación de host para pruebas, no un proxy. No hay túnel, ni protocolo de proxy, ni autenticación. Utiliza un agente para un proxy real.
Conclusión
SuperAgent no dispone de una opción de proxy propia, por lo que hay que elegir entre un agente o una extensión; el agente es la mejor opción por defecto, ya que, en ambos casos, son los paquetes mantenidos los que realizan el trabajo propiamente dicho.
La terminología es la principal trampa. .agent() sin ningún argumento te proporciona un «cookie jar»; .agent(something) configura un agente HTTP. La gente configura lo primero, ve que las solicitudes se realizan con éxito y concluye que el proxy funciona. Nadie les corrige, porque un proxy que se elude falla de forma silenciosa por definición.
Por eso merece la pena adquirir el hábito de verificar. Solicita un servicio que informe de tu dirección, con y sin el agente, y confirma que la respuesta cambia. Para trabajos con segmentación geográfica, ve más allá y confirma que el contenido específico de cada región realmente difiere: la dirección es la parte fácil y la menos informativa.
A continuación, configura ambos tiempos de espera, utiliza la ramificación en err.status para que un 407 y un 401 den lugar a mensajes diferentes, y mantén .retry() alejado de todo lo que no sea idempotente. A través de un proxy hay un salto adicional en el que una solicitud exitosa puede perder su respuesta, y un reintento en esa situación es una duplicación más que una recuperación.