Network 拦截与路由:Fetch Domain 实战

本文是「agent-browser 代码原理」系列第 5 篇,深入剖析 agent-browser 的 Network 拦截和路由功能。


一、为什么需要 Network 拦截

浏览器自动化中,Network 拦截有三大应用场景:

  1. Mock API 响应:测试前端逻辑时,不需要真实后端
  2. 修改请求头:添加认证 Token、User-Agent 等
  3. 拦截广告/追踪:屏蔽不必要的请求,加速页面加载

agent-browser 通过 CDP 的 Fetch domain 实现这些功能。


二、CDP Fetch Domain 基础

2.1 启用拦截

client.send_command(
    "Fetch.enable",
    Some(json!({
        "patterns": [
            { "urlPattern": "*" }  // 拦截所有 URL
        ]
    })),
    Some(session_id),
).await?;

2.2 请求暂停事件

当 Chrome 匹配到拦截规则时,会发送 Fetch.requestPaused 事件:

{
  "method": "Fetch.requestPaused",
  "params": {
    "requestId": "interception-job-1.0",
    "request": {
      "url": "https://api.example.com/data",
      "method": "GET",
      "headers": {...}
    },
    "resourceType": "XHR"
  }
}

2.3 三种处理方式

方式 CDP 命令 用途
继续请求 Fetch.continueRequest 放行,可修改请求头
Mock 响应 Fetch.fulfillRequest 返回自定义响应
中断请求 Fetch.failRequest 模拟网络错误

三、agent-browser 的路由系统

3.1 RouteEntry 结构

pub struct RouteEntry {
    pub url_pattern: String,      // URL 匹配模式
    pub response: Option<RouteResponse>, // Mock 响应
    pub abort: bool,              // 是否中断
    pub resource_types: Vec<String>, // 资源类型过滤
}

pub struct RouteResponse {
    pub status: Option<u16>,      // HTTP 状态码
    pub body: Option<String>,     // 响应体
    pub content_type: Option<String>, // Content-Type
    pub headers: Option<HashMap<String, String>>, // 响应头
}

3.2 用户接口

# Mock API 响应
agent-browser route "**/api/users" --mock '{"status": 200, "body": "{\"users\":[]}"}'

# 中断请求
agent-browser route "**/*.png" --abort

# 修改请求头
agent-browser open example.com --headers '{"Authorization": "Bearer token123"}'

四、拦截处理器实现

4.1 启动拦截

当用户首次使用 --headersroute 时,agent-browser 自动启用 Fetch domain:

// actions.rs:handle_navigate
let scoped_headers = cmd.get("headers")
    .and_then(|v| v.as_object())
    .filter(|m| !m.is_empty());

if let Some(headers_map) = scoped_headers {
    if let Some(origin) = url::Url::parse(url)
        .ok()
        .map(|u| u.origin().ascii_serialization()) 
    {
        let headers: HashMap<String, String> = headers_map
            .iter()
            .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
            .collect();

        let mut map = state.origin_headers.write().await;
        let first = map.is_empty();
        map.insert(origin, headers);

        // 首次使用时启用 Fetch 拦截
        if first {
            let session_id = mgr.active_session_id()?.to_string();
            let mut params = json!({ "patterns": [{ "urlPattern": "*" }] });
            mgr.client.send_command("Fetch.enable", Some(params), Some(&session_id)).await?;
        }
    }
}

4.2 事件处理循环

在 BrowserManager 启动时,会创建一个后台任务持续监听 Fetch.requestPaused 事件:

// browser.rs 伪代码
async fn fetch_handler_task(
    client: CdpClient,
    routes: Arc<RwLock<Vec<RouteEntry>>>,
    origin_headers: Arc<RwLock<HashMap<String, HashMap<String, String>>>>,
) {
    let mut events = client.subscribe();

    while let Ok(event) = events.recv().await {
        if event.method != "Fetch.requestPaused" {
            continue;
        }

        let request_id = event.params.get("requestId").and_then(|v| v.as_str())
            .unwrap_or("");
        let request = event.params.get("request").unwrap_or(&Value::Null);
        let url = request.get("url").and_then(|v| v.as_str()).unwrap_or("");
        let resource_type = event.params.get("resourceType")
            .and_then(|v| v.as_str()).unwrap_or("");

        // 检查路由规则
        let routes = routes.read().await;
        for route in routes.iter() {
            if match_url_pattern(&route.url_pattern, url) {
                if route.abort {
                    // 中断请求
                    let _ = client.send_command(
                        "Fetch.failRequest",
                        Some(json!({
                            "requestId": request_id,
                            "errorReason": "Aborted"
                        })),
                        None,
                    ).await;
                } else if let Some(ref response) = route.response {
                    // Mock 响应
                    let _ = client.send_command(
                        "Fetch.fulfillRequest",
                        Some(json!({
                            "requestId": request_id,
                            "responseCode": response.status.unwrap_or(200),
                            "body": base64_encode(response.body.as_deref().unwrap_or("")),
                        })),
                        None,
                    ).await;
                }
                return;
            }
        }

        // 检查是否需要注入 headers
        let origin_headers = origin_headers.read().await;
        if let Some(headers) = origin_headers.get(&get_origin(url)) {
            let mut merged_headers = Vec::new();
            for (key, value) in headers {
                merged_headers.push(json!({"name": key, "value": value}));
            }
            let _ = client.send_command(
                "Fetch.continueRequest",
                Some(json!({
                    "requestId": request_id,
                    "headers": merged_headers
                })),
                None,
            ).await;
            return;
        }

        // 默认放行
        let _ = client.send_command(
            "Fetch.continueRequest",
            Some(json!({"requestId": request_id})),
            None,
        ).await;
    }
}

4.3 URL 模式匹配

agent-browser 支持 glob 风格的 URL 模式:

fn match_url_pattern(pattern: &str, url: &str) -> bool {
    // ** 匹配任意路径段
    // *  匹配单个路径段
    // ?  匹配单个字符

    let regex = pattern
        .replace("**", ".*")
        .replace("*", "[^/]*")
        .replace("?", ".");

    Regex::new(&regex).map(|re| re.is_match(url)).unwrap_or(false)
}

示例:

模式 匹配的 URL
**/api/** https://example.com/api/users
*.png https://example.com/image.png
https://api.example.com/** https://api.example.com/v1/data

五、Network 事件追踪

除了拦截,agent-browser 还记录了完整的网络请求/响应信息,用于导出 HAR 文件。

5.1 HarEntry 结构

pub struct HarEntry {
    pub request_id: String,
    pub wall_time: f64,              // Unix 时间戳(秒)
    pub method: String,
    pub url: String,
    pub request_headers: Vec<(String, String)>,
    pub post_data: Option<String>,
    pub request_body_size: i64,
    pub resource_type: String,
    pub status: Option<i64>,
    pub status_text: String,
    pub http_version: String,
    pub response_headers: Vec<(String, String)>,
    pub mime_type: String,
    pub response_body_size: i64,
}

5.2 事件监听

async fn network_event_task(client: CdpClient, tracker: NetworkTracker) {
    let mut events = client.subscribe();

    while let Ok(event) = events.recv().await {
        match event.method.as_str() {
            "Network.requestWillBeSent" => {
                let request_id = event.params["requestId"].as_str().unwrap_or("");
                let request = &event.params["request"];
                tracker.record_request(HarEntry {
                    request_id: request_id.to_string(),
                    method: request["method"].as_str().unwrap_or("GET").to_string(),
                    url: request["url"].as_str().unwrap_or("").to_string(),
                    // ...
                }).await;
            }
            "Network.responseReceived" => {
                let request_id = event.params["requestId"].as_str().unwrap_or("");
                let response = &event.params["response"];
                tracker.update_response(request_id, ResponseInfo {
                    status: response["status"].as_i64(),
                    status_text: response["statusText"].as_str().unwrap_or("").to_string(),
                    // ...
                }).await;
            }
            "Network.loadingFinished" => {
                let request_id = event.params["requestId"].as_str().unwrap_or("");
                let encoded_len = event.params["encodedDataLength"].as_i64().unwrap_or(0);
                tracker.update_body_size(request_id, encoded_len).await;
            }
            _ => {}
        }
    }
}

5.3 HAR 导出

agent-browser open example.com
agent-browser har --output trace.har

六、代理认证

当使用需要认证的 HTTP 代理时,agent-browser 自动处理 Fetch.authRequired 事件:

"Fetch.authRequired" => {
    let request_id = event.params["requestId"].as_str().unwrap_or("");
    if let Some(creds) = proxy_credentials.read().await.as_ref() {
        let _ = client.send_command(
            "Fetch.continueWithAuth",
            Some(json!({
                "requestId": request_id,
                "authChallengeResponse": {
                    "response": "ProvideCredentials",
                    "username": creds.username,
                    "password": creds.password
                }
            })),
            None,
        ).await;
    }
}

七、与 Playwright 的对比

特性 agent-browser Playwright
路由语法 Glob 模式 (**/api/**) Regex + Glob
拦截时机 Fetch.requestPaused Fetch.requestPaused
修改响应头
修改请求体
HAR 导出
多浏览器 CDP/WebDriver/Appium Chromium/Firefox/WebKit

八、实际应用场景

8.1 前端测试:Mock API

# 启动带路由的浏览器
agent-browser open "https://my-app.com" \
  --route "**/api/users" --mock '{"status": 200, "body": "{\"users\":[{\"id\":1,\"name\":\"Test\"}]}"}' \
  --route "**/api/orders" --mock '{"status": 500, "body": "{\"error\":\"Server Error\"}"}'

# 测试前端对 500 错误的处理
agent-browser click @submit_order
agent-browser wait-for "Error: Server Error"

8.2 数据采集:绕过反爬

agent-browser open "https://example.com" \
  --headers '{"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}' \
  --route "**/*analytics*" --abort \
  --route "**/*tracking*" --abort

8.3 性能分析:HAR 导出

agent-browser open "https://example.com"
agent-browser har --output performance.har

# 用 har 分析工具查看加载性能

九、总结

agent-browser 的 Network 拦截系统基于 CDP Fetch domain,实现了:

功能 实现方式 价值
请求拦截 Fetch.enable + Fetch.requestPaused 精确控制网络流量
Mock 响应 Fetch.fulfillRequest 无后端测试
中断请求 Fetch.failRequest 模拟错误场景
注入 Header Fetch.continueRequest + headers 认证、UA 修改
代理认证 Fetch.continueWithAuth 企业代理穿透
HAR 记录 Network.* 事件追踪 性能分析

这套系统使得 agent-browser 不仅是浏览器自动化工具,还是一个网络流量操控平台


系列文章: 1. agent-browser 架构概览:从 CLI 到 CDP 的分层设计 2. CDP WebSocket 客户端实现:命令/响应匹配与事件广播 3. Accessibility Tree 快照原理:如何让 AI 看懂网页 4. Chrome 进程管理与多 Backend 架构 5. Network 拦截与路由:Fetch Domain 实战 ← 本文

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐