Cấu hình proxy tĩnh dân cư (HTTP/SOCKS5) cho Python requests, Playwright và Node.js

Khi viết tool tự động hóa (crawler nhỏ, script kiểm tra hiển thị, bot quản lý tài khoản của chính mình...), sớm muộn bạn cũng phải cho traffic đi qua proxy. Phần lớn lỗi mình gặp không nằm ở proxy mà ở cách cấu hình: sai scheme, DNS vẫn phân giải ở máy local, quên encode ký tự đặc biệt trong mật khẩu, hoặc trình duyệt headless để múi giờ UTC trong khi IP ở Việt Nam.
Bài này tổng hợp cách cấu hình proxy tĩnh dân cư (HTTP và SOCKS5) cho ba môi trường hay dùng: Python requests, Playwright và Node.js (axios), kèm cách kiểm tra IP đi ra và xử lý lỗi 407.
1. Chuẩn bị: định dạng proxy và biến môi trường
Nhà cung cấp thường giao proxy dạng:
host:port:user:pass
Trong bài, mình dùng placeholder host:port:user:pass. Bạn thay bằng thông tin thật từ nhà cung cấp, ví dụ gói proxy tĩnh dân cư của ProxyViet giao IP Viettel/VNPT/FPT, chọn được HTTP hoặc SOCKS5, và IP giữ nguyên trong suốt thời gian thuê, rất tiện để test các đoạn code dưới đây.
Đừng hardcode credential vào code. Để trong .env hoặc biến môi trường:
export PROXY_RAW="host:port:user:pass"
Một hàm nhỏ để parse và build URL, có encode user/pass (mật khẩu chứa @, : hay # sẽ làm hỏng URL nếu không encode):
# proxy_util.py
import os
from urllib.parse import quote
def build_proxy_url(raw: str, scheme: str = "http") -> str:
host, port, user, password = raw.strip().split(":", 3)
return f"{scheme}://{quote(user, safe='')}:{quote(password, safe='')}@{host}:{port}"
PROXY_RAW = os.environ["PROXY_RAW"]
2. Python requests
2.1. Proxy HTTP
import requests
from proxy_util import build_proxy_url, PROXY_RAW
proxy_url = build_proxy_url(PROXY_RAW, "http")
proxies = {"http": proxy_url, "https": proxy_url}
r = requests.get("https://api.ipify.org?format=json", proxies=proxies, timeout=15)
print(r.json()) # {"ip": "..."} -> phải là IP của proxy, không phải IP máy bạn
Lưu ý: key "https" trong dict vẫn dùng URL http://.... Scheme trong URL là giao thức nói chuyện với proxy, còn key là giao thức của URL đích. Với request HTTPS, requests sẽ mở tunnel bằng lệnh CONNECT qua proxy HTTP.
Nếu chạy nhiều request, dùng Session để tái sử dụng kết nối:
s = requests.Session()
s.proxies.update(proxies)
s.headers.update({"Accept-Language": "vi-VN,vi;q=0.9,en;q=0.8"})
2.2. Proxy SOCKS5 và socks5h
Cần cài thêm extra:
pip install "requests[socks]"
proxy_url = build_proxy_url(PROXY_RAW, "socks5h")
proxies = {"http": proxy_url, "https": proxy_url}
r = requests.get("https://api.ipify.org?format=json", proxies=proxies, timeout=15)
Khác biệt giữa socks5:// và socks5h://:
socks5://: tên miền được phân giải DNS ở máy bạn, rồi mới gửi IP đích qua proxy. Resolver local có thể lộ ra ngoài.socks5h://: tên miền được gửi nguyên cho proxy phân giải. Đây là lựa chọn nên dùng nếu bạn muốn DNS cũng đi theo đường proxy.
3. Playwright
Playwright nhận proxy qua option proxy, với username/password tách khỏi server:
# pip install playwright && playwright install chromium
import os
from playwright.sync_api import sync_playwright
host, port, user, password = os.environ["PROXY_RAW"].split(":", 3)
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context(
proxy={"server": f"http://{host}:{port}", "username": user, "password": password},
locale="vi-VN",
timezone_id="Asia/Ho_Chi_Minh",
extra_http_headers={"Accept-Language": "vi-VN,vi;q=0.9"},
)
page = context.new_page()
page.goto("https://api.ipify.org?format=json")
print(page.inner_text("body"))
browser.close()
Vài điểm cần biết:
- Đặt proxy ở cấp context thay vì cấp
launchcho phép một process trình duyệt chạy nhiều context, mỗi context một proxy. Rất hợp với mô hình "mỗi tài khoản một IP". - Chromium không hỗ trợ xác thực username/password cho SOCKS5 (truyền vào sẽ bị bỏ qua mà không báo lỗi). Nếu proxy của bạn cần auth, dùng giao thức HTTP cho Playwright, còn SOCKS5 để dành cho
requestshoặc Node. - Dùng
storage_stateđể lưu cookie của từng tài khoản, lần sau mở lại context với đúng proxy cũ và đúng file state.
context.storage_state(path="states/account_01.json")
# lần sau:
context = browser.new_context(proxy=PROXY_OF_ACCOUNT_01, storage_state="states/account_01.json",
locale="vi-VN", timezone_id="Asia/Ho_Chi_Minh")
4. Node.js với axios
npm i axios https-proxy-agent socks-proxy-agent
4.1. HTTP proxy với https-proxy-agent
// ip-check.mjs
import axios from "axios";
import { HttpsProxyAgent } from "https-proxy-agent";
const [host, port, user, pass] = process.env.PROXY_RAW.split(":");
const proxyUrl = `http://${encodeURIComponent(user)}:${encodeURIComponent(pass)}@${host}:${port}`;
const agent = new HttpsProxyAgent(proxyUrl);
const res = await axios.get("https://api.ipify.org?format=json", {
httpsAgent: agent,
proxy: false, // tắt cơ chế proxy có sẵn của axios để không đè lên agent
timeout: 15000,
});
console.log(res.data);
proxy: false rất quan trọng: nếu máy có biến HTTP_PROXY/HTTPS_PROXY, axios sẽ tự dùng proxy từ env và xung đột với agent, dẫn tới lỗi khó hiểu.
4.2. SOCKS5 với socks-proxy-agent
import axios from "axios";
import { SocksProxyAgent } from "socks-proxy-agent";
const [host, port, user, pass] = process.env.PROXY_RAW.split(":");
const agent = new SocksProxyAgent(
`socks5h://${encodeURIComponent(user)}:${encodeURIComponent(pass)}@${host}:${port}`
);
const res = await axios.get("https://api.ipify.org?format=json", {
httpAgent: agent,
httpsAgent: agent,
proxy: false,
timeout: 15000,
});
console.log(res.data);
Tương tự Python, socks5h để DNS được phân giải phía proxy.
5. Kiểm tra IP đi ra (exit IP)
Đừng tin là proxy đã hoạt động chỉ vì request không báo lỗi. Hãy so sánh:
# IP thật của máy
curl -s https://api.ipify.org; echo
# Qua proxy HTTP
curl -s -x "http://user:pass@host:port" https://api.ipify.org; echo
# Qua SOCKS5, DNS phân giải phía proxy
curl -s --socks5-hostname host:port -U user:pass https://api.ipify.org; echo
# Xem thêm quốc gia, nhà mạng
curl -s -x "http://user:pass@host:port" https://ipinfo.io/json
Hai IP phải khác nhau, và kết quả ipinfo.io phải hiện quốc gia VN cùng nhà mạng tương ứng. Nếu không muốn gõ lệnh, có thể dán proxy vào công cụ check proxy miễn phí để xem trạng thái và thông tin IP trước khi đưa vào code.
Trong code, nên có bước "preflight" trước khi chạy job chính: gọi một endpoint trả về IP, so với IP mong đợi của tài khoản, lệch thì dừng.
def assert_exit_ip(session, expected_ip):
ip = session.get("https://api.ipify.org", timeout=15).text.strip()
if ip != expected_ip:
raise RuntimeError(f"Exit IP {ip} khác IP mong đợi {expected_ip}, dừng job.")
6. Xử lý lỗi 407 Proxy Authentication Required
407 nghĩa là proxy nhận được request nhưng xác thực không hợp lệ. Nguyên nhân hay gặp:
- Sai user/pass, hoặc copy dính khoảng trắng, xuống dòng (
.strip()khi đọc file). - Mật khẩu có ký tự đặc biệt nhưng chưa URL-encode.
- Dùng sai scheme (khai báo SOCKS5 cho proxy HTTP hoặc ngược lại).
- Gói đã hết hạn.
- Proxy cấu hình xác thực theo whitelist IP nhưng IP máy bạn chưa được thêm.
Với requests, 407 khi gọi URL HTTPS thường xuất hiện dưới dạng exception ProxyError (tunnel CONNECT thất bại), không phải response có status_code == 407:
from requests.exceptions import ProxyError
try:
r = s.get("https://api.ipify.org", timeout=15)
except ProxyError as e:
if "407" in str(e):
raise SystemExit("Proxy từ chối xác thực: kiểm tra user/pass, encode và hạn gói.")
raise
Một nguyên tắc: đừng retry vô hạn khi gặp 407. Đây là lỗi cấu hình, retry chỉ làm log dài thêm. Retry có backoff chỉ dành cho lỗi mạng tạm thời như timeout hay connection reset.
7. Mỗi tài khoản một IP ổn định
Với proxy tĩnh, lợi thế là IP không đổi trong suốt thời gian thuê, nên hãy tận dụng bằng một file mapping cố định thay vì chọn proxy ngẫu nhiên:
{
"account_01": { "proxy": "host1:port1:user1:pass1", "state": "states/account_01.json" },
"account_02": { "proxy": "host2:port2:user2:pass2", "state": "states/account_02.json" }
}
Quy tắc mình áp dụng:
- Một tài khoản chỉ đi qua đúng một proxy, không fallback sang mạng nhà hay proxy khác khi lỗi. Lỗi thì dừng và báo.
- Không cho nhiều tài khoản chạy chung một proxy.
- Ghi log exit IP mỗi lần chạy để dễ truy vết.
8. Đồng bộ múi giờ và ngôn ngữ
IP ở Việt Nam nhưng trình duyệt báo timezone = UTC và navigator.language = en-US là một tín hiệu bất thường. Với Playwright đã có locale="vi-VN" và timezone_id="Asia/Ho_Chi_Minh" như ở trên. Kiểm tra lại bằng:
print(page.evaluate("() => [Intl.DateTimeFormat().resolvedOptions().timeZone, navigator.language]"))
# ['Asia/Ho_Chi_Minh', 'vi-VN']
Với requests và axios, tối thiểu hãy gửi header Accept-Language: vi-VN,vi;q=0.9. Nếu dùng trình duyệt thật (không headless), nhớ kiểm tra rò rỉ WebRTC và DNS để IP thật không bị lộ.
Tổng kết
- HTTP proxy: dùng
http://user:pass@host:portcho cả keyhttpvàhttps. - SOCKS5: dùng
socks5h://để DNS đi qua proxy; Python cầnrequests[socks], Node dùngsocks-proxy-agent. - Playwright: đặt proxy ở cấp context, dùng HTTP nếu proxy cần auth, đặt
localevàtimezone_id. - Luôn kiểm tra exit IP trước khi chạy job, không retry khi gặp 407.
- Mỗi tài khoản gắn với một IP tĩnh và một file state cố định.
Hy vọng bài viết giúp bạn bớt vài giờ debug. Nếu có cách cấu hình hay hơn cho Puppeteer hay fetch/undici trong Node, comment bên dưới để mọi người cùng tham khảo nhé.
All rights reserved