El punto de partida
Usa un dispatcher para una sola llamada a fetch, de modo que el proxy sea explícito y los demás clientes de red de tu proceso conserven su propia configuración.
Elige un runtime y un dispatcher compatibles
Este ejemplo del lado del servidor está pensado para Node.js 24 y Undici 7. El agente debe proceder de una versión mayor de undici que el fetch integrado del runtime acepte: en Node 26, los agentes de undici 5 y 6 fallan con invalid onError method, y en Node 22 o 24, los agentes de undici 8 fallan con invalid onRequestStart method (cómo corregir ambos). En un proyecto de ejemplo aparte, ejecuta npm install undici@7. El fetch integrado de Node acepta un dispatcher compatible con Undici. Esto es distinto del fetch del navegador, donde el JavaScript de la página no puede seleccionar un proxy del sistema arbitrario.
Usa un gateway HTTP(S) documentado por tu proveedor en PROXY_URL, por ejemplo el https://proxy.example.invalid:8443 que a propósito no funciona. Inyecta PROXY_USERNAME y PROXY_PASSWORD a través del gestor de secretos de tu despliegue. No pongas credenciales del gateway en código de frontend.
Mantén la autenticación del proxy en el dispatcher
Guárdalo como proxy-check.mjs y ejecuta node proxy-check.mjs. El token Basic pertenece al agente del proxy, no a la cabecera Authorization del destino. La petición tiene un plazo de cancelación de 30 segundos y no sigue redirecciones automáticamente.
proxy-check.mjs · Node.js 24 + Undici 7
import { ProxyAgent } from 'undici';
function required(name) {
const value = process.env[name];
if (!value) throw new Error('Missing environment variable: ' + name);
return value;
}
const gateway = new URL(required('PROXY_URL'));
if (!['http:', 'https:'].includes(gateway.protocol) || gateway.username || gateway.password) {
throw new Error('Use a credential-free HTTP(S) proxy URL');
}
const credentials = required('PROXY_USERNAME') + ':' + required('PROXY_PASSWORD');
const dispatcher = new ProxyAgent({
uri: gateway.href,
token: 'Basic ' + Buffer.from(credentials).toString('base64'),
});
try {
const response = await fetch('https://example.com/', {
dispatcher,
signal: AbortSignal.timeout(30_000),
redirect: 'manual',
});
await response.body?.cancel();
if (!response.ok) throw new Error('Destination HTTP ' + response.status);
console.log({ status: response.status });
} finally {
await dispatcher.close();
}Pasa de un diagnóstico a un worker
Este script cancela a propósito el cuerpo de la respuesta porque el diagnóstico solo necesita un estado. Un worker que use el resultado debería consumir el cuerpo dentro de su presupuesto. Consúmelo o cancélalo siempre antes de continuar, y cierra el dispatcher cuando el worker se apague.
Para trabajos repetidos, reutiliza un dispatcher con concurrencia acotada en lugar de crear uno por petición. Mantén el plazo de cada trabajo por separado. No instales un dispatcher a nivel de proceso a menos que cambiar el enrutamiento de todos los clientes afectados sea intencional.
Cuando una petición sí falla, imprime toda la cadena err.cause antes de cambiar nada: Corregir TypeError: fetch failed de Node.js detrás de un proxy descifra cada cadena de causa, desde un HTTPS_PROXY ignorado y el desfase de versiones de undici hasta el estado del CONNECT que el error oculta.
- Un error HTTP es una respuesta; fetch no rechaza la promesa solo porque su estado sea 4xx o 5xx.
- Un CONNECT rechazado, por ejemplo con un 407 o un 403, rechaza la promesa con
TypeError: fetch failedantes de que exista ninguna Response; el motivo real está enerr.cause, uno o dos niveles más abajo. - Captura categorías de error sin datos sensibles; no registres el token Basic ni URLs completas con credenciales.
Antes de pasar a producción
- Usa las versiones mayores documentadas de Node y Undici.
- Pasa el dispatcher de forma explícita.
- Consume o cancela los cuerpos de respuesta y cierra el agente.
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.