Cómo usar un proxy en Node.js: Axios y node-fetch

Publicado:

13 min de lectura

Acar Diveroli
Autor: Acar Diveroli
Un cubo de Node conectado a tarjetas de axios y fetch, que llevan a un nodo proxy

En Python, configurar un proxy es un solo parámetro proxies en la mayoría de las librerías. En Node.js es más enrevesado: el fetch integrado, Axios y node-fetch reciben el proxy por tres mecanismos distintos, SOCKS5 necesita un paquete aparte y, cuando das a una librería la opción equivocada, a menudo ni siquiera recibes un error; la petición sale en silencio sin proxy. Por eso importa saber qué librería lee qué opción.

En este artículo vemos, con código que funciona, las tres formas básicas de configurar un proxy en Node.js (variable de entorno, agent y dispatcher), el uso de un proxy con autenticación con el fetch integrado, Axios y node-fetch, los proxies SOCKS5 y la rotación de IP con reintentos. Ejecutamos los ejemplos en Node.js 24 contra un proxy de prueba local que exige usuario y contraseña, y anotamos la incompatibilidad de versiones que encontramos y cómo se ven de forma distinta los errores de autenticación según la librería.

Formas de configurar un proxy en Node.js

Node.js tiene tres mecanismos para enviar una petición HTTP a través de un proxy. La librería que uses decide qué mecanismo se aplica.

  1. Variable de entorno. Las variables HTTP_PROXY, HTTPS_PROXY y NO_PROXY. Enrutan un script entero por el proxy sin cambiar el código, pero no todas las librerías las leen.
  2. Agent. Los módulos clásicos http y https de Node.js abren conexiones a través de un objeto Agent. Axios y node-fetch usan estos módulos, así que cuando les das un agent que se conecta a un proxy (como HttpsProxyAgent), las peticiones pasan por el proxy.
  3. Dispatcher. El fetch integrado de Node.js no usa el módulo clásico http; usa un cliente llamado undici. En undici, el objeto que gestiona las conexiones se llama dispatcher, y para un proxy le das un ProxyAgent.

Esta distinción es el punto más importante del artículo: el fetch integrado no reconoce la opción agent. Si por costumbre de Axios escribes fetch(url, { agent }), no recibes ningún error y la petición sale sin proxy.

Explicamos cómo funciona un proxy en general y el túnel CONNECT que se establece para peticiones HTTPS en Qué es un servidor proxy y cómo funciona. Traducir comandos cURL a fetch y Axios (cabeceras, cuerpo, datos de formulario) es el tema de nuestro artículo cURL en JavaScript; este artículo se centra solo en la parte del proxy.

¿Cómo se usa un proxy con el fetch integrado?

ProxyAgent de undici

Instala el paquete undici:

bash
npm install undici

Después importa fetch y ProxyAgent del mismo paquete:

javascript
import { fetch, ProxyAgent } from "undici";

const dispatcher = new ProxyAgent("http://user:pass@pr.proxynet.io:8000");

const res = await fetch("https://httpbin.org/ip", {
  dispatcher,
  signal: AbortSignal.timeout(20_000),
});
console.log(res.status, await res.json());

Aunque la dirección del proxy empiece por http://, puedes llegar sin problema a sitios HTTPS: ProxyAgent abre un túnel CONNECT con el proxy y la conexión TLS con el destino se hace dentro de ese túnel.

La trampa de las versiones. Node.js incluye su propia copia de undici; la undici que instalas desde npm puede ser más nueva. Cuando pasamos el ProxyAgent de npm al fetch integrado de Node.js (usando el fetch global sin importarlo), la petición falló con un error fetch failed en Node.js 24.11 con undici 8.10; la causa era invalid onRequestStart method. Usar el mismo agent con el fetch propio de undici funcionó sin problemas. La regla es sencilla: toma fetch del mismo paquete del que tomaste ProxyAgent.

Para que todas las llamadas fetch de undici de la aplicación usen el mismo proxy, puedes definir un dispatcher global:

javascript
import { fetch, ProxyAgent, setGlobalDispatcher } from "undici";

setGlobalDispatcher(new ProxyAgent("http://user:pass@pr.proxynet.io:8000"));

const res = await fetch("https://httpbin.org/ip"); // no hace falta pasar dispatcher

La variable de entorno NODE_USE_ENV_PROXY

En las versiones actuales de Node.js, el fetch integrado lee las variables de entorno estándar de proxy cuando NODE_USE_ENV_PROXY está activada. Según la guía de configuración de red empresarial de Node.js, la función está disponible desde las versiones 22.21.0 y 24.5.0; el mismo comportamiento se activa con la opción de línea de comandos --use-env-proxy.

En Linux y macOS:

bash
NODE_USE_ENV_PROXY=1 HTTPS_PROXY="http://user:pass@pr.proxynet.io:8000" node app.mjs

En Windows PowerShell:

powershell
$env:NODE_USE_ENV_PROXY = "1"
$env:HTTPS_PROXY = "http://user:pass@pr.proxynet.io:8000"
node app.mjs

Este método no requiere cambios en el código; fetch("https://httpbin.org/ip") pasa directamente por el proxy. En nuestra prueba, cuando ejecutamos el mismo comando sin NODE_USE_ENV_PROXY, la petición no llegó al proxy aunque HTTPS_PROXY estaba definida. Puedes dejar las direcciones de la red interna fuera del proxy con la variable NO_PROXY.

Si quieres un dispatcher que lea las variables de entorno desde el código, la clase EnvHttpProxyAgent de undici hace lo mismo:

javascript
import { fetch, EnvHttpProxyAgent } from "undici";

const res = await fetch("https://httpbin.org/ip", { dispatcher: new EnvHttpProxyAgent() });

¿Cómo se usa un proxy con Axios?

Axios usa los módulos clásicos http y https en Node.js y ofrece dos formas de usar un proxy.

La opción proxy integrada

La opción proxy de la configuración de peticiones de Axios funciona con direcciones http:// sin cifrar:

javascript
import axios from "axios";

const { data } = await axios.get("http://httpbin.org/ip", {
  proxy: {
    protocol: "http",
    host: "pr.proxynet.io",
    port: 8000,
    auth: { username: "user", password: "pass" },
  },
  timeout: 20_000,
});
console.log(data);

La contraseña del campo auth no se codifica; puedes escribir los caracteres especiales tal cual.

https-proxy-agent para direcciones HTTPS

Cuando la dirección de destino es HTTPS, dejar que un agent establezca el túnel da resultados más fiables:

bash
npm install axios https-proxy-agent
javascript
import axios from "axios";
import { HttpsProxyAgent } from "https-proxy-agent";

const httpsAgent = new HttpsProxyAgent("http://user:pass@pr.proxynet.io:8000");

const client = axios.create({
  httpsAgent,
  proxy: false, // desactiva la lógica de proxy propia de Axios y deja el túnel al agent
  timeout: 20_000,
});

const { status, data } = await client.get("https://httpbin.org/ip");
console.log(status, data);

No te saltes la línea proxy: false. Si no se indica la opción proxy, Axios puede intentar aplicar con su propia lógica el proxy de las variables de entorno, lo que supone dos comportamientos de proxy en conflicto junto al agent. Vincular el agent una sola vez con axios.create te evita repetirlo en cada llamada.

¿Cómo se usa un proxy con node-fetch?

Antes de que llegara el fetch integrado, node-fetch era la implementación de fetch más común en Node.js, y todavía aparece a menudo en proyectos antiguos. A diferencia del fetch integrado, usa el módulo clásico http, así que el proxy se pasa con la opción agent:

bash
npm install node-fetch https-proxy-agent
javascript
import fetch from "node-fetch";
import { HttpsProxyAgent } from "https-proxy-agent";

const agent = new HttpsProxyAgent("http://user:pass@pr.proxynet.io:8000");

const res = await fetch("https://httpbin.org/ip", { agent });
console.log(res.status, await res.json());

La versión 3 de node-fetch se publica solo como módulo ES; si no puedes cargarla con import en un proyecto CommonJS que usa require, pasar al fetch integrado suele dar menos trabajo. No hay motivo para añadir node-fetch a un proyecto nuevo.

¿Cómo se usa un proxy SOCKS5?

El ProxyAgent de undici es para proxies HTTP; el fetch integrado no admite SOCKS5. Para trabajar con un proxy SOCKS5, usa el paquete socks-proxy-agent con Axios o node-fetch:

bash
npm install socks-proxy-agent
javascript
import axios from "axios";
import { SocksProxyAgent } from "socks-proxy-agent";

// socks5h: el nombre de dominio se resuelve en el lado del proxy, así que ninguna consulta DNS sale de tu red
const agent = new SocksProxyAgent("socks5h://user:pass@pr.proxynet.io:1080");

const { data } = await axios.get("https://httpbin.org/ip", {
  httpAgent: agent,
  httpsAgent: agent,
  proxy: false,
});
console.log(data);

En node-fetch, el mismo agent se pasa como fetch(url, { agent }). En los registros de nuestro proxy de prueba, las peticiones enviadas con el esquema socks5h:// llegaron al proxy como nombre de dominio, no como dirección IP. El esquema socks5:// resuelve el nombre de dominio en tu equipo; los problemas que eso causa se explican en Fugas de WebRTC y DNS. Antes de elegir entre SOCKS5 y un proxy HTTP, consulta SOCKS o HTTP.

¿Cómo recibe el proxy cada librería?

ClienteCómo pasar el proxyVariable de entornoSOCKS5Error de autenticación
Fetch integradoNODE_USE_ENV_PROXY o --use-env-proxySolo con el flagNofetch failed
fetch de undicidispatcher: new ProxyAgent(...)Con EnvHttpProxyAgentNofetch failed, causa: petición cancelada
AxiosOpción proxy o httpsAgent + proxy: falseSi no se indica opciónCon socks-proxy-agentError con código de estado 407
node-fetchagentNoCon socks-proxy-agentDepende del agent usado
http.requestagentNoCon socks-proxy-agentDepende del agent usado

La última columna importa al depurar. En nuestra prueba con una contraseña incorrecta, Axios mostró la causa con claridad con el mensaje Request failed with status code 407 y error.response.status === 407. El fetch de undici solo dio un error fetch failed e indicó como causa «Request was cancelled»; el mensaje no menciona la autenticación. Si ves fetch failed con undici, lo primero que debes comprobar es el usuario y la contraseña del proxy. Reunimos todas las causas del 407 en Autenticación de proxy: user:pass o lista blanca de IP.

Credenciales y caracteres especiales

En todos los métodos que reciben la dirección del proxy como URL (ProxyAgent de undici, https-proxy-agent, socks-proxy-agent, variables de entorno), los caracteres como @, :, / y # de la contraseña deben codificarse. Si no, la dirección se interpreta mal.

javascript
const user = process.env.PROXY_USER;
const pass = encodeURIComponent(process.env.PROXY_PASS);

const proxyUrl = `http://${user}:${pass}@pr.proxynet.io:8000`;

Leer las credenciales de variables de entorno en lugar de escribirlas en el código evita que la contraseña aparezca allí donde se comparta el código. En el campo proxy.auth de Axios, la contraseña se escribe sin codificar.

Rotación de IP y reintentos

En un trabajo real de recogida de datos, las peticiones fallan de vez en cuando: el punto de salida del proxy no llega al destino, el destino devuelve 429 o 503, o la conexión da timeout. El ejemplo siguiente elige al azar entre varios proxies, reintenta una petición fallida con backoff exponencial y limita con lotes pequeños el número de peticiones que se ejecutan a la vez:

javascript
import { fetch, ProxyAgent } from "undici";

const PROXIES = [
  "http://user:pass@pr.proxynet.io:8000",
  "http://user:pass@pr.proxynet.io:8001",
];
const agents = PROXIES.map((p) => new ProxyAgent(p));
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function getWithRetry(url, { attempts = 4, baseMs = 500 } = {}) {
  let lastError;
  for (let i = 0; i < attempts; i++) {
    const dispatcher = agents[Math.floor(Math.random() * agents.length)];
    try {
      const res = await fetch(url, { dispatcher, signal: AbortSignal.timeout(20_000) });
      if (res.status === 429 || res.status >= 500) {
        await res.body?.cancel();
        throw new Error(`HTTP ${res.status}`);
      }
      return await res.text();
    } catch (err) {
      lastError = err;
      await sleep(baseMs * 2 ** i + Math.random() * 250);
    }
  }
  throw lastError;
}

async function crawl(urls, concurrency = 5) {
  const results = [];
  for (let i = 0; i < urls.length; i += concurrency) {
    const batch = urls.slice(i, i + concurrency);
    results.push(...(await Promise.allSettled(batch.map((u) => getWithRetry(u)))));
  }
  return results;
}

const urls = ["https://httpbin.org/ip", "https://httpbin.org/status/503", "https://example.com/"];
const results = await crawl(urls, 2);
results.forEach((r, i) =>
  console.log(urls[i], r.status, r.status === "fulfilled" ? `${r.value.length} bytes` : r.reason.message),
);

Hay tres detalles del código que importan:

  • Promise.allSettled evita que una petición fallida de un lote detenga a las demás. Con Promise.all, un solo 503 haría perder los resultados de todo el lote. En el ejemplo, la dirección /status/503 quedó marcada como rejected tras cuatro intentos, mientras las demás devolvieron sus resultados.
  • res.body?.cancel() libera la conexión sin leer el cuerpo de una respuesta que vamos a reintentar. Los cuerpos sin leer pueden llenar el pool de conexiones.
  • El componente aleatorio (Math.random() * 250) evita que las peticiones que fallaron en el mismo momento se reintenten a la vez y provoquen una nueva acumulación.

En lugar de gestionar tú la lista de proxies, si usas un Proxies rotativos que da una IP de salida distinta en cada conexión a través de una sola dirección, el array PROXIES se reduce a un elemento y la rotación ocurre en el lado del proveedor. Para trabajos en los que hay que mantener la misma IP durante la sesión (páginas con sesión iniciada, flujos de varios pasos), se prefiere un Proxies de sesión fija.

Este ejemplo no lee la cabecera Retry-After de una respuesta 429. Un enfoque que respeta la cabecera y separa qué códigos de estado no deben reintentarse se explica en Códigos de estado HTTP en web scraping. Cómo afecta realmente el valor de concurrencia a la velocidad se trata en Concurrencia y paralelismo.

¿Cómo se comprueba que el proxy funciona?

Comprueba con cada configuración nueva que la petición pasa realmente por el proxy. La forma más sencilla es enviar una petición a una dirección que devuelve la IP de salida, primero sin proxy y luego con él:

javascript
import { fetch, ProxyAgent } from "undici";

const ip = async (options = {}) => (await (await fetch("https://api.ipify.org?format=json", options)).json()).ip;

console.log("sin proxy:", await ip());
console.log("con proxy:", await ip({ dispatcher: new ProxyAgent(process.env.HTTPS_PROXY) }));

Si ambas líneas muestran la misma dirección, la petición no pasa por el proxy. La causa más común es pasar agent al fetch integrado, o usar un agent en Axios sin escribir proxy: false.

Errores comunes

  • Pasar agent al fetch integrado. La opción se ignora en silencio. La clave correcta para fetch es dispatcher.
  • Pasar el ProxyAgent de undici de npm al fetch global. Cuando las versiones no coinciden recibes un error fetch failed; importa también fetch desde undici.
  • Definir HTTPS_PROXY y suponer que el fetch integrado la leerá. Sin NODE_USE_ENV_PROXY o --use-env-proxy, no la lee.
  • No escribir proxy: false con un agent en Axios. Dos mecanismos de proxy entran en conflicto.
  • No codificar los caracteres especiales de la contraseña. La dirección se interpreta mal y la autenticación falla.
  • Resolver el DNS localmente con socks5://. Usa socks5h:// para la resolución remota.
  • No poner timeout. Una salida de proxy que no responde deja esperando minutos una petición sin timeout. Usa AbortSignal.timeout en undici y timeout en Axios.
  • Iniciar todas las peticiones a la vez con Promise.all. Satura tanto tu propio pool de conexiones como el límite de velocidad del sitio de destino.

¿Qué método conviene elegir?

Tu situaciónRecomendación
Proyecto nuevo, pocas dependenciasfetch de undici + ProxyAgent
Enrutar un script existente por un proxy sin cambiar el códigoNODE_USE_ENV_PROXY=1 + HTTPS_PROXY
El proyecto ya usa AxiosAxios + https-proxy-agent + proxy: false
Proyecto antiguo con node-fetchnode-fetch + agent
Proxy SOCKS5Axios o node-fetch + socks-proxy-agent (socks5h://)
Una IP distinta en cada peticiónProxy rotativo, una sola dirección
La misma IP durante la sesiónProxy de sesión fija
Importan los mensajes de error clarosAxios (muestra el 407 con el código de estado)

Nada de esto se aplica a JavaScript que se ejecuta en el navegador: el fetch del navegador no puede cambiar el proxy desde el código; el proxy se configura en los ajustes del navegador o del sistema operativo. Para la configuración en Windows, consulta Configuración de proxy en Windows y Chrome.

Preguntas frecuentes

¿El fetch integrado de Node.js admite proxies?

Desde Node.js 22.21.0 y 24.5.0, lee las variables HTTP_PROXY y HTTPS_PROXY cuando se usa la variable de entorno NODE_USE_ENV_PROXY=1 o la opción --use-env-proxy. Para definir un proxy por petición desde el código, usa el fetch y el ProxyAgent de undici.

¿Axios lee la variable de entorno HTTPS_PROXY?

En Node.js, cuando no se indica la opción proxy, Axios intenta usar el proxy de las variables de entorno. Para un comportamiento claro y predecible, recomendamos definir el proxy con un agent y escribir proxy: false.

¿Puedo usar un proxy distinto en cada petición?

Sí. En undici puedes dar a cada llamada fetch un ProxyAgent distinto, y en Axios un httpsAgent distinto. En lugar de crear agents en cada petición, créalos una vez y reutilízalos como en el ejemplo anterior; cada agent nuevo abre su propio pool de conexiones.

¿Cómo se mantienen las cookies al usar un proxy?

El fetch integrado y undici no guardan cookies; tienes que leer la cabecera Set-Cookie y añadirla a la petición siguiente como cabecera Cookie. En Axios puedes usar un cookie jar basado en tough-cookie. Recuerda que, junto con las cookies, la dirección IP también debe mantenerse durante toda la sesión.

¿Cómo se pasa un proxy en Puppeteer o Playwright?

Las herramientas de automatización de navegador no reciben el proxy como las librerías de Node.js; lo reciben como opción al iniciar el navegador. Los agents de este artículo no afectan al navegador.

¿Elijo JavaScript o Python?

Los proxies funcionan en ambos lenguajes; en Python la mayoría de las librerías reciben el proxy con un solo parámetro, mientras que en Node.js el mecanismo cambia según la librería. La elección suele depender del lenguaje del equipo y de la estructura de las páginas de destino. Los comparamos en Web scraping: ¿JavaScript o Python?.

En resumen

En Node.js, el proxy se pasa de tres formas distintas según el cliente: un dispatcher de undici o NODE_USE_ENV_PROXY para el fetch integrado, y un agent para Axios y node-fetch. Para destinos HTTPS, usa Axios con https-proxy-agent y proxy: false, y SOCKS5 con socks-proxy-agent y el esquema socks5h://. Toma ProxyAgent y fetch del mismo paquete, codifica la contraseña, pon un timeout y ejecuta las peticiones en lotes pequeños de Promise.allSettled con lógica de reintentos. Encontrarás planes para tu trabajo de recogida de datos en nuestra página de solución de extracción de datos.