viewpoint_core/browser/connector/
mod.rs

1//! Browser connection via CDP endpoints.
2//!
3//! This module provides the `ConnectOverCdpBuilder` for connecting to browsers
4//! via HTTP or WebSocket endpoints.
5
6use std::collections::HashMap;
7use std::sync::Arc;
8use std::time::Duration;
9
10use tracing::{info, instrument};
11use viewpoint_cdp::{CdpConnection, CdpConnectionOptions};
12
13use super::Browser;
14use crate::error::BrowserError;
15
16/// Default connection timeout.
17const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
18
19/// Builder for connecting to a browser via CDP.
20///
21/// This builder supports connecting via:
22/// - HTTP endpoint URL (e.g., `http://localhost:9222`) - auto-discovers WebSocket URL
23/// - WebSocket URL (e.g., `ws://localhost:9222/devtools/browser/...`) - direct connection
24///
25/// # Example
26///
27/// ```no_run
28/// use viewpoint_core::Browser;
29/// use std::time::Duration;
30///
31/// # async fn example() -> Result<(), viewpoint_core::CoreError> {
32/// // Connect via HTTP endpoint (auto-discovers WebSocket URL)
33/// let browser = Browser::connect_over_cdp("http://localhost:9222")
34///     .timeout(Duration::from_secs(10))
35///     .connect()
36///     .await?;
37///
38/// // Connect with custom headers
39/// let browser = Browser::connect_over_cdp("http://remote-host:9222")
40///     .header("Authorization", "Bearer token")
41///     .connect()
42///     .await?;
43/// # Ok(())
44/// # }
45/// ```
46#[derive(Debug, Clone)]
47pub struct ConnectOverCdpBuilder {
48    /// The endpoint URL (HTTP or WebSocket).
49    endpoint_url: String,
50    /// Connection timeout.
51    timeout: Option<Duration>,
52    /// Custom headers for the connection.
53    headers: HashMap<String, String>,
54}
55
56impl ConnectOverCdpBuilder {
57    /// Create a new connection builder.
58    pub(crate) fn new(endpoint_url: impl Into<String>) -> Self {
59        Self {
60            endpoint_url: endpoint_url.into(),
61            timeout: None,
62            headers: HashMap::new(),
63        }
64    }
65
66    /// Set the connection timeout.
67    ///
68    /// Default is 30 seconds.
69    #[must_use]
70    pub fn timeout(mut self, timeout: Duration) -> Self {
71        self.timeout = Some(timeout);
72        self
73    }
74
75    /// Add a custom header for the WebSocket connection.
76    ///
77    /// Headers are sent during the WebSocket upgrade request.
78    #[must_use]
79    pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
80        self.headers.insert(name.into(), value.into());
81        self
82    }
83
84    /// Add multiple custom headers for the WebSocket connection.
85    #[must_use]
86    pub fn headers(mut self, headers: HashMap<String, String>) -> Self {
87        self.headers.extend(headers);
88        self
89    }
90
91    /// Connect to the browser.
92    ///
93    /// If the endpoint URL is an HTTP URL, this will first discover the WebSocket
94    /// URL by fetching `/json/version`. Then it connects to the browser via WebSocket.
95    ///
96    /// # Errors
97    ///
98    /// Returns an error if:
99    /// - The endpoint URL is invalid
100    /// - The HTTP endpoint doesn't expose CDP
101    /// - The WebSocket connection fails
102    /// - The connection times out
103    #[instrument(level = "info", skip(self), fields(endpoint_url = %self.endpoint_url))]
104    pub async fn connect(self) -> Result<Browser, BrowserError> {
105        info!("Connecting to browser via CDP endpoint");
106
107        // Build connection options
108        let options = CdpConnectionOptions::new()
109            .timeout(self.timeout.unwrap_or(DEFAULT_TIMEOUT))
110            .headers(self.headers);
111
112        // Connect using the CDP layer's HTTP discovery
113        let connection = CdpConnection::connect_via_http_with_options(&self.endpoint_url, options)
114            .await
115            .map_err(|e| match e {
116                viewpoint_cdp::CdpError::ConnectionTimeout(d) => BrowserError::ConnectionTimeout(d),
117                viewpoint_cdp::CdpError::InvalidEndpointUrl(s) => {
118                    BrowserError::InvalidEndpointUrl(s)
119                }
120                viewpoint_cdp::CdpError::EndpointDiscoveryFailed { url, reason } => {
121                    BrowserError::EndpointDiscoveryFailed(format!("{url}: {reason}"))
122                }
123                viewpoint_cdp::CdpError::ConnectionFailed(s) => BrowserError::ConnectionFailed(s),
124                other => BrowserError::Cdp(other),
125            })?;
126
127        info!("Successfully connected to browser");
128
129        Ok(Browser {
130            connection: Arc::new(connection),
131            process: None,
132            owned: false,
133        })
134    }
135}