use crate::error::{Error, Result};
use crate::protocol::browser_context::Viewport;
use crate::protocol::{Dialog, Download, Request, ResponseObject, Route, WebSocket};
use crate::server::channel::Channel;
use crate::server::channel_owner::{ChannelOwner, ChannelOwnerImpl, ParentOrConnection};
use crate::server::connection::{ConnectionExt, downcast_parent};
use base64::Engine;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::any::Any;
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, RwLock};
#[derive(Clone)]
pub struct Page {
base: ChannelOwnerImpl,
url: Arc<RwLock<String>>,
main_frame_guid: Arc<str>,
cached_main_frame: Arc<Mutex<Option<crate::protocol::Frame>>>,
route_handlers: Arc<Mutex<Vec<RouteHandlerEntry>>>,
download_handlers: Arc<Mutex<Vec<DownloadHandler>>>,
dialog_handlers: Arc<Mutex<Vec<DialogHandler>>>,
request_handlers: Arc<Mutex<Vec<RequestHandler>>>,
request_finished_handlers: Arc<Mutex<Vec<RequestHandler>>>,
request_failed_handlers: Arc<Mutex<Vec<RequestHandler>>>,
response_handlers: Arc<Mutex<Vec<ResponseHandler>>>,
websocket_handlers: Arc<Mutex<Vec<WebSocketHandler>>>,
viewport: Arc<RwLock<Option<Viewport>>>,
is_closed: Arc<AtomicBool>,
default_timeout_ms: Arc<AtomicU64>,
default_navigation_timeout_ms: Arc<AtomicU64>,
binding_callbacks: Arc<Mutex<HashMap<String, PageBindingCallback>>>,
}
type RouteHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
type DownloadHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
type DialogHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
type RequestHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
type ResponseHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
type WebSocketHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
#[derive(Clone)]
struct RouteHandlerEntry {
pattern: String,
handler: Arc<dyn Fn(Route) -> RouteHandlerFuture + Send + Sync>,
}
type DownloadHandler = Arc<dyn Fn(Download) -> DownloadHandlerFuture + Send + Sync>;
type DialogHandler = Arc<dyn Fn(Dialog) -> DialogHandlerFuture + Send + Sync>;
type RequestHandler = Arc<dyn Fn(Request) -> RequestHandlerFuture + Send + Sync>;
type ResponseHandler = Arc<dyn Fn(ResponseObject) -> ResponseHandlerFuture + Send + Sync>;
type WebSocketHandler = Arc<dyn Fn(WebSocket) -> WebSocketHandlerFuture + Send + Sync>;
type PageBindingCallbackFuture = Pin<Box<dyn Future<Output = serde_json::Value> + Send>>;
type PageBindingCallback =
Arc<dyn Fn(Vec<serde_json::Value>) -> PageBindingCallbackFuture + Send + Sync>;
impl Page {
pub fn new(
parent: Arc<dyn ChannelOwner>,
type_name: String,
guid: Arc<str>,
initializer: Value,
) -> Result<Self> {
let main_frame_guid: Arc<str> =
Arc::from(initializer["mainFrame"]["guid"].as_str().ok_or_else(|| {
crate::error::Error::ProtocolError(
"Page initializer missing 'mainFrame.guid' field".to_string(),
)
})?);
let base = ChannelOwnerImpl::new(
ParentOrConnection::Parent(parent),
type_name,
guid,
initializer,
);
let url = Arc::new(RwLock::new("about:blank".to_string()));
let route_handlers = Arc::new(Mutex::new(Vec::new()));
let download_handlers = Arc::new(Mutex::new(Vec::new()));
let dialog_handlers = Arc::new(Mutex::new(Vec::new()));
let websocket_handlers = Arc::new(Mutex::new(Vec::new()));
let cached_main_frame = Arc::new(Mutex::new(None));
let initial_viewport: Option<Viewport> =
base.initializer().get("viewportSize").and_then(|v| {
if v.is_null() {
None
} else {
serde_json::from_value(v.clone()).ok()
}
});
let viewport = Arc::new(RwLock::new(initial_viewport));
Ok(Self {
base,
url,
main_frame_guid,
cached_main_frame,
route_handlers,
download_handlers,
dialog_handlers,
request_handlers: Default::default(),
request_finished_handlers: Default::default(),
request_failed_handlers: Default::default(),
response_handlers: Default::default(),
websocket_handlers,
viewport,
is_closed: Arc::new(AtomicBool::new(false)),
default_timeout_ms: Arc::new(AtomicU64::new(crate::DEFAULT_TIMEOUT_MS.to_bits())),
default_navigation_timeout_ms: Arc::new(AtomicU64::new(
crate::DEFAULT_TIMEOUT_MS.to_bits(),
)),
binding_callbacks: Arc::new(Mutex::new(HashMap::new())),
})
}
fn channel(&self) -> &Channel {
self.base.channel()
}
pub async fn main_frame(&self) -> Result<crate::protocol::Frame> {
let frame: crate::protocol::Frame = self
.connection()
.get_typed::<crate::protocol::Frame>(&self.main_frame_guid)
.await?;
frame.set_page(self.clone());
if let Ok(mut cached) = self.cached_main_frame.lock() {
*cached = Some(frame.clone());
}
Ok(frame)
}
pub fn url(&self) -> String {
if let Ok(cached) = self.cached_main_frame.lock()
&& let Some(frame) = cached.as_ref()
{
return frame.url();
}
self.url.read().unwrap().clone()
}
pub async fn close(&self) -> Result<()> {
let result = self
.channel()
.send_no_result("close", serde_json::json!({}))
.await;
self.is_closed.store(true, Ordering::Relaxed);
result
}
pub fn is_closed(&self) -> bool {
self.is_closed.load(Ordering::Relaxed)
}
pub async fn set_default_timeout(&self, timeout: f64) {
self.default_timeout_ms
.store(timeout.to_bits(), Ordering::Relaxed);
set_timeout_and_notify(self.channel(), "setDefaultTimeoutNoReply", timeout).await;
}
pub async fn set_default_navigation_timeout(&self, timeout: f64) {
self.default_navigation_timeout_ms
.store(timeout.to_bits(), Ordering::Relaxed);
set_timeout_and_notify(
self.channel(),
"setDefaultNavigationTimeoutNoReply",
timeout,
)
.await;
}
pub fn default_timeout_ms(&self) -> f64 {
f64::from_bits(self.default_timeout_ms.load(Ordering::Relaxed))
}
pub fn default_navigation_timeout_ms(&self) -> f64 {
f64::from_bits(self.default_navigation_timeout_ms.load(Ordering::Relaxed))
}
fn with_navigation_timeout(&self, options: Option<GotoOptions>) -> GotoOptions {
let nav_timeout = self.default_navigation_timeout_ms();
match options {
Some(opts) if opts.timeout.is_some() => opts,
Some(mut opts) => {
opts.timeout = Some(std::time::Duration::from_millis(nav_timeout as u64));
opts
}
None => GotoOptions {
timeout: Some(std::time::Duration::from_millis(nav_timeout as u64)),
wait_until: None,
},
}
}
pub async fn frames(&self) -> Result<Vec<crate::protocol::Frame>> {
let main = self.main_frame().await?;
Ok(vec![main])
}
pub async fn goto(&self, url: &str, options: Option<GotoOptions>) -> Result<Option<Response>> {
let options = self.with_navigation_timeout(options);
let frame = self.main_frame().await.map_err(|e| match e {
Error::TargetClosed { context, .. } => Error::TargetClosed {
target_type: "Page".to_string(),
context,
},
other => other,
})?;
let response = frame.goto(url, Some(options)).await.map_err(|e| match e {
Error::TargetClosed { context, .. } => Error::TargetClosed {
target_type: "Page".to_string(),
context,
},
other => other,
})?;
if let Some(ref resp) = response
&& let Ok(mut page_url) = self.url.write()
{
*page_url = resp.url().to_string();
}
Ok(response)
}
pub fn context(&self) -> Result<crate::protocol::BrowserContext> {
downcast_parent::<crate::protocol::BrowserContext>(self)
.ok_or_else(|| Error::ProtocolError("Page parent is not a BrowserContext".to_string()))
}
pub async fn pause(&self) -> Result<()> {
self.context()?.pause().await
}
pub async fn title(&self) -> Result<String> {
let frame = self.main_frame().await?;
frame.title().await
}
pub async fn content(&self) -> Result<String> {
let frame = self.main_frame().await?;
frame.content().await
}
pub async fn set_content(&self, html: &str, options: Option<GotoOptions>) -> Result<()> {
let frame = self.main_frame().await?;
frame.set_content(html, options).await
}
pub async fn wait_for_load_state(&self, state: Option<WaitUntil>) -> Result<()> {
let frame = self.main_frame().await?;
frame.wait_for_load_state(state).await
}
pub async fn wait_for_url(&self, url: &str, options: Option<GotoOptions>) -> Result<()> {
let frame = self.main_frame().await?;
frame.wait_for_url(url, options).await
}
pub async fn locator(&self, selector: &str) -> crate::protocol::Locator {
let frame = self.main_frame().await.expect("Main frame should exist");
crate::protocol::Locator::new(Arc::new(frame), selector.to_string(), self.clone())
}
pub async fn frame_locator(&self, selector: &str) -> crate::protocol::FrameLocator {
let frame = self.main_frame().await.expect("Main frame should exist");
crate::protocol::FrameLocator::new(Arc::new(frame), selector.to_string(), self.clone())
}
pub async fn get_by_text(&self, text: &str, exact: bool) -> crate::protocol::Locator {
self.locator(&crate::protocol::locator::get_by_text_selector(text, exact))
.await
}
pub async fn get_by_label(&self, text: &str, exact: bool) -> crate::protocol::Locator {
self.locator(&crate::protocol::locator::get_by_label_selector(
text, exact,
))
.await
}
pub async fn get_by_placeholder(&self, text: &str, exact: bool) -> crate::protocol::Locator {
self.locator(&crate::protocol::locator::get_by_placeholder_selector(
text, exact,
))
.await
}
pub async fn get_by_alt_text(&self, text: &str, exact: bool) -> crate::protocol::Locator {
self.locator(&crate::protocol::locator::get_by_alt_text_selector(
text, exact,
))
.await
}
pub async fn get_by_title(&self, text: &str, exact: bool) -> crate::protocol::Locator {
self.locator(&crate::protocol::locator::get_by_title_selector(
text, exact,
))
.await
}
pub async fn get_by_test_id(&self, test_id: &str) -> crate::protocol::Locator {
self.locator(&crate::protocol::locator::get_by_test_id_selector(test_id))
.await
}
pub async fn get_by_role(
&self,
role: crate::protocol::locator::AriaRole,
options: Option<crate::protocol::locator::GetByRoleOptions>,
) -> crate::protocol::Locator {
self.locator(&crate::protocol::locator::get_by_role_selector(
role, options,
))
.await
}
pub fn keyboard(&self) -> crate::protocol::Keyboard {
crate::protocol::Keyboard::new(self.clone())
}
pub fn mouse(&self) -> crate::protocol::Mouse {
crate::protocol::Mouse::new(self.clone())
}
pub(crate) async fn keyboard_down(&self, key: &str) -> Result<()> {
self.channel()
.send_no_result(
"keyboardDown",
serde_json::json!({
"key": key
}),
)
.await
}
pub(crate) async fn keyboard_up(&self, key: &str) -> Result<()> {
self.channel()
.send_no_result(
"keyboardUp",
serde_json::json!({
"key": key
}),
)
.await
}
pub(crate) async fn keyboard_press(
&self,
key: &str,
options: Option<crate::protocol::KeyboardOptions>,
) -> Result<()> {
let mut params = serde_json::json!({
"key": key
});
if let Some(opts) = options {
let opts_json = opts.to_json();
if let Some(obj) = params.as_object_mut()
&& let Some(opts_obj) = opts_json.as_object()
{
obj.extend(opts_obj.clone());
}
}
self.channel().send_no_result("keyboardPress", params).await
}
pub(crate) async fn keyboard_type(
&self,
text: &str,
options: Option<crate::protocol::KeyboardOptions>,
) -> Result<()> {
let mut params = serde_json::json!({
"text": text
});
if let Some(opts) = options {
let opts_json = opts.to_json();
if let Some(obj) = params.as_object_mut()
&& let Some(opts_obj) = opts_json.as_object()
{
obj.extend(opts_obj.clone());
}
}
self.channel().send_no_result("keyboardType", params).await
}
pub(crate) async fn keyboard_insert_text(&self, text: &str) -> Result<()> {
self.channel()
.send_no_result(
"keyboardInsertText",
serde_json::json!({
"text": text
}),
)
.await
}
pub(crate) async fn mouse_move(
&self,
x: i32,
y: i32,
options: Option<crate::protocol::MouseOptions>,
) -> Result<()> {
let mut params = serde_json::json!({
"x": x,
"y": y
});
if let Some(opts) = options {
let opts_json = opts.to_json();
if let Some(obj) = params.as_object_mut()
&& let Some(opts_obj) = opts_json.as_object()
{
obj.extend(opts_obj.clone());
}
}
self.channel().send_no_result("mouseMove", params).await
}
pub(crate) async fn mouse_click(
&self,
x: i32,
y: i32,
options: Option<crate::protocol::MouseOptions>,
) -> Result<()> {
let mut params = serde_json::json!({
"x": x,
"y": y
});
if let Some(opts) = options {
let opts_json = opts.to_json();
if let Some(obj) = params.as_object_mut()
&& let Some(opts_obj) = opts_json.as_object()
{
obj.extend(opts_obj.clone());
}
}
self.channel().send_no_result("mouseClick", params).await
}
pub(crate) async fn mouse_dblclick(
&self,
x: i32,
y: i32,
options: Option<crate::protocol::MouseOptions>,
) -> Result<()> {
let mut params = serde_json::json!({
"x": x,
"y": y,
"clickCount": 2
});
if let Some(opts) = options {
let opts_json = opts.to_json();
if let Some(obj) = params.as_object_mut()
&& let Some(opts_obj) = opts_json.as_object()
{
obj.extend(opts_obj.clone());
}
}
self.channel().send_no_result("mouseClick", params).await
}
pub(crate) async fn mouse_down(
&self,
options: Option<crate::protocol::MouseOptions>,
) -> Result<()> {
let mut params = serde_json::json!({});
if let Some(opts) = options {
let opts_json = opts.to_json();
if let Some(obj) = params.as_object_mut()
&& let Some(opts_obj) = opts_json.as_object()
{
obj.extend(opts_obj.clone());
}
}
self.channel().send_no_result("mouseDown", params).await
}
pub(crate) async fn mouse_up(
&self,
options: Option<crate::protocol::MouseOptions>,
) -> Result<()> {
let mut params = serde_json::json!({});
if let Some(opts) = options {
let opts_json = opts.to_json();
if let Some(obj) = params.as_object_mut()
&& let Some(opts_obj) = opts_json.as_object()
{
obj.extend(opts_obj.clone());
}
}
self.channel().send_no_result("mouseUp", params).await
}
pub(crate) async fn mouse_wheel(&self, delta_x: i32, delta_y: i32) -> Result<()> {
self.channel()
.send_no_result(
"mouseWheel",
serde_json::json!({
"deltaX": delta_x,
"deltaY": delta_y
}),
)
.await
}
pub async fn reload(&self, options: Option<GotoOptions>) -> Result<Option<Response>> {
self.navigate_history("reload", options).await
}
pub async fn go_back(&self, options: Option<GotoOptions>) -> Result<Option<Response>> {
self.navigate_history("goBack", options).await
}
pub async fn go_forward(&self, options: Option<GotoOptions>) -> Result<Option<Response>> {
self.navigate_history("goForward", options).await
}
async fn navigate_history(
&self,
method: &str,
options: Option<GotoOptions>,
) -> Result<Option<Response>> {
let opts = self.with_navigation_timeout(options);
let mut params = serde_json::json!({});
if let Some(timeout) = opts.timeout {
params["timeout"] = serde_json::json!(timeout.as_millis() as u64);
} else {
params["timeout"] = serde_json::json!(crate::DEFAULT_TIMEOUT_MS);
}
if let Some(wait_until) = opts.wait_until {
params["waitUntil"] = serde_json::json!(wait_until.as_str());
}
#[derive(Deserialize)]
struct NavigationResponse {
response: Option<ResponseReference>,
}
#[derive(Deserialize)]
struct ResponseReference {
#[serde(deserialize_with = "crate::server::connection::deserialize_arc_str")]
guid: Arc<str>,
}
let result: NavigationResponse = self.channel().send(method, params).await?;
if let Some(response_ref) = result.response {
let response_arc = {
let mut attempts = 0;
let max_attempts = 20;
loop {
match self.connection().get_object(&response_ref.guid).await {
Ok(obj) => break obj,
Err(_) if attempts < max_attempts => {
attempts += 1;
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
Err(e) => return Err(e),
}
}
};
let initializer = response_arc.initializer();
let status = initializer["status"].as_u64().ok_or_else(|| {
crate::error::Error::ProtocolError("Response missing status".to_string())
})? as u16;
let headers = initializer["headers"]
.as_array()
.ok_or_else(|| {
crate::error::Error::ProtocolError("Response missing headers".to_string())
})?
.iter()
.filter_map(|h| {
let name = h["name"].as_str()?;
let value = h["value"].as_str()?;
Some((name.to_string(), value.to_string()))
})
.collect();
let response = Response::new(
initializer["url"]
.as_str()
.ok_or_else(|| {
crate::error::Error::ProtocolError("Response missing url".to_string())
})?
.to_string(),
status,
initializer["statusText"].as_str().unwrap_or("").to_string(),
headers,
Some(response_arc),
);
if let Ok(mut page_url) = self.url.write() {
*page_url = response.url().to_string();
}
Ok(Some(response))
} else {
Ok(None)
}
}
pub async fn query_selector(
&self,
selector: &str,
) -> Result<Option<Arc<crate::protocol::ElementHandle>>> {
let frame = self.main_frame().await?;
frame.query_selector(selector).await
}
pub async fn query_selector_all(
&self,
selector: &str,
) -> Result<Vec<Arc<crate::protocol::ElementHandle>>> {
let frame = self.main_frame().await?;
frame.query_selector_all(selector).await
}
pub async fn screenshot(
&self,
options: Option<crate::protocol::ScreenshotOptions>,
) -> Result<Vec<u8>> {
let params = if let Some(opts) = options {
opts.to_json()
} else {
serde_json::json!({
"type": "png",
"timeout": crate::DEFAULT_TIMEOUT_MS
})
};
#[derive(Deserialize)]
struct ScreenshotResponse {
binary: String,
}
let response: ScreenshotResponse = self.channel().send("screenshot", params).await?;
let bytes = base64::prelude::BASE64_STANDARD
.decode(&response.binary)
.map_err(|e| {
crate::error::Error::ProtocolError(format!("Failed to decode screenshot: {}", e))
})?;
Ok(bytes)
}
pub async fn screenshot_to_file(
&self,
path: &std::path::Path,
options: Option<crate::protocol::ScreenshotOptions>,
) -> Result<Vec<u8>> {
let bytes = self.screenshot(options).await?;
tokio::fs::write(path, &bytes).await.map_err(|e| {
crate::error::Error::ProtocolError(format!("Failed to write screenshot file: {}", e))
})?;
Ok(bytes)
}
pub async fn evaluate_expression(&self, expression: &str) -> Result<()> {
let frame = self.main_frame().await?;
frame.frame_evaluate_expression(expression).await
}
pub async fn evaluate<T: serde::Serialize, U: serde::de::DeserializeOwned>(
&self,
expression: &str,
arg: Option<&T>,
) -> Result<U> {
let frame = self.main_frame().await?;
let result = frame.evaluate(expression, arg).await?;
serde_json::from_value(result).map_err(Error::from)
}
pub async fn evaluate_value(&self, expression: &str) -> Result<String> {
let frame = self.main_frame().await?;
frame.frame_evaluate_expression_value(expression).await
}
pub async fn route<F, Fut>(&self, pattern: &str, handler: F) -> Result<()>
where
F: Fn(Route) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<()>> + Send + 'static,
{
let handler =
Arc::new(move |route: Route| -> RouteHandlerFuture { Box::pin(handler(route)) });
self.route_handlers.lock().unwrap().push(RouteHandlerEntry {
pattern: pattern.to_string(),
handler,
});
self.enable_network_interception().await?;
Ok(())
}
async fn enable_network_interception(&self) -> Result<()> {
let patterns: Vec<serde_json::Value> = self
.route_handlers
.lock()
.unwrap()
.iter()
.map(|entry| serde_json::json!({ "glob": entry.pattern }))
.collect();
self.channel()
.send_no_result(
"setNetworkInterceptionPatterns",
serde_json::json!({
"patterns": patterns
}),
)
.await
}
pub async fn unroute(&self, pattern: &str) -> Result<()> {
self.route_handlers
.lock()
.unwrap()
.retain(|entry| entry.pattern != pattern);
self.enable_network_interception().await
}
pub async fn unroute_all(
&self,
_behavior: Option<crate::protocol::route::UnrouteBehavior>,
) -> Result<()> {
self.route_handlers.lock().unwrap().clear();
self.enable_network_interception().await
}
async fn on_route_event(&self, route: Route) {
let handlers = self.route_handlers.lock().unwrap().clone();
let url = route.request().url().to_string();
for entry in handlers.iter().rev() {
if crate::protocol::route::matches_pattern(&entry.pattern, &url) {
let handler = entry.handler.clone();
if let Err(e) = handler(route.clone()).await {
tracing::warn!("Route handler error: {}", e);
break;
}
if !route.was_handled() {
continue;
}
break;
}
}
}
pub async fn on_download<F, Fut>(&self, handler: F) -> Result<()>
where
F: Fn(Download) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<()>> + Send + 'static,
{
let handler = Arc::new(move |download: Download| -> DownloadHandlerFuture {
Box::pin(handler(download))
});
self.download_handlers.lock().unwrap().push(handler);
Ok(())
}
pub async fn on_dialog<F, Fut>(&self, handler: F) -> Result<()>
where
F: Fn(Dialog) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<()>> + Send + 'static,
{
let handler =
Arc::new(move |dialog: Dialog| -> DialogHandlerFuture { Box::pin(handler(dialog)) });
self.dialog_handlers.lock().unwrap().push(handler);
Ok(())
}
pub async fn on_request<F, Fut>(&self, handler: F) -> Result<()>
where
F: Fn(Request) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<()>> + Send + 'static,
{
let handler = Arc::new(move |request: Request| -> RequestHandlerFuture {
Box::pin(handler(request))
});
let needs_subscription = self.request_handlers.lock().unwrap().is_empty();
if needs_subscription {
_ = self.channel().update_subscription("request", true).await;
}
self.request_handlers.lock().unwrap().push(handler);
Ok(())
}
pub async fn on_request_finished<F, Fut>(&self, handler: F) -> Result<()>
where
F: Fn(Request) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<()>> + Send + 'static,
{
let handler = Arc::new(move |request: Request| -> RequestHandlerFuture {
Box::pin(handler(request))
});
let needs_subscription = self.request_finished_handlers.lock().unwrap().is_empty();
if needs_subscription {
_ = self
.channel()
.update_subscription("requestFinished", true)
.await;
}
self.request_finished_handlers.lock().unwrap().push(handler);
Ok(())
}
pub async fn on_request_failed<F, Fut>(&self, handler: F) -> Result<()>
where
F: Fn(Request) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<()>> + Send + 'static,
{
let handler = Arc::new(move |request: Request| -> RequestHandlerFuture {
Box::pin(handler(request))
});
let needs_subscription = self.request_failed_handlers.lock().unwrap().is_empty();
if needs_subscription {
_ = self
.channel()
.update_subscription("requestFailed", true)
.await;
}
self.request_failed_handlers.lock().unwrap().push(handler);
Ok(())
}
pub async fn on_response<F, Fut>(&self, handler: F) -> Result<()>
where
F: Fn(ResponseObject) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<()>> + Send + 'static,
{
let handler = Arc::new(move |response: ResponseObject| -> ResponseHandlerFuture {
Box::pin(handler(response))
});
let needs_subscription = self.response_handlers.lock().unwrap().is_empty();
if needs_subscription {
_ = self.channel().update_subscription("response", true).await;
}
self.response_handlers.lock().unwrap().push(handler);
Ok(())
}
pub async fn on_websocket<F, Fut>(&self, handler: F) -> Result<()>
where
F: Fn(WebSocket) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<()>> + Send + 'static,
{
let handler =
Arc::new(move |ws: WebSocket| -> WebSocketHandlerFuture { Box::pin(handler(ws)) });
self.websocket_handlers.lock().unwrap().push(handler);
Ok(())
}
pub async fn expose_function<F, Fut>(&self, name: &str, callback: F) -> Result<()>
where
F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
Fut: Future<Output = serde_json::Value> + Send + 'static,
{
self.expose_binding_internal(name, false, callback).await
}
pub async fn expose_binding<F, Fut>(&self, name: &str, callback: F) -> Result<()>
where
F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
Fut: Future<Output = serde_json::Value> + Send + 'static,
{
self.expose_binding_internal(name, true, callback).await
}
async fn expose_binding_internal<F, Fut>(
&self,
name: &str,
_needs_handle: bool,
callback: F,
) -> Result<()>
where
F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
Fut: Future<Output = serde_json::Value> + Send + 'static,
{
let callback: PageBindingCallback = Arc::new(move |args: Vec<serde_json::Value>| {
Box::pin(callback(args)) as PageBindingCallbackFuture
});
self.binding_callbacks
.lock()
.unwrap()
.insert(name.to_string(), callback);
self.channel()
.send_no_result(
"exposeBinding",
serde_json::json!({ "name": name, "needsHandle": false }),
)
.await
}
async fn on_download_event(&self, download: Download) {
let handlers = self.download_handlers.lock().unwrap().clone();
for handler in handlers {
if let Err(e) = handler(download.clone()).await {
tracing::warn!("Download handler error: {}", e);
}
}
}
async fn on_dialog_event(&self, dialog: Dialog) {
let handlers = self.dialog_handlers.lock().unwrap().clone();
for handler in handlers {
if let Err(e) = handler(dialog.clone()).await {
tracing::warn!("Dialog handler error: {}", e);
}
}
}
async fn on_request_event(&self, request: Request) {
let handlers = self.request_handlers.lock().unwrap().clone();
for handler in handlers {
if let Err(e) = handler(request.clone()).await {
tracing::warn!("Request handler error: {}", e);
}
}
}
async fn on_request_failed_event(&self, request: Request) {
let handlers = self.request_failed_handlers.lock().unwrap().clone();
for handler in handlers {
if let Err(e) = handler(request.clone()).await {
tracing::warn!("RequestFailed handler error: {}", e);
}
}
}
async fn on_request_finished_event(&self, request: Request) {
let handlers = self.request_finished_handlers.lock().unwrap().clone();
for handler in handlers {
if let Err(e) = handler(request.clone()).await {
tracing::warn!("RequestFinished handler error: {}", e);
}
}
}
async fn on_response_event(&self, response: ResponseObject) {
let handlers = self.response_handlers.lock().unwrap().clone();
for handler in handlers {
if let Err(e) = handler(response.clone()).await {
tracing::warn!("Response handler error: {}", e);
}
}
}
pub async fn trigger_dialog_event(&self, dialog: Dialog) {
self.on_dialog_event(dialog).await;
}
pub(crate) async fn trigger_request_event(&self, request: Request) {
self.on_request_event(request).await;
}
pub(crate) async fn trigger_request_finished_event(&self, request: Request) {
self.on_request_finished_event(request).await;
}
pub(crate) async fn trigger_request_failed_event(&self, request: Request) {
self.on_request_failed_event(request).await;
}
pub(crate) async fn trigger_response_event(&self, response: ResponseObject) {
self.on_response_event(response).await;
}
pub async fn add_style_tag(
&self,
options: AddStyleTagOptions,
) -> Result<Arc<crate::protocol::ElementHandle>> {
let frame = self.main_frame().await?;
frame.add_style_tag(options).await
}
pub async fn add_init_script(&self, script: &str) -> Result<()> {
self.channel()
.send_no_result("addInitScript", serde_json::json!({ "source": script }))
.await
}
pub async fn set_viewport_size(&self, viewport: crate::protocol::Viewport) -> Result<()> {
if let Ok(mut guard) = self.viewport.write() {
*guard = Some(viewport.clone());
}
self.channel()
.send_no_result(
"setViewportSize",
serde_json::json!({ "viewportSize": viewport }),
)
.await
}
pub async fn bring_to_front(&self) -> Result<()> {
self.channel()
.send_no_result("bringToFront", serde_json::json!({}))
.await
}
pub async fn set_extra_http_headers(
&self,
headers: std::collections::HashMap<String, String>,
) -> Result<()> {
let headers_array: Vec<serde_json::Value> = headers
.into_iter()
.map(|(name, value)| serde_json::json!({ "name": name, "value": value }))
.collect();
self.channel()
.send_no_result(
"setExtraHTTPHeaders",
serde_json::json!({ "headers": headers_array }),
)
.await
}
pub async fn emulate_media(&self, options: Option<EmulateMediaOptions>) -> Result<()> {
let mut params = serde_json::json!({});
if let Some(opts) = options {
if let Some(media) = opts.media {
params["media"] = serde_json::to_value(media).map_err(|e| {
crate::error::Error::ProtocolError(format!("Failed to serialize media: {}", e))
})?;
}
if let Some(color_scheme) = opts.color_scheme {
params["colorScheme"] = serde_json::to_value(color_scheme).map_err(|e| {
crate::error::Error::ProtocolError(format!(
"Failed to serialize colorScheme: {}",
e
))
})?;
}
if let Some(reduced_motion) = opts.reduced_motion {
params["reducedMotion"] = serde_json::to_value(reduced_motion).map_err(|e| {
crate::error::Error::ProtocolError(format!(
"Failed to serialize reducedMotion: {}",
e
))
})?;
}
if let Some(forced_colors) = opts.forced_colors {
params["forcedColors"] = serde_json::to_value(forced_colors).map_err(|e| {
crate::error::Error::ProtocolError(format!(
"Failed to serialize forcedColors: {}",
e
))
})?;
}
}
self.channel().send_no_result("emulateMedia", params).await
}
pub async fn pdf(&self, options: Option<PdfOptions>) -> Result<Vec<u8>> {
let mut params = serde_json::json!({});
let mut save_path: Option<std::path::PathBuf> = None;
if let Some(opts) = options {
save_path = opts.path;
if let Some(scale) = opts.scale {
params["scale"] = serde_json::json!(scale);
}
if let Some(v) = opts.display_header_footer {
params["displayHeaderFooter"] = serde_json::json!(v);
}
if let Some(v) = opts.header_template {
params["headerTemplate"] = serde_json::json!(v);
}
if let Some(v) = opts.footer_template {
params["footerTemplate"] = serde_json::json!(v);
}
if let Some(v) = opts.print_background {
params["printBackground"] = serde_json::json!(v);
}
if let Some(v) = opts.landscape {
params["landscape"] = serde_json::json!(v);
}
if let Some(v) = opts.page_ranges {
params["pageRanges"] = serde_json::json!(v);
}
if let Some(v) = opts.format {
params["format"] = serde_json::json!(v);
}
if let Some(v) = opts.width {
params["width"] = serde_json::json!(v);
}
if let Some(v) = opts.height {
params["height"] = serde_json::json!(v);
}
if let Some(v) = opts.prefer_css_page_size {
params["preferCSSPageSize"] = serde_json::json!(v);
}
if let Some(margin) = opts.margin {
params["margin"] = serde_json::to_value(margin).map_err(|e| {
crate::error::Error::ProtocolError(format!("Failed to serialize margin: {}", e))
})?;
}
}
#[derive(Deserialize)]
struct PdfResponse {
pdf: String,
}
let response: PdfResponse = self.channel().send("pdf", params).await?;
let pdf_bytes = base64::engine::general_purpose::STANDARD
.decode(&response.pdf)
.map_err(|e| {
crate::error::Error::ProtocolError(format!("Failed to decode PDF base64: {}", e))
})?;
if let Some(path) = save_path {
tokio::fs::write(&path, &pdf_bytes).await.map_err(|e| {
crate::error::Error::InvalidArgument(format!(
"Failed to write PDF to '{}': {}",
path.display(),
e
))
})?;
}
Ok(pdf_bytes)
}
pub async fn add_script_tag(
&self,
options: Option<AddScriptTagOptions>,
) -> Result<Arc<crate::protocol::ElementHandle>> {
let opts = options.ok_or_else(|| {
Error::InvalidArgument(
"At least one of content, url, or path must be specified".to_string(),
)
})?;
let frame = self.main_frame().await?;
frame.add_script_tag(opts).await
}
pub fn viewport_size(&self) -> Option<Viewport> {
self.viewport.read().ok()?.clone()
}
}
impl ChannelOwner for Page {
fn guid(&self) -> &str {
self.base.guid()
}
fn type_name(&self) -> &str {
self.base.type_name()
}
fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
self.base.parent()
}
fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
self.base.connection()
}
fn initializer(&self) -> &Value {
self.base.initializer()
}
fn channel(&self) -> &Channel {
self.base.channel()
}
fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
self.base.dispose(reason)
}
fn adopt(&self, child: Arc<dyn ChannelOwner>) {
self.base.adopt(child)
}
fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
self.base.add_child(guid, child)
}
fn remove_child(&self, guid: &str) {
self.base.remove_child(guid)
}
fn on_event(&self, method: &str, params: Value) {
match method {
"navigated" => {
if let Some(url_value) = params.get("url")
&& let Some(url_str) = url_value.as_str()
&& let Ok(mut url) = self.url.write()
{
*url = url_str.to_string();
}
}
"route" => {
if let Some(route_guid) = params
.get("route")
.and_then(|v| v.get("guid"))
.and_then(|v| v.as_str())
{
let connection = self.connection();
let route_guid_owned = route_guid.to_string();
let self_clone = self.clone();
tokio::spawn(async move {
let route: Route =
match connection.get_typed::<Route>(&route_guid_owned).await {
Ok(r) => r,
Err(e) => {
tracing::warn!("Failed to get route object: {}", e);
return;
}
};
if let Some(ctx) =
downcast_parent::<crate::protocol::BrowserContext>(&self_clone)
&& let Ok(api_ctx) = ctx.request().await
{
route.set_api_request_context(api_ctx);
}
self_clone.on_route_event(route).await;
});
}
}
"download" => {
let url = params
.get("url")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let suggested_filename = params
.get("suggestedFilename")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if let Some(artifact_guid) = params
.get("artifact")
.and_then(|v| v.get("guid"))
.and_then(|v| v.as_str())
{
let connection = self.connection();
let artifact_guid_owned = artifact_guid.to_string();
let self_clone = self.clone();
tokio::spawn(async move {
let artifact_arc = match connection.get_object(&artifact_guid_owned).await {
Ok(obj) => obj,
Err(e) => {
tracing::warn!("Failed to get artifact object: {}", e);
return;
}
};
let download = Download::from_artifact(
artifact_arc,
url,
suggested_filename,
self_clone.clone(),
);
self_clone.on_download_event(download).await;
});
}
}
"dialog" => {
}
"webSocket" => {
if let Some(ws_guid) = params
.get("webSocket")
.and_then(|v| v.get("guid"))
.and_then(|v| v.as_str())
{
let connection = self.connection();
let ws_guid_owned = ws_guid.to_string();
let self_clone = self.clone();
tokio::spawn(async move {
let ws: WebSocket =
match connection.get_typed::<WebSocket>(&ws_guid_owned).await {
Ok(ws) => ws,
Err(e) => {
tracing::warn!("Failed to get WebSocket object: {}", e);
return;
}
};
let handlers = self_clone.websocket_handlers.lock().unwrap().clone();
for handler in handlers {
let ws_clone = ws.clone();
tokio::spawn(async move {
if let Err(e) = handler(ws_clone).await {
tracing::error!("Error in websocket handler: {}", e);
}
});
}
});
}
}
"bindingCall" => {
if let Some(binding_guid) = params
.get("binding")
.and_then(|v| v.get("guid"))
.and_then(|v| v.as_str())
{
let connection = self.connection();
let binding_guid_owned = binding_guid.to_string();
let binding_callbacks = self.binding_callbacks.clone();
tokio::spawn(async move {
let binding_call: crate::protocol::BindingCall = match connection
.get_typed::<crate::protocol::BindingCall>(&binding_guid_owned)
.await
{
Ok(bc) => bc,
Err(e) => {
tracing::warn!("Failed to get BindingCall object: {}", e);
return;
}
};
let name = binding_call.name().to_string();
let callback = {
let callbacks = binding_callbacks.lock().unwrap();
callbacks.get(&name).cloned()
};
let Some(callback) = callback else {
return;
};
let raw_args = binding_call.args();
let args = crate::protocol::browser_context::BrowserContext::deserialize_binding_args_pub(raw_args);
let result_value = callback(args).await;
let serialized =
crate::protocol::evaluate_conversion::serialize_argument(&result_value);
if let Err(e) = binding_call.resolve(serialized).await {
tracing::warn!("Failed to resolve BindingCall '{}': {}", name, e);
}
});
}
}
"close" => {
self.is_closed.store(true, Ordering::Relaxed);
}
_ => {
}
}
}
fn was_collected(&self) -> bool {
self.base.was_collected()
}
fn as_any(&self) -> &dyn Any {
self
}
}
impl std::fmt::Debug for Page {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Page")
.field("guid", &self.guid())
.field("url", &self.url())
.finish()
}
}
#[derive(Debug, Clone)]
pub struct GotoOptions {
pub timeout: Option<std::time::Duration>,
pub wait_until: Option<WaitUntil>,
}
impl GotoOptions {
pub fn new() -> Self {
Self {
timeout: None,
wait_until: None,
}
}
pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn wait_until(mut self, wait_until: WaitUntil) -> Self {
self.wait_until = Some(wait_until);
self
}
}
impl Default for GotoOptions {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WaitUntil {
Load,
DomContentLoaded,
NetworkIdle,
Commit,
}
impl WaitUntil {
pub(crate) fn as_str(&self) -> &'static str {
match self {
WaitUntil::Load => "load",
WaitUntil::DomContentLoaded => "domcontentloaded",
WaitUntil::NetworkIdle => "networkidle",
WaitUntil::Commit => "commit",
}
}
}
#[derive(Debug, Clone, Default)]
pub struct AddStyleTagOptions {
pub content: Option<String>,
pub url: Option<String>,
pub path: Option<String>,
}
impl AddStyleTagOptions {
pub fn builder() -> AddStyleTagOptionsBuilder {
AddStyleTagOptionsBuilder::default()
}
pub(crate) fn validate(&self) -> Result<()> {
if self.content.is_none() && self.url.is_none() && self.path.is_none() {
return Err(Error::InvalidArgument(
"At least one of content, url, or path must be specified".to_string(),
));
}
Ok(())
}
}
#[derive(Debug, Clone, Default)]
pub struct AddStyleTagOptionsBuilder {
content: Option<String>,
url: Option<String>,
path: Option<String>,
}
impl AddStyleTagOptionsBuilder {
pub fn content(mut self, content: impl Into<String>) -> Self {
self.content = Some(content.into());
self
}
pub fn url(mut self, url: impl Into<String>) -> Self {
self.url = Some(url.into());
self
}
pub fn path(mut self, path: impl Into<String>) -> Self {
self.path = Some(path.into());
self
}
pub fn build(self) -> AddStyleTagOptions {
AddStyleTagOptions {
content: self.content,
url: self.url,
path: self.path,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct AddScriptTagOptions {
pub content: Option<String>,
pub url: Option<String>,
pub path: Option<String>,
pub type_: Option<String>,
}
impl AddScriptTagOptions {
pub fn builder() -> AddScriptTagOptionsBuilder {
AddScriptTagOptionsBuilder::default()
}
pub(crate) fn validate(&self) -> Result<()> {
if self.content.is_none() && self.url.is_none() && self.path.is_none() {
return Err(Error::InvalidArgument(
"At least one of content, url, or path must be specified".to_string(),
));
}
Ok(())
}
}
#[derive(Debug, Clone, Default)]
pub struct AddScriptTagOptionsBuilder {
content: Option<String>,
url: Option<String>,
path: Option<String>,
type_: Option<String>,
}
impl AddScriptTagOptionsBuilder {
pub fn content(mut self, content: impl Into<String>) -> Self {
self.content = Some(content.into());
self
}
pub fn url(mut self, url: impl Into<String>) -> Self {
self.url = Some(url.into());
self
}
pub fn path(mut self, path: impl Into<String>) -> Self {
self.path = Some(path.into());
self
}
pub fn type_(mut self, type_: impl Into<String>) -> Self {
self.type_ = Some(type_.into());
self
}
pub fn build(self) -> AddScriptTagOptions {
AddScriptTagOptions {
content: self.content,
url: self.url,
path: self.path,
type_: self.type_,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Media {
Screen,
Print,
#[serde(rename = "no-override")]
NoOverride,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum ColorScheme {
#[serde(rename = "light")]
Light,
#[serde(rename = "dark")]
Dark,
#[serde(rename = "no-preference")]
NoPreference,
#[serde(rename = "no-override")]
NoOverride,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum ReducedMotion {
#[serde(rename = "reduce")]
Reduce,
#[serde(rename = "no-preference")]
NoPreference,
#[serde(rename = "no-override")]
NoOverride,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum ForcedColors {
#[serde(rename = "active")]
Active,
#[serde(rename = "none")]
None_,
#[serde(rename = "no-override")]
NoOverride,
}
#[derive(Debug, Clone, Default)]
pub struct EmulateMediaOptions {
pub media: Option<Media>,
pub color_scheme: Option<ColorScheme>,
pub reduced_motion: Option<ReducedMotion>,
pub forced_colors: Option<ForcedColors>,
}
impl EmulateMediaOptions {
pub fn builder() -> EmulateMediaOptionsBuilder {
EmulateMediaOptionsBuilder::default()
}
}
#[derive(Debug, Clone, Default)]
pub struct EmulateMediaOptionsBuilder {
media: Option<Media>,
color_scheme: Option<ColorScheme>,
reduced_motion: Option<ReducedMotion>,
forced_colors: Option<ForcedColors>,
}
impl EmulateMediaOptionsBuilder {
pub fn media(mut self, media: Media) -> Self {
self.media = Some(media);
self
}
pub fn color_scheme(mut self, color_scheme: ColorScheme) -> Self {
self.color_scheme = Some(color_scheme);
self
}
pub fn reduced_motion(mut self, reduced_motion: ReducedMotion) -> Self {
self.reduced_motion = Some(reduced_motion);
self
}
pub fn forced_colors(mut self, forced_colors: ForcedColors) -> Self {
self.forced_colors = Some(forced_colors);
self
}
pub fn build(self) -> EmulateMediaOptions {
EmulateMediaOptions {
media: self.media,
color_scheme: self.color_scheme,
reduced_motion: self.reduced_motion,
forced_colors: self.forced_colors,
}
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct PdfMargin {
#[serde(skip_serializing_if = "Option::is_none")]
pub top: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub right: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bottom: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub left: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct PdfOptions {
pub path: Option<std::path::PathBuf>,
pub scale: Option<f64>,
pub display_header_footer: Option<bool>,
pub header_template: Option<String>,
pub footer_template: Option<String>,
pub print_background: Option<bool>,
pub landscape: Option<bool>,
pub page_ranges: Option<String>,
pub format: Option<String>,
pub width: Option<String>,
pub height: Option<String>,
pub prefer_css_page_size: Option<bool>,
pub margin: Option<PdfMargin>,
}
impl PdfOptions {
pub fn builder() -> PdfOptionsBuilder {
PdfOptionsBuilder::default()
}
}
#[derive(Debug, Clone, Default)]
pub struct PdfOptionsBuilder {
path: Option<std::path::PathBuf>,
scale: Option<f64>,
display_header_footer: Option<bool>,
header_template: Option<String>,
footer_template: Option<String>,
print_background: Option<bool>,
landscape: Option<bool>,
page_ranges: Option<String>,
format: Option<String>,
width: Option<String>,
height: Option<String>,
prefer_css_page_size: Option<bool>,
margin: Option<PdfMargin>,
}
impl PdfOptionsBuilder {
pub fn path(mut self, path: std::path::PathBuf) -> Self {
self.path = Some(path);
self
}
pub fn scale(mut self, scale: f64) -> Self {
self.scale = Some(scale);
self
}
pub fn display_header_footer(mut self, display: bool) -> Self {
self.display_header_footer = Some(display);
self
}
pub fn header_template(mut self, template: impl Into<String>) -> Self {
self.header_template = Some(template.into());
self
}
pub fn footer_template(mut self, template: impl Into<String>) -> Self {
self.footer_template = Some(template.into());
self
}
pub fn print_background(mut self, print: bool) -> Self {
self.print_background = Some(print);
self
}
pub fn landscape(mut self, landscape: bool) -> Self {
self.landscape = Some(landscape);
self
}
pub fn page_ranges(mut self, ranges: impl Into<String>) -> Self {
self.page_ranges = Some(ranges.into());
self
}
pub fn format(mut self, format: impl Into<String>) -> Self {
self.format = Some(format.into());
self
}
pub fn width(mut self, width: impl Into<String>) -> Self {
self.width = Some(width.into());
self
}
pub fn height(mut self, height: impl Into<String>) -> Self {
self.height = Some(height.into());
self
}
pub fn prefer_css_page_size(mut self, prefer: bool) -> Self {
self.prefer_css_page_size = Some(prefer);
self
}
pub fn margin(mut self, margin: PdfMargin) -> Self {
self.margin = Some(margin);
self
}
pub fn build(self) -> PdfOptions {
PdfOptions {
path: self.path,
scale: self.scale,
display_header_footer: self.display_header_footer,
header_template: self.header_template,
footer_template: self.footer_template,
print_background: self.print_background,
landscape: self.landscape,
page_ranges: self.page_ranges,
format: self.format,
width: self.width,
height: self.height,
prefer_css_page_size: self.prefer_css_page_size,
margin: self.margin,
}
}
}
#[derive(Clone)]
pub struct Response {
url: String,
status: u16,
status_text: String,
ok: bool,
headers: std::collections::HashMap<String, String>,
response_channel_owner: Option<std::sync::Arc<dyn crate::server::channel_owner::ChannelOwner>>,
}
impl Response {
pub(crate) fn new(
url: String,
status: u16,
status_text: String,
headers: std::collections::HashMap<String, String>,
response_channel_owner: Option<
std::sync::Arc<dyn crate::server::channel_owner::ChannelOwner>,
>,
) -> Self {
Self {
url,
status,
status_text,
ok: (200..300).contains(&status),
headers,
response_channel_owner,
}
}
}
impl Response {
pub fn url(&self) -> &str {
&self.url
}
pub fn status(&self) -> u16 {
self.status
}
pub fn status_text(&self) -> &str {
&self.status_text
}
pub fn ok(&self) -> bool {
self.ok
}
pub fn headers(&self) -> &std::collections::HashMap<String, String> {
&self.headers
}
pub fn request(&self) -> Option<crate::protocol::Request> {
let owner = self.response_channel_owner.as_ref()?;
downcast_parent::<crate::protocol::Request>(&**owner)
}
pub fn frame(&self) -> Option<crate::protocol::Frame> {
let request = self.request()?;
request.frame()
}
pub(crate) fn response_object(&self) -> crate::error::Result<crate::protocol::ResponseObject> {
let arc = self.response_channel_owner.as_ref().ok_or_else(|| {
crate::error::Error::ProtocolError(
"Response has no backing protocol object".to_string(),
)
})?;
arc.as_any()
.downcast_ref::<crate::protocol::ResponseObject>()
.cloned()
.ok_or_else(|| crate::error::Error::TypeMismatch {
guid: arc.guid().to_string(),
expected: "ResponseObject".to_string(),
actual: arc.type_name().to_string(),
})
}
pub async fn security_details(
&self,
) -> crate::error::Result<Option<crate::protocol::response::SecurityDetails>> {
self.response_object()?.security_details().await
}
pub async fn server_addr(
&self,
) -> crate::error::Result<Option<crate::protocol::response::RemoteAddr>> {
self.response_object()?.server_addr().await
}
pub async fn finished(&self) -> crate::error::Result<()> {
Ok(())
}
pub async fn body(&self) -> crate::error::Result<Vec<u8>> {
self.response_object()?.body().await
}
pub async fn text(&self) -> crate::error::Result<String> {
let bytes = self.body().await?;
String::from_utf8(bytes).map_err(|e| {
crate::error::Error::ProtocolError(format!("Response body is not valid UTF-8: {}", e))
})
}
pub async fn json<T: serde::de::DeserializeOwned>(&self) -> crate::error::Result<T> {
let text = self.text().await?;
serde_json::from_str(&text).map_err(|e| {
crate::error::Error::ProtocolError(format!("Failed to parse response JSON: {}", e))
})
}
pub async fn headers_array(
&self,
) -> crate::error::Result<Vec<crate::protocol::response::HeaderEntry>> {
self.response_object()?.raw_headers().await
}
pub async fn all_headers(
&self,
) -> crate::error::Result<std::collections::HashMap<String, String>> {
let entries = self.headers_array().await?;
let mut map: std::collections::HashMap<String, String> = std::collections::HashMap::new();
for entry in entries {
let key = entry.name.to_lowercase();
map.entry(key)
.and_modify(|v| {
v.push_str(", ");
v.push_str(&entry.value);
})
.or_insert(entry.value);
}
Ok(map)
}
pub async fn header_value(&self, name: &str) -> crate::error::Result<Option<String>> {
let entries = self.headers_array().await?;
let name_lower = name.to_lowercase();
let mut values: Vec<String> = entries
.into_iter()
.filter(|h| h.name.to_lowercase() == name_lower)
.map(|h| h.value)
.collect();
if values.is_empty() {
Ok(None)
} else if values.len() == 1 {
Ok(Some(values.remove(0)))
} else {
Ok(Some(values.join(", ")))
}
}
}
impl std::fmt::Debug for Response {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Response")
.field("url", &self.url)
.field("status", &self.status)
.field("status_text", &self.status_text)
.field("ok", &self.ok)
.finish_non_exhaustive()
}
}
pub(crate) async fn set_timeout_and_notify(
channel: &crate::server::channel::Channel,
method: &str,
timeout: f64,
) {
if let Err(e) = channel
.send_no_result(method, serde_json::json!({ "timeout": timeout }))
.await
{
tracing::warn!("{} send error: {}", method, e);
}
}