Features

Proxy Manager

The ProxyManager class provides enterprise-grade proxy pool management. It handles rotation strategies, automatic health monitoring, cooldown and recovery, URL-based filtering, and a comprehensive hook system for proxy lifecycle events.

Quick Start

import { Rezo, ProxyManager } from 'rezo';

const manager = new ProxyManager({
  rotation: 'random',
  proxies: [
    'http://proxy1.example.com:8080',
    'http://proxy2.example.com:8080',
    'socks5://proxy3.example.com:1080'
  ],
  autoDisableDeadProxies: true,
  maxFailures: 3
});

// Use with Rezo
const rezo = new Rezo({ proxyManager: manager });
await rezo.get('https://api.example.com/data');

Rotation Strategies

Random

Select a random proxy from the active pool for each request:

const manager = new ProxyManager({
  rotation: 'random',
  proxies: [
    'http://proxy1.example.com:8080',
    'http://proxy2.example.com:8080',
    'http://proxy3.example.com:8080'
  ]
});

const proxy = manager.next('https://api.example.com');

Sequential

Cycle through proxies in order. Optionally rotate after N requests per proxy:

const manager = new ProxyManager({
  rotation: 'sequential',
  requestsPerProxy: 5, // Use each proxy for 5 requests, then move to the next
  proxies: [
    'http://proxy1.example.com:8080',
    'http://proxy2.example.com:8080',
    'http://proxy3.example.com:8080'
  ]
});

With requestsPerProxy: 1 (the default), each request uses the next proxy in the list.

Per-Proxy Limit

Each proxy is used for a maximum number of total requests, then permanently removed from the pool:

const manager = new ProxyManager({
  rotation: 'per-proxy-limit',
  limit: 100, // Each proxy handles at most 100 requests
  proxies: [
    'http://proxy1.example.com:8080',
    'http://proxy2.example.com:8080'
  ]
});

Proxy Selection

next()

Returns the selected proxy for a URL, or null if the request should go direct:

const proxy = manager.next('https://api.example.com/data');
if (proxy) {
  console.log(`Using ${proxy.protocol}://${proxy.host}:${proxy.port}`);
}

select()

Returns a detailed selection result with the reason:

const result = manager.select('https://api.example.com/data');
console.log(result.proxy);  // ProxyInfo | null
console.log(result.reason); // 'selected' | 'whitelist-no-match' | 'blacklist-match' | 'no-proxies-available'

Whitelist and Blacklist Filtering

Control which URLs use the proxy pool. Whitelist and blacklist accept strings (domain matching) or RegExp patterns.

const manager = new ProxyManager({
  rotation: 'random',
  proxies: ['http://proxy.example.com:8080'],

  // Only proxy requests to these domains
  whitelist: [
    'api.example.com',        // Exact match + subdomains
    /^https://secure./     // RegExp match against full URL
  ],

  // Never proxy requests to these domains (checked after whitelist)
  blacklist: [
    'internal.example.com',
    /localhost/
  ]
});

manager.shouldProxy('https://api.example.com/data');       // true
manager.shouldProxy('https://internal.example.com/data');  // false
manager.shouldProxy('https://other.example.com/data');     // false (not in whitelist)

String patterns support subdomain matching: 'example.com' matches both example.com and api.example.com.

Health Monitoring

Automatically disable proxies that exceed a failure threshold:

const manager = new ProxyManager({
  rotation: 'random',
  proxies: ['http://proxy1.example.com:8080', 'http://proxy2.example.com:8080'],
  autoDisableDeadProxies: true,
  maxFailures: 3 // Disable after 3 consecutive failures (default)
});

Report successes and failures after each request:

const proxy = manager.next(url);

try {
  const response = await fetch(url, { agent: createProxyAgent(proxy) });
  manager.reportSuccess(proxy);
} catch (error) {
  manager.reportFailure(proxy, error, url);
}

A successful request resets the consecutive failure counter for that proxy.

Cooldown Recovery

Instead of permanently disabling failed proxies, configure a cooldown period so they automatically re-enable:

const manager = new ProxyManager({
  rotation: 'random',
  proxies: ['http://proxy1.example.com:8080', 'http://proxy2.example.com:8080'],
  autoDisableDeadProxies: true,
  maxFailures: 3,
  cooldown: {
    enabled: true,
    durationMs: 60_000 // Re-enable after 60 seconds
  }
});

// Shorthand:
const manager2 = new ProxyManager({
  rotation: 'random',
  proxies: ['http://proxy1.example.com:8080'],
  autoDisableDeadProxies: true,
  cooldownPeriod: 60_000 // Equivalent to cooldown: { enabled: true, durationMs: 60000 }
});

After the cooldown expires, the proxy is re-enabled with its failure counter reset.

waitForProxy()

When all proxies are in cooldown, waitForProxy() returns a promise that resolves when the first proxy recovers:

const proxy = manager.next(url);

if (!proxy && manager.isCoolingDown()) {
  console.log(`Next proxy available in ${manager.nextCooldownMs()}ms`);
  const recovered = await manager.waitForProxy();
  console.log('Recovered proxy:', recovered.host);
}

If no proxies are in cooldown (pool is permanently exhausted), the promise rejects.

Helper Methods

manager.hasAvailableProxies(); // true if any proxy is active
manager.isCoolingDown();       // true if any proxy is in cooldown
manager.nextCooldownMs();      // ms until next proxy recovers (0 if available, -1 if none recovering)

Proxy Lifecycle Hooks

ProxyManager provides 8 hook points for observing and controlling proxy lifecycle events. Hooks can be set via the constructor or the hooks property.

Via Constructor

const manager = new ProxyManager({
  rotation: 'random',
  proxies: ['http://proxy1.example.com:8080'],
  hooks: {
    afterProxySelect: [(ctx) => {
      console.log(`Selected proxy for ${ctx.url}: ${ctx.proxy?.host}`);
    }],
    afterProxyDisable: [(ctx) => {
      console.log(`Proxy ${ctx.proxy.host} disabled: ${ctx.reason}`);
    }]
  }
});

Via Property

manager.hooks.beforeProxySelect.push((ctx) => {
  // Return a ProxyInfo to override selection
  if (ctx.url.includes('/priority-api/')) {
    return ctx.proxies[0]; // Always use first proxy for priority APIs
  }
});

Hook Reference

beforeProxySelect

Runs before proxy selection. Return a ProxyInfo to override the normal selection logic.

manager.hooks.beforeProxySelect.push((ctx) => {
  // ctx.url - Request URL
  // ctx.proxies - Available active proxies
  // ctx.isRetry - Whether this is a retry attempt
  // ctx.retryCount - Current retry count
  return specificProxy; // or void for normal selection
});

afterProxySelect

Runs after a proxy is selected (or when going direct).

manager.hooks.afterProxySelect.push((ctx) => {
  // ctx.url - Request URL
  // ctx.proxy - Selected proxy (null if direct)
  // ctx.reason - 'selected' | 'whitelist-no-match' | 'blacklist-match' | 'no-proxies-available'
});

beforeProxyError

Runs before a failure is recorded.

manager.hooks.beforeProxyError.push((ctx) => {
  // ctx.proxy - The proxy that failed
  // ctx.error - The error
  // ctx.url - Request URL
  // ctx.failureCount - Failure count after this error
  // ctx.willBeDisabled - Whether the proxy will be disabled
});

afterProxyError

Runs after a failure is processed.

manager.hooks.afterProxyError.push((ctx) => {
  // ctx.proxy - The proxy that failed
  // ctx.error - The error
  // ctx.action - 'retry-next-proxy' | 'disabled' | 'continue'
});

beforeProxyDisable

Runs before a proxy is disabled. Return false to prevent disabling.

manager.hooks.beforeProxyDisable.push((ctx) => {
  // ctx.proxy - Proxy about to be disabled
  // ctx.reason - 'dead' | 'limit-reached' | 'manual'
  // ctx.state - Current proxy state
  if (ctx.proxy.label === 'premium') {
    return false; // Keep premium proxies active
  }
});

afterProxyDisable

Runs after a proxy is disabled.

manager.hooks.afterProxyDisable.push((ctx) => {
  // ctx.proxy - Disabled proxy
  // ctx.reason - 'dead' | 'limit-reached' | 'manual'
  // ctx.hasCooldown - Whether cooldown is enabled
  // ctx.reenableAt - Timestamp when proxy will re-enable (if cooldown)
  alertSystem.notify(`Proxy ${ctx.proxy.host} disabled: ${ctx.reason}`);
});

afterProxyRotate

Runs when the active proxy changes (sequential rotation or failure-based switch).

manager.hooks.afterProxyRotate.push((ctx) => {
  // ctx.from - Previous proxy (null if first selection)
  // ctx.to - New proxy
  // ctx.reason - 'scheduled' | 'failure' | 'limit-reached'
});

afterProxyEnable

Runs when a proxy is re-enabled (cooldown expired or manual).

manager.hooks.afterProxyEnable.push((ctx) => {
  // ctx.proxy - The re-enabled proxy
  // ctx.reason - 'cooldown-expired' | 'manual'
});

afterProxySuccess

Runs when a request succeeds through a proxy.

manager.hooks.afterProxySuccess.push((ctx) => {
  // ctx.proxy - The proxy that succeeded
  // ctx.state - Current proxy state (requestCount, successCount, etc.)
});

onNoProxiesAvailable

Runs when no proxies are available for a request. Use for alerting or triggering proxy pool refresh.

manager.hooks.onNoProxiesAvailable.push((ctx) => {
  // ctx.url - Request URL
  // ctx.error - The error being thrown
  // ctx.allProxies - All proxy states
  // ctx.activeCount, ctx.disabledCount, ctx.cooldownCount
  // ctx.disabledReasons - { dead: N, limitReached: N, manual: N }
  // ctx.timestamp
  alertSystem.critical('Proxy pool exhausted', ctx.disabledReasons);
});

Pool Management

Adding Proxies

// Single proxy
manager.add('http://new-proxy.example.com:8080');
manager.add({ protocol: 'socks5', host: '127.0.0.1', port: 1080 });

// Multiple proxies
manager.add([
  'http://proxy4.example.com:8080',
  'http://proxy5.example.com:8080'
]);

Removing Proxies

manager.remove(proxy);       // Remove by reference
manager.removeById(proxyId); // Remove by ID

Resetting

Re-enable all proxies and reset all counters:

manager.reset();

Clearing

Remove all proxies and reset counters (pool becomes empty):

manager.clear();

Manually Disabling / Enabling

manager.disableProxy(proxy, 'manual');
manager.enableProxy(proxy, 'manual');

getStatus()

Get a complete snapshot of the proxy pool:

const status = manager.getStatus();
console.log(status);
// {
//   active: [ProxyInfo, ...],
//   disabled: [ProxyInfo, ...],
//   cooldown: [ProxyInfo, ...],
//   total: 5,
//   rotation: 'random',
//   totalRequests: 142,
//   totalSuccesses: 138,
//   totalFailures: 4
// }

Individual Proxy State

const state = manager.getProxyState(proxy);
console.log(state);
// {
//   proxy: { ... },
//   requestCount: 47,
//   failureCount: 0,
//   successCount: 45,
//   totalFailures: 2,
//   isActive: true,
//   lastSuccessAt: 1712150400000,
//   lastFailureAt: 1712149200000
// }

Aggregate Properties

manager.size;           // Total proxies in pool
manager.totalRequests;  // Total requests routed
manager.totalSuccesses; // Total successes
manager.totalFailures;  // Total failures

Cleanup

Destroy the manager when finished to clean up cooldown timers:

manager.destroy();

Unlike clear(), destroy() fully tears down the instance. Use clear() if you plan to add new proxies later.