跳转到内容
本站为社区维护的非官方中文翻译,与 Cloudflare, Inc. 无关联。本站不提供账户登录,请勿在任何非官方站点输入您的 Cloudflare 账户信息。官方文档:developers.cloudflare.com

Outbound Workers

最后更新 查看 MarkdownAgent 设置

使用 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 命令:

src/index.jsjs
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);
		}
	},
};
src/index.tsts
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 不会随浏览器会话持久化,也无法被后续调用复用。
  • 出站路由适用于映射中的主机名。对其他主机名的请求使用浏览器的正常网络路径。

后续步骤

这篇文档对您有帮助吗?