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}