feat: add browserless provider integration to native browser implementation (#746)
* feat: add browserless provider integration to native browser implementation This PR adds support for the Browserless provider to the native browser implementation, expanding the available remote browser providers from 3 to 4. ## Changes Made - **Added `connect_browserless()` function**: Implements session creation with Browserless API using environment variables for configuration - **Updated provider routing**: Added "browserless" case to the main provider switch statement - **Added session cleanup**: Implemented proper session termination using the stop URL returned by Browserless - **Updated documentation**: Modified comments and error messages to include Browserless in the supported provider list - **Environment variable support**: Added support for configurable Browserless settings including API key, URL, browser type, TTL, and stealth mode ## Implementation Details - Uses standard Browserless session API with POST to create sessions and DELETE to terminate - Supports both chromium and chrome browser types with validation - Includes proper error handling for API failures and missing configuration - Stores the stop URL as session_id for cleanup purposes - Follows the existing provider pattern for consistency Fixes #744 * fix: URL-encode API key in browserless session request Use reqwest's .query() method instead of string-formatting the token directly into the URL, matching the Node.js implementation's use of encodeURIComponent. Prevents malformed URLs if the API key contains special characters. --------- Co-authored-by: ctate <366502+ctate@users.noreply.github.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
//! Browser provider connections for remote CDP sessions.
|
//! Browser provider connections for remote CDP sessions.
|
||||||
//!
|
//!
|
||||||
//! Supports Browserbase, Browser Use, and Kernel providers. Each provider
|
//! Supports Browserbase, Browserless, Browser Use, and Kernel providers.
|
||||||
//! returns a CDP WebSocket URL for connecting via BrowserManager.
|
//! Each provider returns a CDP WebSocket URL for connecting via BrowserManager.
|
||||||
|
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use std::env;
|
use std::env;
|
||||||
@@ -19,10 +19,11 @@ pub async fn connect_provider(
|
|||||||
) -> Result<(String, Option<ProviderSession>), String> {
|
) -> Result<(String, Option<ProviderSession>), String> {
|
||||||
match provider_name.to_lowercase().as_str() {
|
match provider_name.to_lowercase().as_str() {
|
||||||
"browserbase" => connect_browserbase().await,
|
"browserbase" => connect_browserbase().await,
|
||||||
|
"browserless" => connect_browserless().await,
|
||||||
"browser-use" | "browseruse" => connect_browser_use().await,
|
"browser-use" | "browseruse" => connect_browser_use().await,
|
||||||
"kernel" => connect_kernel().await,
|
"kernel" => connect_kernel().await,
|
||||||
_ => Err(format!(
|
_ => Err(format!(
|
||||||
"Unknown provider '{}'. Supported: browserbase, browser-use, kernel",
|
"Unknown provider '{}'. Supported: browserbase, browserless, browser-use, kernel",
|
||||||
provider_name
|
provider_name
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
@@ -60,6 +61,10 @@ pub async fn close_provider_session(session: &ProviderSession) {
|
|||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"browserless" => {
|
||||||
|
// session_id holds the stop URL for browserless
|
||||||
|
let _ = client.delete(&session.session_id).send().await;
|
||||||
|
}
|
||||||
"kernel" => {
|
"kernel" => {
|
||||||
if let Ok(api_key) = env::var("KERNEL_API_KEY") {
|
if let Ok(api_key) = env::var("KERNEL_API_KEY") {
|
||||||
let endpoint = env::var("KERNEL_ENDPOINT")
|
let endpoint = env::var("KERNEL_ENDPOINT")
|
||||||
@@ -129,6 +134,87 @@ async fn connect_browserbase() -> Result<(String, Option<ProviderSession>), Stri
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn connect_browserless() -> Result<(String, Option<ProviderSession>), String> {
|
||||||
|
let api_key = env::var("BROWSERLESS_API_KEY")
|
||||||
|
.map_err(|_| "BROWSERLESS_API_KEY environment variable is not set")?;
|
||||||
|
|
||||||
|
let api_url = env::var("BROWSERLESS_API_URL")
|
||||||
|
.unwrap_or_else(|_| "https://production-sfo.browserless.io".to_string());
|
||||||
|
let browser_type =
|
||||||
|
env::var("BROWSERLESS_BROWSER_TYPE").unwrap_or_else(|_| "chromium".to_string());
|
||||||
|
|
||||||
|
let supported = ["chromium", "chrome"];
|
||||||
|
if !supported.contains(&browser_type.as_str()) {
|
||||||
|
return Err(format!(
|
||||||
|
"BROWSERLESS_BROWSER_TYPE \"{}\" is not supported. Only {} are allowed.",
|
||||||
|
browser_type,
|
||||||
|
supported.join(", ")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let ttl: u64 = env::var("BROWSERLESS_TTL")
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.parse().ok())
|
||||||
|
.unwrap_or(300000);
|
||||||
|
let stealth = env::var("BROWSERLESS_STEALTH")
|
||||||
|
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
|
||||||
|
.unwrap_or(true);
|
||||||
|
|
||||||
|
let url = format!("{}/session", api_url.trim_end_matches('/'));
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let response = client
|
||||||
|
.post(&url)
|
||||||
|
.query(&[("token", &api_key)])
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.json(&json!({
|
||||||
|
"ttl": ttl,
|
||||||
|
"stealth": stealth,
|
||||||
|
"browser": browser_type,
|
||||||
|
}))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Browserless request failed: {}", e))?;
|
||||||
|
|
||||||
|
let status = response.status();
|
||||||
|
let body = response
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to read Browserless response: {}", e))?;
|
||||||
|
|
||||||
|
if !status.is_success() {
|
||||||
|
return Err(format!(
|
||||||
|
"Browserless API error ({}): {}",
|
||||||
|
status.as_u16(),
|
||||||
|
body
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let json: Value =
|
||||||
|
serde_json::from_str(&body).map_err(|e| format!("Invalid Browserless response: {}", e))?;
|
||||||
|
|
||||||
|
let connect_url = json
|
||||||
|
.get("connect")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(String::from)
|
||||||
|
.ok_or_else(|| "Browserless response missing 'connect' URL".to_string())?;
|
||||||
|
|
||||||
|
let stop_url = json
|
||||||
|
.get("stop")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(String::from)
|
||||||
|
.ok_or_else(|| "Browserless response missing 'stop' URL".to_string())?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
connect_url,
|
||||||
|
Some(ProviderSession {
|
||||||
|
provider: "browserless".to_string(),
|
||||||
|
// Store the stop URL as the session_id for cleanup
|
||||||
|
session_id: stop_url,
|
||||||
|
}),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
async fn connect_browser_use() -> Result<(String, Option<ProviderSession>), String> {
|
async fn connect_browser_use() -> Result<(String, Option<ProviderSession>), String> {
|
||||||
let api_key = env::var("BROWSER_USE_API_KEY")
|
let api_key = env::var("BROWSER_USE_API_KEY")
|
||||||
.map_err(|_| "BROWSER_USE_API_KEY environment variable is not set")?;
|
.map_err(|_| "BROWSER_USE_API_KEY environment variable is not set")?;
|
||||||
|
|||||||
Reference in New Issue
Block a user