为什么 Deno 和 Bun 的 fetch() 默认忽略代理
如果你正在在 Deno 和 Bun 中使用代理,你可能会发现一个令人困惑的问题:设置了 HTTP_PROXY 环境变量后,fetch() 仍然直连目标服务器。这是因为 Deno 和 Bun 都实现了 WHATWG Fetch 标准,而该标准本身不包含代理配置的概念——浏览器代理由操作系统或浏览器扩展处理,JavaScript 代码无法直接控制。
Node.js 生态通过 https-proxy-agent 等第三方库绕过了这一限制。Deno 和 Bun 选择了不同的路径:它们在运行时层面提供了原生代理支持,但需要你显式配置。本文将带你从基础配置走到生产级轮换策略,涵盖住宅代理、SOCKS5、并发会话管理和重试机制。
技术背景:Deno 与 Bun 的代理支持差异
Deno 和 Bun 都基于现代 JavaScript 引擎(V8 和 JavaScriptCore),但它们的代理实现方式截然不同。理解这些差异是正确配置 Deno proxy 和 Bun fetch proxy 的前提。
| 特性 | Deno | Bun |
|---|---|---|
| 代理配置方式 | Deno.createHttpClient({ proxy }) 传入 fetch | fetch(url, { proxy }) 直接传入 |
| 环境变量支持 | 支持 HTTP_PROXY / HTTPS_PROXY | 支持 HTTP_PROXY / HTTPS_PROXY |
| SOCKS5 支持 | 通过 proxy URL scheme | 通过 proxy URL scheme |
| 自定义 CA 证书 | caCerts 选项 | tls 选项 |
| 连接复用 | HttpClient 实例复用 | 自动连接池 |
两者都支持通过 HTTP_PROXY 和 HTTPS_PROXY 环境变量配置全局代理,但在需要细粒度控制(如按请求轮换 IP)时,应优先使用 per-client 配置。
Deno 中使用 Deno.createHttpClient 配置代理
Deno 的代理配置通过 Deno.createHttpClient 创建一个可复用的 HTTP 客户端,然后将该客户端作为 fetch 的 client 选项传入。这种方式的核心优势是连接复用——同一个 HttpClient 实例可以跨多个请求共享 TCP 连接,减少握手开销约 200ms 每次请求。
基础示例:Deno HTTP 代理
const client = Deno.createHttpClient({
proxy: {
url: "http://gate.proxyhat.com:8080",
basicAuth: {
username: "user-country-US",
password: "pass",
},
},
});
const res = await fetch("https://httpbin.org/ip", { client });
const data = await res.json();
console.log("出口 IP:", data.origin);
注意 basicAuth 是独立字段,不写在 URL 里。如果你只需要无认证的代理,可以省略 basicAuth。
SOCKS5 代理(端口 1080)
const socksClient = Deno.createHttpClient({
proxy: {
url: "socks5://gate.proxyhat.com:1080",
basicAuth: {
username: "user-country-DE-session-abc123",
password: "pass",
},
},
});
const res = await fetch("https://httpbin.org/ip", { client: socksClient });
console.log(await res.json());
SOCKS5 代理在需要更低延迟或绕过 HTTP 代理限制时非常有用。ProxyHat 的 SOCKS5 端口为 1080。
Bun 中使用 fetch proxy 配置代理
Bun 的代理配置更加简洁——直接在 fetch 调用中传入 proxy 选项即可,无需创建额外的客户端对象。参考 Bun 官方文档。这种 proxy fetch JavaScript 方式语法更接近 curl 的 --proxy 参数。
基础示例:Bun fetch proxy
const res = await fetch("https://httpbin.org/ip", {
proxy: "http://user-country-US:pass@gate.proxyhat.com:8080",
});
const data = await res.json();
console.log("出口 IP:", data.origin);
Bun SOCKS5 代理
const res = await fetch("https://httpbin.org/ip", {
proxy: "socks5://user-country-JP:pass@gate.proxyhat.com:1080",
});
console.log(await res.json());
地理定位与粘性会话:在用户名中编码
ProxyHat 通过用户名字段支持地理定位和会话管理。这种设计的好处是无需额外的 API 调用或 header——所有控制逻辑都编码在认证字符串中。
地理定位
// 指定国家
const usProxy = "http://user-country-US:pass@gate.proxyhat.com:8080";
// 指定国家和城市
const berlinProxy = "http://user-country-DE-city-berlin:pass@gate.proxyhat.com:8080";
// Bun 示例
const res = await fetch("https://httpbin.org/ip", {
proxy: berlinProxy,
});
粘性会话(Sticky Session)
粘性会话确保在指定时间窗口内所有请求使用同一个出口 IP。这对于需要登录状态保持的爬虫任务至关重要——如果每次请求都换 IP,目标网站会立即检测到异常。
// 生成随机会话 ID
const sessionId = crypto.randomUUID().slice(0, 8);
// Deno 示例:同一会话复用同一 IP
const stickyClient = Deno.createHttpClient({
proxy: {
url: "http://gate.proxyhat.com:8080",
basicAuth: {
username: `user-country-US-session-${sessionId}`,
password: "pass",
},
},
});
// 多个请求共享同一出口 IP
const results = await Promise.all([
fetch("https://httpbin.org/ip", { client: stickyClient }),
fetch("https://httpbin.org/headers", { client: stickyClient }),
]);
for (const r of results) {
console.log(await r.json());
}
HTTP_PROXY 环境变量:何时使用,何时避免
两个运行时都支持通过环境变量配置全局代理:
# 设置环境变量
export HTTP_PROXY="http://user-country-US:pass@gate.proxyhat.com:8080"
export HTTPS_PROXY="http://user-country-US:pass@gate.proxyhat.com:8080"
# Deno 和 Bun 的 fetch() 会自动读取这些变量
deno run --allow-net script.ts
bun run script.ts
环境变量方式适合开发环境或单一代理场景。但在以下情况中,应优先使用 per-client 配置:
- 需要按请求轮换不同国家或城市的 IP
- 需要同时管理多个粘性会话
- 需要混合使用 HTTP 和 SOCKS5 代理
- 需要为不同请求设置不同的超时和重试策略
住宅代理轮换:并发请求实战
对于 SERP 爬取、电商价格监控等高封禁风险场景,数据中心代理往往在几十个请求后就被目标网站封禁。住宅代理使用真实 ISP 分配的 IP 地址,封禁难度显著更高。下面是一个完整的并发轮换示例,使用 Promise.all 和 AbortController 实现超时控制。
// Deno 示例:住宅代理池并发轮换
const PROXY_GATEWAY = "gate.proxyhat.com:8080";
const PROXY_USER = "your_user";
const PROXY_PASS = "your_pass";
const TARGETS = [
"https://httpbin.org/ip",
"https://httpbin.org/headers",
"https://httpbin.org/user-agent",
"https://httpbin.org/ip",
"https://httpbin.org/ip",
];
// 为每个请求生成独立的粘性会话
function createProxyClient(sessionId: string, country: string) {
return Deno.createHttpClient({
proxy: {
url: `http://${PROXY_GATEWAY}`,
basicAuth: {
username: `${PROXY_USER}-country-${country}-session-${sessionId}`,
password: PROXY_PASS,
},
},
});
}
async function fetchWithTimeout(
url: string,
client: Deno.HttpClient,
timeoutMs = 10000,
) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch(url, { client, signal: controller.signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} finally {
clearTimeout(timer);
}
}
// 并发执行,每个请求使用不同的会话 ID
const countries = ["US", "DE", "JP", "GB", "FR"];
const tasks = TARGETS.map((url, i) => {
const sessionId = crypto.randomUUID().slice(0, 8);
const country = countries[i % countries.length];
const client = createProxyClient(sessionId, country);
return fetchWithTimeout(url, client, 10000)
.then((data) => ({ url, success: true, data }))
.catch((err) => ({ url, success: false, error: err.message }));
});
const results = await Promise.all(tasks);
let successCount = 0;
for (const r of results) {
if (r.success) {
successCount++;
console.log(`✓ ${r.url} →`, JSON.stringify(r.data).slice(0, 80));
} else {
console.error(`✗ ${r.url} → ${r.error}`);
}
}
console.log(
`成功率: ${successCount}/${results.length} ` +
`(${(successCount / results.length * 100).toFixed(1)}%)`
);
这个示例展示了几个关键模式:每个请求使用独立的 sessionId 以获得不同的出口 IP;AbortController 确保单个请求不会无限挂起(超时设为 10000ms);Promise.all 实现最大并发。在实际生产中,你可能需要限制并发数以避免内存压力——可以使用信号量模式或分批处理,建议控制在 50-100 个并发会话以内。
生产环境最佳实践
指数退避重试
网络请求会因各种原因失败——代理服务器重启、目标网站临时 503、DNS 解析超时等。一个健壮的重试策略应该包含指数退避和抖动(jitter),避免所有客户端同时重试造成惊群效应。
// Deno / Bun 通用:指数退避重试封装
async function fetchWithRetry(
url: string,
options: RequestInit & { proxy?: string } = {},
maxRetries = 3,
baseDelayMs = 1000,
): Promise<Response> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const res = await fetch(url, options);
if (res.ok || attempt === maxRetries) return res;
// 仅 5xx 错误才重试
if (res.status < 500) return res;
} catch (err) {
if (attempt === maxRetries) throw err;
}
// 指数退避 + 随机抖动
const delay = baseDelayMs * Math.pow(2, attempt) +
Math.random() * 500;
await new Promise((r) => setTimeout(r, delay));
}
throw new Error("max retries exceeded");
}
// Bun 使用示例
const res = await fetchWithRetry("https://httpbin.org/ip", {
proxy: "http://user-country-US:pass@gate.proxyhat.com:8080",
}, 3, 1000);
console.log(await res.json());
自定义 CA 证书(Deno)
在需要通过 MITM 代理或使用自签名证书的企业环境中,Deno 允许你通过 caCerts 选项注入自定义 CA 证书:
const caCert = await Deno.readTextFile("./custom-ca.pem");
const client = Deno.createHttpClient({
proxy: {
url: "http://gate.proxyhat.com:8080",
basicAuth: { username: "user", password: "pass" },
},
caCerts: [caCert],
});
const res = await fetch("https://internal-api.local/data", { client });
连接复用与资源清理
Deno 中复用同一个 HttpClient 实例可以显著降低延迟——避免每次请求都重新建立 TCP 连接和 TLS 握手。对于需要同一出口 IP 的批量请求,应创建一个 client 并在所有请求中共享。对于需要轮换 IP 的场景,则为每个会话创建一个 client,使用后调用 client.close() 释放资源,避免文件描述符泄漏。
ProxyHat Node SDK:在 Deno 和 Bun 中通用
如果你已经在使用 Node.js 生态的库,ProxyHat 的 Node SDK 也可以在 Deno 和 Bun 中运行。下面是原生 fetch 与 SDK 的对比:
| 特性 | 原生 fetch + proxy | ProxyHat Node SDK |
|---|---|---|
| 代理配置 | 手动编码用户名参数 | SDK 自动处理轮换和会话 |
| 重试逻辑 | 需自行实现 | 内置重试与退避 |
| IP 轮换 | 手动管理会话 ID 池 | SDK 自动轮换 |
| 运行时兼容性 | Deno / Bun 原生 | 需 npm 兼容层 |
| 控制粒度 | 完全可控 | 封装层抽象 |
// ProxyHat Node SDK 在 Bun 中的使用示例
import { ProxyHat } from "proxyhat-sdk";
const proxy = new ProxyHat({
username: "your_user",
password: "your_pass",
gateway: "gate.proxyhat.com",
port: 8080,
type: "residential",
});
// SDK 自动管理会话和轮换
const result = await proxy.fetch("https://httpbin.org/ip", {
country: "US",
session: "my-session-001",
});
console.log(await result.json());
// 对比:Bun 原生方式
const nativeRes = await fetch("https://httpbin.org/ip", {
proxy: "http://user-country-US-session-my-session-001:pass@gate.proxyhat.com:8080",
});
console.log(await nativeRes.json());
选择 SDK 还是原生方式取决于你的需求:如果需要最大控制和最小依赖,原生方式更合适;如果希望减少样板代码并利用内置重试逻辑,SDK 是更好的选择。查看 ProxyHat 文档了解更多 SDK 细节。
伦理与合规:爬取前必读
代理技术是一把双刃剑。在使用代理进行数据采集时,必须遵守以下原则:
- 优先使用官方 API:如果目标网站提供公开 API,应优先使用 API 而非爬取。API 通常有更稳定的接口、更清晰的使用条款和更低的被封禁风险。
- 仅采集公开数据:不绕过登录墙、不访问需要认证才能查看的内容。在美国,《计算机欺诈和滥用法》(CFAA)对未授权访问计算机系统有严格规定。
- 遵守 robots.txt:robots.txt 是网站对爬虫的明确指令,应予以尊重。
- 遵守 GDPR:在欧盟,采集涉及个人身份信息的数据需遵守《通用数据保护条例》。确保你有合法依据处理个人数据。
- 控制请求频率:即使使用住宅代理,也应设置合理的请求间隔(如 200ms-1000ms),避免对目标服务器造成压力。
更多关于合规爬取的实践,请参考我们的 Web 爬取用例指南和 SERP 追踪用例。
关键要点
- Deno 通过
Deno.createHttpClient({ proxy })配置代理,Bun 通过fetch(url, { proxy })直接配置——两者都支持 HTTP 和 SOCKS5。- ProxyHat 的地理定位和会话管理编码在用户名字段中:
user-country-US-session-abc123,无需额外 API 调用。- HTTP 代理端口
8080,SOCKS5 端口1080,网关地址始终为gate.proxyhat.com。- 住宅代理在高封禁场景下成功率可达 99% 以上,远优于数据中心代理。
- 生产环境必须实现重试机制、超时控制和连接复用——指数退避 + 抖动是最佳实践。
- 伦理爬取:优先使用官方 API,仅采集公开数据,遵守 CFAA 和 GDPR。
准备好开始了吗?查看 ProxyHat 定价方案和 全球代理位置,选择适合你项目的代理类型。






