← Quay lại blog

Undici ProxyAgent trong Node.js: cấu hình proxy HTTP/SOCKS5 và xoay vòng IP không cần phụ thuộc thừa

Chúng ta sẽ xem xét cách sử dụng ProxyAgent tích hợp sẵn undici trong Node.js để làm việc với proxy HTTP và SOCKS5, tổ chức xoay vòng IP và không cần đến https-proxy-agent và socks-proxy-agent.

📅18 tháng 10, 2026

Hầu hết các hướng dẫn về proxy trong Node.js khuyên bạn nên thêm vào dự án https-proxy-agent, socks-proxy-agent và một vài gói khác. Trên thực tế, từ phiên bản Node.js 18+, HTTP client tích hợp undici đã bao gồm ProxyAgent, giải quyết hầu hết các nhiệm vụ phân tích và kiểm tra API qua proxy. Trong bài viết này, chúng ta sẽ tìm hiểu cách cấu hình đường hầm HTTP/HTTPS qua undici, tổ chức xoay vòng IP giữa các yêu cầu và cách làm nếu bạn cần SOCKS5 mà không muốn thêm phụ thuộc.

Tại sao chọn undici thay vì axios và request

undici là một client HTTP/1.1 và HTTP/2, được viết bởi đội ngũ Node.js và từ phiên bản 18 trở đi, nó là nền tảng cho fetch toàn cầu. Nó không có các phụ thuộc trung gian, nhanh hơn axios trong hầu hết các bài kiểm tra về kết nối pooling, và điều quan trọng cho chủ đề của chúng ta là nó có một mô-đun gốc để làm việc với proxy thông qua đường hầm HTTP CONNECT.

Khi bạn phân tích Wildberries, Ozon hoặc kiểm tra các quảng cáo từ các khu vực khác nhau, mỗi gói thêm vào trong node_modules là một điểm có thể gây ra sự cố khi cập nhật Node.js. undici đi kèm với runtime, vì vậy bạn có được triển khai TLS, HTTP/2 và keep-alive mới nhất mà không có rủi ro rằng một thư viện đã bị bỏ rơi sẽ bị hỏng trong phiên bản chính mới.

Việc cài đặt cực kỳ đơn giản:

npm install undici

Nếu bạn có Node.js 20+, một phần API có sẵn toàn cầu mà không cần nhập khẩu, nhưng đối với ProxyAgent, bạn vẫn cần nhập khẩu vì nó không được xuất ra trong fetch toàn cầu theo mặc định.

Cấu hình cơ bản cho ProxyAgent

Ví dụ làm việc tối thiểu cho một yêu cầu qua proxy trông như sau:

import { ProxyAgent, fetch } from 'undici';

const proxyAgent = new ProxyAgent('http://proxy.example.com:8080');

const response = await fetch('https://httpbin.org/ip', {
  dispatcher: proxyAgent,
});

const data = await response.json();
console.log(data);

Điểm chính là tham số dispatcher. Đây không phải là tùy chọn agent, như trong mô-đun cũ http, mà là dispatcher của undici, nó chặn kết nối trước khi bắt tay TLS và thiết lập một đường hầm qua phương thức CONNECT đến máy chủ mục tiêu. Đối với các yêu cầu HTTPS qua proxy, điều này là rất quan trọng: proxy HTTP thông thường không nhìn thấy nội dung của lưu lượng mã hóa, nó chỉ chuyển tiếp byte bên trong đường hầm TCP.

Nếu bạn không làm việc với fetch mà sử dụng undici.request ở mức thấp hơn, cú pháp cũng tương tự:

import { request, ProxyAgent } from 'undici';

const proxyAgent = new ProxyAgent('http://proxy.example.com:8080');

const { statusCode, body } = await request('https://api.example.com/data', {
  dispatcher: proxyAgent,
});

console.log(statusCode);
for await (const chunk of body) {
  console.log(chunk.toString());
}

Đối với các trình phân tích sản xuất, thực hiện hàng trăm yêu cầu mỗi phút, điều quan trọng là không tạo một ProxyAgent mới cho mỗi yêu cầu — điều này sẽ giết chết connection pooling. Tạo một instance của agent một lần cho pool kết nối và tái sử dụng nó giữa các lần gọi.

Xác thực proxy bằng tên đăng nhập và mật khẩu

Hầu hết các nhà cung cấp proxy trả phí cung cấp quyền truy cập bằng cặp tên đăng nhập/mật khẩu, thay vì bằng IP-whitelisting. Trong undici, thông tin xác thực được truyền trực tiếp trong URL của proxy:

import { ProxyAgent, fetch } from 'undici';

const login = 'user123';
const password = 'pass456';
const proxyUrl = `http://${login}:${password}@gate.proxycove.com:9000`;

const proxyAgent = new ProxyAgent(proxyUrl);

const response = await fetch('https://httpbin.org/ip', {
  dispatcher: proxyAgent,
});

console.log(await response.json());

Lưu ý: nếu tên đăng nhập hoặc mật khẩu chứa ký tự đặc biệt (@, :, /), chúng cần được mã hóa qua encodeURIComponent, nếu không, parser URL bên trong undici sẽ không phân tích đúng chuỗi kết nối và bạn sẽ nhận được lỗi xác thực 407 thay vì thông báo dễ hiểu.

Đối với các nhiệm vụ mà tính ổn định của phiên là quan trọng — chẳng hạn như xác thực trên thị trường với gắn bó với một IP trong thời gian phiên — tốt hơn là sử dụng proxy cư trú với phiên sticky: IP không thay đổi trong một khoảng thời gian nhất định, có nghĩa là dịch vụ không thấy "nhảy" giữa các khu vực và không yêu cầu captcha lại trên mỗi yêu cầu.

Xoay vòng proxy giữa các yêu cầu

Nếu bạn có danh sách N proxy và nhiệm vụ là phân phối các yêu cầu giữa chúng, phương án đơn giản nhất là round-robin mà không cần thư viện bên ngoài cho hàng đợi:

import { ProxyAgent, fetch } from 'undici';

const proxyList = [
  'http://user:[email protected]:8000',
  'http://user:[email protected]:8000',
  'http://user:[email protected]:8000',
];

let currentIndex = 0;

function getNextAgent() {
  const proxyUrl = proxyList[currentIndex];
  currentIndex = (currentIndex + 1) % proxyList.length;
  return new ProxyAgent(proxyUrl);
}

async function fetchWithRotation(url) {
  const agent = getNextAgent();
  return fetch(url, { dispatcher: agent });
}

for (let i = 0; i < 10; i++) {
  const res = await fetchWithRotation('https://httpbin.org/ip');
  console.log(await res.json());
}

Cách tiếp cận này hoạt động tốt cho việc phân tích thẻ sản phẩm hoặc giám sát giá, khi mỗi yêu cầu tiếp theo về lý thuyết độc lập với yêu cầu trước đó. Đối với các nhiệm vụ cần tốc độ cao trong việc thay đổi IP cho mỗi yêu cầu mà không cần quản lý danh sách thủ công, tốt hơn là sử dụng proxy với xoay vòng tự động từ phía nhà cung cấp — khi đó toàn bộ mã ở trên chỉ cần một URL tĩnh cho gateway, và việc xoay vòng được thực hiện bởi proxy trung tâm dữ liệu hoặc pool cư trú với xoay vòng theo thời gian.

Nếu bạn cần không chỉ xoay vòng tròn mà còn xoay vòng có trọng số — chẳng hạn, ưu tiên các proxy nhanh hơn — hãy thêm trọng số vào cấu trúc dữ liệu:

const weightedProxies = [
  { url: 'http://user:[email protected]:8000', weight: 5 },
  { url: 'http://user:[email protected]:8000', weight: 1 },
];

function pickWeighted(list) {
  const total = list.reduce((sum, p) => sum + p.weight, 0);
  let rnd = Math.random() * total;
  for (const p of list) {
    if (rnd < p.weight) return p.url;
    rnd -= p.weight;
  }
  return list[0].url;
}

Kiểm tra tính khả dụng của proxy và loại trừ các IP "chết"

Pool proxy theo thời gian sẽ "hỏng": một số IP bị cấm bởi trang web mục tiêu, một số không còn phản hồi. Để xoay vòng không gửi yêu cầu qua các địa chỉ chết, hãy thêm kiểm tra sức khỏe với thời gian chờ thông qua AbortController tích hợp:

import { ProxyAgent, fetch } from 'undici';

async function isProxyAlive(proxyUrl, timeoutMs = 5000) {
  const agent = new ProxyAgent(proxyUrl);
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const res = await fetch('https://httpbin.org/ip', {
      dispatcher: agent,
      signal: controller.signal,
    });
    return res.status === 200;
  } catch {
    return false;
  } finally {
    clearTimeout(timer);
  }
}

async function filterAliveProxies(proxyList) {
  const checks = await Promise.all(
    proxyList.map(async (url) => ({ url, alive: await isProxyAlive(url) }))
  );
  return checks.filter((c) => c.alive).map((c) => c.url);
}

Hãy chạy kiểm tra như vậy mỗi vài phút bằng một bộ đếm thời gian nền và cập nhật pool hoạt động, chứ không phải toàn bộ danh sách tĩnh. Điều này đặc biệt quan trọng nếu bạn giữ các proxy của riêng mình trên các IP trung tâm dữ liệu rẻ tiền — tỷ lệ hỏng ở đó cao hơn so với các pool của nhà cung cấp được quản lý.

SOCKS5 mà không cần thư viện bên ngoài

Một lưu ý quan trọng: ProxyAgent tích hợp trong undici hoạt động dựa trên đường hầm HTTP CONNECT và "ngay từ hộp" chỉ hiểu các proxy HTTP/HTTPS. Giao thức SOCKS5 hoạt động khác — đó là một bắt tay nhị phân ở cấp độ TCP, và undici không phân tích nó một cách tự nhiên. Nếu bạn cần SOCKS5 mà không cài đặt socks-proxy-agent, bạn có thể triển khai một connector tối thiểu bằng tay thông qua mô-đun net và tùy chọn connect trong một Agent tùy chỉnh:

import net from 'node:net';
import { Agent, fetch } from 'undici';

function socksHandshake(socket, targetHost, targetPort) {
  return new Promise((resolve, reject) => {
    // Bước 1: chào hỏi, phương thức không xác thực
    socket.write(Buffer.from([0x05, 0x01, 0x00]));

    socket.once('data', (greeting) => {
      if (greeting[1] !== 0x00) {
        return reject(new Error('SOCKS5: phương thức xác thực không được hỗ trợ'));
      }

      const hostBuf = Buffer.from(targetHost, 'utf8');
      const request = Buffer.concat([
        Buffer.from([0x05, 0x01, 0x00, 0x03, hostBuf.length]),
        hostBuf,
        Buffer.from([targetPort >> 8, targetPort & 0xff]),
      ]);

      socket.write(request);

      socket.once('data', (reply) => {
        if (reply[1] !== 0x00) {
          return reject(new Error('SOCKS5: kết nối bị từ chối'));
        }
        resolve(socket);
      });
    });
  });
}

const socksAgent = new Agent({
  connect: (options, callback) => {
    const socket = net.connect(1080, 'socks-proxy.example.com', async () => {
      try {
        await socksHandshake(socket, options.hostname, 443);
        callback(null, socket);
      } catch (err) {
        callback(err, null);
      }
    });
    socket.on('error', (err) => callback(err, null));
  },
});

const response = await fetch('https://httpbin.org/ip', { dispatcher: socksAgent });
console.log(await response.json());

Đây là một bắt tay đơn giản mà không có xác thực bằng tên đăng nhập và mật khẩu (phương thức 0x02) và không hỗ trợ IPv6 — cho sản xuất, bạn nên viết thêm nhánh với xác thực và xử lý các loại địa chỉ khác nhau. Nhưng để hiểu cơ chế — điều này là đủ: SOCKS5 không yêu cầu TLS bên trong đường hầm trong giai đoạn bắt tay, khác với HTTP CONNECT, vì vậy mã ngắn hơn so với vẻ bề ngoài.

Trong thực tế, SOCKS5 thường không được sử dụng để phân tích các trang web, mà để chuyển tiếp lưu lượng của các ứng dụng di động và trình duyệt chống phát hiện như Dolphin Anty hoặc AdsPower, nơi giao thức cần thiết để hoạt động ở cấp độ socket, không phải các yêu cầu HTTP. Nếu nhiệm vụ của bạn là lưu lượng HTTP/HTTPS trong Node.js, ProxyAgent tích hợp thường đủ, và không cần phải loay hoay với connector SOCKS5 thủ công.

Xử lý thời gian chờ và logic thử lại

Proxy thường ngắt kết nối nhiều hơn so với kết nối trực tiếp — điều này là bình thường đối với các mạng residential, nơi nút có thể offline bất cứ lúc nào. Cần một lớp thử lại với độ trễ theo cấp số nhân:

import { ProxyAgent, fetch } from 'undici';

async function fetchWithRetry(url, proxyList, maxRetries = 3) {
  let lastError;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const proxyUrl = proxyList[attempt % proxyList.length];
    const agent = new ProxyAgent(proxyUrl);
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), 8000);

    try {
      const res = await fetch(url, { dispatcher: agent, signal: controller.signal });
      clearTimeout(timer);
      if (res.status >= 500) throw new Error(`Lỗi máy chủ: ${res.status}`);
      return res;
    } catch (err) {
      clearTimeout(timer);
      lastError = err;
      const delay = 500 * 2 ** attempt;
      await new Promise((r) => setTimeout(r, delay));
    }
  }

  throw new Error(`Tất cả các nỗ lực đã hết: ${lastError?.message}`);
}

Lớp bọc này cũng giải quyết vấn đề xoay vòng khi có lỗi: nếu proxy hiện tại bị ngắt, nỗ lực tiếp theo sẽ tự động chuyển sang một IP khác trong danh sách, thay vì tiếp tục cố gắng với nút chết đó.

So sánh undici ProxyAgent, https-proxy-agent và socks-proxy-agent

Công cụ Phụ thuộc Hỗ trợ SOCKS5 HTTP/2 Tốc độ
undici ProxyAgent không (tích hợp trong Node 18+) không mặc định có cao
https-proxy-agent 1-2 gói không không trung bình
socks-proxy-agent 3-4 gói có không trung bình

Đối với việc phân tích HTTP/HTTPS thuần túy với số lượng phụ thuộc tối thiểu, undici ProxyAgent thắng lợi nhờ tính tích hợp và tương thích với HTTP/2. Đối với SOCKS5 trong sản xuất, dễ dàng hơn khi sử dụng socks-proxy-agent đã được kiểm chứng, hơn là duy trì một bắt tay tự viết — việc triển khai thủ công trong phần trên tốt cho việc học tập và các trường hợp edge hiếm hoi, nhưng không cho sản xuất nặng với hàng ngàn kết nối mỗi giờ.

Các trường hợp thực tiễn: phân tích và kiểm tra địa lý

Theo dõi giá trên các thị trường. Một script trên undici với xoay vòng proxy và logic thử lại từ các phần trên hoàn hảo cho việc vượt qua rate-limit trên Wildberries và Ozon: mỗi yêu cầu tiếp theo về thẻ sản phẩm sẽ đi từ một IP mới, và kiểm tra sức khỏe sẽ tự động loại trừ các địa chỉ bị cấm khỏi pool.

Kiểm tra quảng cáo theo địa lý. Nếu cần kiểm tra cách nội dung được gửi đến người dùng từ một quốc gia cụ thể cho Google Ads hoặc Facebook Ads, chỉ cần thay đổi URL proxy trong ProxyAgent thành nút của khu vực cần thiết — không cần phải khởi động trình duyệt nếu việc kiểm tra diễn ra qua API hoặc phản hồi HTML từ máy chủ.

Kiểm tra khả dụng của nội dung ở các quốc gia khác nhau. Đối với các nhiệm vụ mà trang web cung cấp nội dung khác nhau tùy thuộc vào IP, các kết nối cư trú trông giống như người dùng thông thường đối với máy chủ mục tiêu, điều này giảm thiểu rủi ro bị chặn so với các dải IP trung tâm dữ liệu. Kịch bản này đặc biệt quan trọng đối với proxy di động, nếu dịch vụ mục tiêu kiểm tra loại mạng và chặn các subnet trung tâm dữ liệu rõ ràng.

Kiểm tra hàng loạt các API endpoints. Trong kiểm tra tải API của riêng bạn thông qua một pool proxy bên ngoài, điều quan trọng là theo dõi connection pooling — undici mặc định tái sử dụng các kết nối TCP bên trong một ProxyAgent, điều này tiết kiệm thời gian đáng kể cho các bắt tay TLS khi thực hiện hàng trăm yêu cầu liên tiếp đến một máy chủ.

Kết luận

undici ProxyAgent tích hợp giải quyết hầu hết các nhiệm vụ với proxy HTTP/HTTPS trong Node.js mà không cần phải thêm các gói bên ngoài: xác thực qua URL, đường hầm qua CONNECT, tương thích với HTTP/2 và connection pooling hoạt động ngay lập tức. Việc xoay vòng và kiểm tra sức khỏe proxy dễ dàng được xây dựng trên các promise và bộ đếm thời gian sạch, và SOCKS5 nếu cần có thể được triển khai thủ công thông qua một connector connect tùy chỉnh, mặc dù cho sản xuất, hợp lý hơn là sử dụng thư viện đã có sẵn dành riêng cho SOCKS.

Nếu script của bạn trên undici dành cho việc phân tích các thị trường hoặc theo dõi giá, hãy chú ý đến proxy cư trú — chúng ổn định hơn trước các hệ thống chống bot so với các IP trung tâm dữ liệu cổ điển, và ít khi yêu cầu giải quyết captcha bổ sung khi thực hiện các yêu cầu hàng loạt đến Wildberries, Ozon hoặc Avito.