使用 outboundByHost 将选定主机名的浏览器请求发送到另一个 Worker。Outbound Worker 可以访问私有服务、添加身份验证,或在响应到达浏览器之前对其进行转换。
当浏览器应请求没有公共 DNS 记录的主机名时,此功能很有用。Browser Run 会匹配主机名,并将请求发送到 Worker Fetcher,而无需公共 DNS 解析。
声明 Browser Run 绑定以及 Outbound Worker 的服务绑定:
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "browser-runner",
"main": "src/index.ts",
"upload_source_maps": true,
// Set this to today's date
"compatibility_date": "2026-11-14",
"compatibility_flags": [
"nodejs_compat"
],
"browser": {
"binding": "BROWSER"
},
"services": [
{
"binding": "OUTBOUND",
"service": "outbound-worker"
}
]
}name = "browser-runner"
main = "src/index.ts"
upload_source_maps = true
# Set this to today's date
compatibility_date = "2026-11-14"
compatibility_flags = ["nodejs_compat"]
[browser]
binding = "BROWSER"
[[services]]
binding = "OUTBOUND"
service = "outbound-worker"服务绑定为浏览器 Worker 提供 Outbound Worker 的 Fetcher。Outbound Worker 不需要公共路由。
将服务绑定通过 outboundByHost 映射传递给 launch() 或 acquire()。映射的键必须与浏览器请求中的主机名完全匹配。
以下示例使用原始 CDP 命令。它打开 Browser Run 会话,将 private.example.test 通过 Outbound Worker 路由,并发送 Page.navigate 命令:
let nextCdpCommandId = 0;
function sendCdpCommand(socket, method, params = {}) {
return new Promise((resolve, reject) => {
const id = ++nextCdpCommandId;
const timeout = setTimeout(() => {
cleanup();
reject(new Error(`CDP command "${method}" timed out`));
}, 30_000);
const cleanup = () => {
clearTimeout(timeout);
socket.removeEventListener("message", onMessage);
socket.removeEventListener("close", onClose);
};
const onClose = () => {
cleanup();
reject(new Error("CDP connection closed before the command completed"));
};
const onMessage = (event) => {
const message = JSON.parse(event.data);
if (message.id !== id) return;
cleanup();
if (message.error) {
reject(new Error(message.error.message));
} else {
resolve(message.result);
}
};
socket.addEventListener("message", onMessage);
socket.addEventListener("close", onClose);
try {
socket.send(JSON.stringify({ id, method, params }));
} catch (error) {
cleanup();
reject(error);
}
});
}
export default {
async fetch(_request, env) {
const connection = await env.BROWSER.launch({
outboundByHost: {
"private.example.test": env.OUTBOUND,
},
});
const response = await connection.webSocket.fetch(
"https://browser-binding.invalid",
{ headers: { Upgrade: "websocket" } },
);
if (!response.webSocket) {
throw new Error("Browser Run did not return a WebSocket");
}
const socket = response.webSocket;
socket.accept();
try {
await sendCdpCommand(socket, "Page.navigate", {
url: "http://private.example.test",
});
return new Response("Navigation sent through the outbound Worker");
} finally {
socket.close();
await env.BROWSER.closeSession(connection.sessionId);
}
},
};interface Env {
BROWSER: Fetcher;
OUTBOUND: Fetcher;
}
type CdpResponse = {
id: number;
result?: unknown;
error?: { message: string };
};
let nextCdpCommandId = 0;
function sendCdpCommand(
socket: WebSocket,
method: string,
params: Record<string, unknown> = {},
): Promise<unknown> {
return new Promise((resolve, reject) => {
const id = ++nextCdpCommandId;
const timeout = setTimeout(() => {
cleanup();
reject(new Error(`CDP command "${method}" timed out`));
}, 30_000);
const cleanup = () => {
clearTimeout(timeout);
socket.removeEventListener("message", onMessage);
socket.removeEventListener("close", onClose);
};
const onClose = () => {
cleanup();
reject(new Error("CDP connection closed before the command completed"));
};
const onMessage = (event: MessageEvent<string>) => {
const message = JSON.parse(event.data) as CdpResponse;
if (message.id !== id) return;
cleanup();
if (message.error) {
reject(new Error(message.error.message));
} else {
resolve(message.result);
}
};
socket.addEventListener("message", onMessage);
socket.addEventListener("close", onClose);
try {
socket.send(JSON.stringify({ id, method, params }));
} catch (error) {
cleanup();
reject(error);
}
});
}
export default {
async fetch(_request: Request, env: Env): Promise<Response> {
const connection = await env.BROWSER.launch({
outboundByHost: {
"private.example.test": env.OUTBOUND,
},
});
const response = await connection.webSocket.fetch(
"https://browser-binding.invalid",
{ headers: { Upgrade: "websocket" } },
);
if (!response.webSocket) {
throw new Error("Browser Run did not return a WebSocket");
}
const socket = response.webSocket;
socket.accept();
try {
await sendCdpCommand(socket, "Page.navigate", {
url: "http://private.example.test",
});
return new Response("Navigation sent through the outbound Worker");
} finally {
socket.close();
await env.BROWSER.closeSession(connection.sessionId);
}
},
};.test 顶级域保留用于测试。对虚拟出站路由使用不会在公共 DNS 上解析的主机名。主机名仍必须与 outboundByHost 中的键匹配。
Outbound Worker 路由仅支持 HTTP 请求。对路由的主机名使用 http:// URL。HTTPS 请求不会使用 Outbound Worker。
- 仅将
outboundByHost与 Browser Run 绑定方法一起使用。REST 端点和旧版仅 HTTP 绑定不支持它。 - 在同一次 Worker 调用中创建 Fetcher 并调用
acquire()或launch()。 - Fetcher 不会随浏览器会话持久化,也无法被后续调用复用。
- 出站路由适用于映射中的主机名。对其他主机名的请求使用浏览器的正常网络路径。
- 查看浏览器绑定 API。
- 了解如何使用 Chrome DevTools Protocol。