Skip to main content

chromiumoxide/handler/
mod.rs

1use crate::listeners::{EventListenerRequest, EventListeners};
2use chromiumoxide_cdp::cdp::browser_protocol::browser::*;
3use chromiumoxide_cdp::cdp::browser_protocol::target::*;
4use chromiumoxide_cdp::cdp::events::CdpEvent;
5use chromiumoxide_cdp::cdp::events::CdpEventMessage;
6use chromiumoxide_types::{CallId, Message, Method, Response};
7use chromiumoxide_types::{MethodId, Request as CdpRequest};
8use fnv::FnvHashMap;
9use futures_util::Stream;
10use hashbrown::{HashMap, HashSet};
11use spider_network_blocker::intercept_manager::NetworkInterceptManager;
12use std::pin::Pin;
13use std::task::{Context, Poll};
14use std::time::{Duration, Instant};
15use tokio::sync::mpsc::Receiver;
16use tokio::sync::oneshot::Sender as OneshotSender;
17use tokio_tungstenite::tungstenite::error::ProtocolError;
18use tokio_tungstenite::tungstenite::Error;
19
20use std::sync::Arc;
21use tokio::sync::Notify;
22
23use crate::cmd::{to_command_response, CommandMessage};
24use crate::conn::Connection;
25use crate::error::{CdpError, Result};
26use crate::handler::browser::BrowserContext;
27use crate::handler::frame::FrameRequestedNavigation;
28use crate::handler::frame::{NavigationError, NavigationId, NavigationOk};
29use crate::handler::httpfuture::navigation_continues;
30use crate::handler::job::PeriodicJob;
31use crate::handler::session::Session;
32use crate::handler::target::TargetEvent;
33use crate::handler::target::{Target, TargetConfig};
34use crate::handler::viewport::Viewport;
35use crate::page::Page;
36pub(crate) use page::PageInner;
37
38/// Standard timeout in MS
39pub const REQUEST_TIMEOUT: u64 = 30_000;
40
41pub mod blockers;
42pub mod browser;
43pub mod commandfuture;
44pub mod domworld;
45pub mod emulation;
46pub mod frame;
47pub mod http;
48pub mod httpfuture;
49mod job;
50pub mod network;
51pub mod network_utils;
52pub mod page;
53#[cfg(feature = "parallel-handler")]
54pub mod parallel;
55pub mod sender;
56mod session;
57pub mod target;
58pub mod target_message_future;
59pub mod viewport;
60
61/// The handler that monitors the state of the chromium browser and drives all
62/// the requests and events.
63#[must_use = "streams do nothing unless polled"]
64#[derive(Debug)]
65pub struct Handler {
66    pub default_browser_context: BrowserContext,
67    pub browser_contexts: HashSet<BrowserContext>,
68    /// Commands that are being processed and awaiting a response from the
69    /// chromium instance together with the timestamp when the request
70    /// started.
71    pending_commands: FnvHashMap<CallId, (PendingRequest, MethodId, Instant)>,
72    /// Connection to the browser instance
73    from_browser: Receiver<HandlerMessage>,
74    /// Used to loop over all targets in a consistent manner
75    target_ids: Vec<TargetId>,
76    /// The created and attached targets
77    targets: HashMap<TargetId, Target>,
78    /// Currently queued in navigations for targets
79    navigations: FnvHashMap<NavigationId, NavigationRequest>,
80    /// Keeps track of all the current active sessions
81    ///
82    /// There can be multiple sessions per target.
83    sessions: HashMap<SessionId, Session>,
84    /// The websocket connection to the chromium instance.
85    /// `Option` so that `run()` can `.take()` it for splitting.
86    conn: Option<Connection<CdpEventMessage>>,
87    /// Evicts timed out requests periodically
88    evict_command_timeout: PeriodicJob,
89    /// The internal identifier for a specific navigation
90    next_navigation_id: usize,
91    /// How this handler will configure targets etc,
92    config: HandlerConfig,
93    /// All registered event subscriptions
94    event_listeners: EventListeners,
95    /// Keeps track is the browser is closing
96    closing: bool,
97    /// Track the bytes remainder until network request will be blocked.
98    remaining_bytes: Option<u64>,
99    /// The budget is exhausted.
100    budget_exhausted: bool,
101    /// Tracks which targets we've already attached to, to avoid multiple sessions per target.
102    attached_targets: HashSet<TargetId>,
103    /// Optional notify for waking `Handler::run()`'s `tokio::select!` loop
104    /// when a page sends a message.  `None` when using the `Stream` API.
105    page_wake: Option<Arc<Notify>>,
106}
107
108lazy_static::lazy_static! {
109    /// Set the discovery ID target.
110    static ref DISCOVER_ID: (std::borrow::Cow<'static, str>, serde_json::Value) = {
111        let discover = SetDiscoverTargetsParams::new(true);
112        (discover.identifier(), serde_json::to_value(discover).expect("valid discover target params"))
113    };
114    /// Targets params id.
115    static ref TARGET_PARAMS_ID: (std::borrow::Cow<'static, str>, serde_json::Value) = {
116        let msg = GetTargetsParams { filter: None };
117        (msg.identifier(), serde_json::to_value(msg).expect("valid paramtarget"))
118    };
119    /// Set the close targets.
120    static ref CLOSE_PARAMS_ID: (std::borrow::Cow<'static, str>, serde_json::Value) = {
121        let close_msg = CloseParams::default();
122        (close_msg.identifier(), serde_json::to_value(close_msg).expect("valid close params"))
123    };
124}
125
126fn maybe_store_attach_session_id(target: &mut Target, method: &MethodId, resp: &Response) {
127    if method.as_ref() != AttachToTargetParams::IDENTIFIER {
128        return;
129    }
130
131    if let Ok(resp) = to_command_response::<AttachToTargetParams>(resp.clone(), method.clone()) {
132        target.set_session_id(resp.result.session_id);
133    }
134}
135
136impl Handler {
137    /// Create a new `Handler` that drives the connection and listens for
138    /// messages on the receiver `rx`.
139    pub(crate) fn new(
140        mut conn: Connection<CdpEventMessage>,
141        rx: Receiver<HandlerMessage>,
142        config: HandlerConfig,
143    ) -> Self {
144        let discover = DISCOVER_ID.clone();
145        let _ = conn.submit_command(discover.0, None, discover.1);
146        let conn = Some(conn);
147
148        let browser_contexts = config
149            .context_ids
150            .iter()
151            .map(|id| BrowserContext::from(id.clone()))
152            .collect();
153
154        Self {
155            pending_commands: Default::default(),
156            from_browser: rx,
157            default_browser_context: Default::default(),
158            browser_contexts,
159            target_ids: Default::default(),
160            targets: Default::default(),
161            navigations: Default::default(),
162            sessions: Default::default(),
163            conn,
164            evict_command_timeout: PeriodicJob::new(config.request_timeout),
165            next_navigation_id: 0,
166            config,
167            event_listeners: Default::default(),
168            closing: false,
169            remaining_bytes: None,
170            budget_exhausted: false,
171            attached_targets: Default::default(),
172            page_wake: None,
173        }
174    }
175
176    /// Borrow the WebSocket connection, returning an error if it has been
177    /// consumed by [`Handler::run()`].
178    #[inline]
179    fn conn(&mut self) -> Result<&mut Connection<CdpEventMessage>> {
180        self.conn
181            .as_mut()
182            .ok_or_else(|| CdpError::msg("connection consumed by Handler::run()"))
183    }
184
185    /// Return the target with the matching `target_id`
186    pub fn get_target(&self, target_id: &TargetId) -> Option<&Target> {
187        self.targets.get(target_id)
188    }
189
190    /// Iterator over all currently attached targets
191    pub fn targets(&self) -> impl Iterator<Item = &Target> + '_ {
192        self.targets.values()
193    }
194
195    /// The default Browser context
196    pub fn default_browser_context(&self) -> &BrowserContext {
197        &self.default_browser_context
198    }
199
200    /// Iterator over all currently available browser contexts
201    pub fn browser_contexts(&self) -> impl Iterator<Item = &BrowserContext> + '_ {
202        self.browser_contexts.iter()
203    }
204
205    /// received a response to a navigation request like `Page.navigate`
206    fn on_navigation_response(&mut self, id: NavigationId, resp: Response) {
207        if let Some(nav) = self.navigations.remove(&id) {
208            match nav {
209                NavigationRequest::Navigate(target_id, mut nav) => {
210                    // Read the field directly rather than decoding NavigateReturns:
211                    // cheaper, and a malformed sibling field cannot make the whole
212                    // decode fail. A silent decode failure here would park the ack
213                    // again, which is the hang this branch exists to remove.
214                    // The empty check is load-bearing and duplicated in HttpFuture's
215                    // probe: both paths must treat an empty errorText as absent.
216                    let failed = resp
217                        .result
218                        .as_ref()
219                        .and_then(|result| result.get("errorText"))
220                        .and_then(serde_json::Value::as_str)
221                        .is_some_and(|err| !err.is_empty() && !navigation_continues(err));
222                    if failed {
223                        // Terminal failures may never emit lifecycle events. Release
224                        // the ack and remove the watcher before another goto is queued.
225                        if let Some(target) = self.targets.get_mut(&target_id) {
226                            target.frame_manager_mut().abandon_navigation(id);
227                        }
228                        let _ = nav.tx.send(Ok(resp));
229                    } else if nav.navigated {
230                        let _ = nav.tx.send(Ok(resp));
231                    } else {
232                        nav.set_response(resp);
233                        self.navigations
234                            .insert(id, NavigationRequest::Navigate(target_id, nav));
235                    }
236                }
237            }
238        }
239    }
240
241    /// A navigation has finished.
242    fn on_navigation_lifecycle_completed(&mut self, res: Result<NavigationOk, NavigationError>) {
243        match res {
244            Ok(ok) => {
245                let id = *ok.navigation_id();
246                if let Some(nav) = self.navigations.remove(&id) {
247                    match nav {
248                        NavigationRequest::Navigate(target_id, mut nav) => {
249                            if let Some(resp) = nav.response.take() {
250                                let _ = nav.tx.send(Ok(resp));
251                            } else {
252                                nav.set_navigated();
253                                self.navigations
254                                    .insert(id, NavigationRequest::Navigate(target_id, nav));
255                            }
256                        }
257                    }
258                }
259            }
260            Err(err) => {
261                if let Some(nav) = self.navigations.remove(err.navigation_id()) {
262                    match nav {
263                        NavigationRequest::Navigate(_, nav) => {
264                            let _ = nav.tx.send(Err(err.into()));
265                        }
266                    }
267                }
268            }
269        }
270    }
271
272    /// Received a response to a request.
273    fn on_response(&mut self, resp: Response) {
274        if let Some((req, method, _)) = self.pending_commands.remove(&resp.id) {
275            match req {
276                PendingRequest::CreateTarget(tx) => {
277                    match to_command_response::<CreateTargetParams>(resp, method) {
278                        Ok(resp) => {
279                            if let Some(target) = self.targets.get_mut(&resp.target_id) {
280                                target.set_initiator(tx);
281                            } else {
282                                let _ = tx.send(Err(CdpError::NotFound)).ok();
283                            }
284                        }
285                        Err(err) => {
286                            let _ = tx.send(Err(err)).ok();
287                        }
288                    }
289                }
290                PendingRequest::GetTargets(tx) => {
291                    match to_command_response::<GetTargetsParams>(resp, method) {
292                        Ok(resp) => {
293                            let targets = resp.result.target_infos;
294                            let results = targets.clone();
295
296                            for target_info in targets {
297                                let event: EventTargetCreated = EventTargetCreated { target_info };
298                                self.on_target_created(event);
299                            }
300
301                            let _ = tx.send(Ok(results)).ok();
302                        }
303                        Err(err) => {
304                            let _ = tx.send(Err(err)).ok();
305                        }
306                    }
307                }
308                PendingRequest::Navigate(id) => {
309                    self.on_navigation_response(id, resp);
310                    if self.config.only_html && !self.config.created_first_target {
311                        self.config.created_first_target = true;
312                    }
313                }
314                PendingRequest::ExternalCommand { tx, .. } => {
315                    let _ = tx.send(Ok(resp)).ok();
316                }
317                PendingRequest::InternalCommand(target_id) => {
318                    if let Some(target) = self.targets.get_mut(&target_id) {
319                        maybe_store_attach_session_id(target, &method, &resp);
320                        target.on_response(resp, method.as_ref());
321                    }
322                }
323                PendingRequest::CloseBrowser(tx) => {
324                    self.closing = true;
325                    let _ = tx.send(Ok(CloseReturns {})).ok();
326                }
327            }
328        }
329    }
330
331    /// Submit a command initiated via channel
332    pub(crate) fn submit_external_command(
333        &mut self,
334        msg: CommandMessage,
335        now: Instant,
336    ) -> Result<()> {
337        // Resolve session_id → target_id before `submit_command`
338        // consumes `msg.session_id`. `None` when the session hasn't
339        // landed in `self.sessions` yet; that command then relies on
340        // the normal request_timeout path if the target later crashes.
341        let target_id = msg
342            .session_id
343            .as_ref()
344            .and_then(|sid| self.sessions.get(sid.as_ref()))
345            .map(|s| s.target_id().clone());
346        let call_id =
347            self.conn()?
348                .submit_command(msg.method.clone(), msg.session_id, msg.params)?;
349        self.pending_commands.insert(
350            call_id,
351            (
352                PendingRequest::ExternalCommand {
353                    tx: msg.sender,
354                    target_id,
355                },
356                msg.method,
357                now,
358            ),
359        );
360        Ok(())
361    }
362
363    pub(crate) fn submit_internal_command(
364        &mut self,
365        target_id: TargetId,
366        req: CdpRequest,
367        now: Instant,
368    ) -> Result<()> {
369        let call_id = self.conn()?.submit_command(
370            req.method.clone(),
371            req.session_id.map(Into::into),
372            req.params,
373        )?;
374        self.pending_commands.insert(
375            call_id,
376            (PendingRequest::InternalCommand(target_id), req.method, now),
377        );
378        Ok(())
379    }
380
381    fn submit_fetch_targets(&mut self, tx: OneshotSender<Result<Vec<TargetInfo>>>, now: Instant) {
382        let msg = TARGET_PARAMS_ID.clone();
383
384        if let Some(conn) = self.conn.as_mut() {
385            if let Ok(call_id) = conn.submit_command(msg.0.clone(), None, msg.1) {
386                self.pending_commands
387                    .insert(call_id, (PendingRequest::GetTargets(tx), msg.0, now));
388            }
389        }
390    }
391
392    /// Send the Request over to the server and store its identifier to handle
393    /// the response once received.
394    fn submit_navigation(&mut self, id: NavigationId, req: CdpRequest, now: Instant) {
395        if let Some(conn) = self.conn.as_mut() {
396            if let Ok(call_id) = conn.submit_command(
397                req.method.clone(),
398                req.session_id.map(Into::into),
399                req.params,
400            ) {
401                self.pending_commands
402                    .insert(call_id, (PendingRequest::Navigate(id), req.method, now));
403            }
404        }
405    }
406
407    fn submit_close(&mut self, tx: OneshotSender<Result<CloseReturns>>, now: Instant) {
408        let close_msg = CLOSE_PARAMS_ID.clone();
409
410        if let Some(conn) = self.conn.as_mut() {
411            if let Ok(call_id) = conn.submit_command(close_msg.0.clone(), None, close_msg.1) {
412                self.pending_commands.insert(
413                    call_id,
414                    (PendingRequest::CloseBrowser(tx), close_msg.0, now),
415                );
416            }
417        }
418    }
419
420    /// Process a message received by the target's page via channel
421    fn on_target_message(&mut self, target: &mut Target, msg: CommandMessage, now: Instant) {
422        if msg.is_navigation() {
423            let (req, tx) = msg.split();
424            let id = self.next_navigation_id();
425
426            target.goto(FrameRequestedNavigation::new(
427                id,
428                req,
429                self.config.request_timeout,
430            ));
431
432            self.navigations.insert(
433                id,
434                NavigationRequest::Navigate(
435                    target.target_id().clone(),
436                    NavigationInProgress::new(tx),
437                ),
438            );
439        } else {
440            let _ = self.submit_external_command(msg, now);
441        }
442    }
443
444    /// An identifier for queued `NavigationRequest`s.
445    fn next_navigation_id(&mut self) -> NavigationId {
446        let id = NavigationId(self.next_navigation_id);
447        self.next_navigation_id = self.next_navigation_id.wrapping_add(1);
448        id
449    }
450
451    /// Create a new page and send it to the receiver when ready
452    ///
453    /// First a `CreateTargetParams` is send to the server, this will trigger
454    /// `EventTargetCreated` which results in a new `Target` being created.
455    /// Once the response to the request is received the initialization process
456    /// of the target kicks in. This triggers a queue of initialization requests
457    /// of the `Target`, once those are all processed and the `url` fo the
458    /// `CreateTargetParams` has finished loading (The `Target`'s `Page` is
459    /// ready and idle), the `Target` sends its newly created `Page` as response
460    /// to the initiator (`tx`) of the `CreateTargetParams` request.
461    fn create_page(&mut self, params: CreateTargetParams, tx: OneshotSender<Result<Page>>) {
462        let about_blank = params.url == "about:blank";
463        let http_check =
464            !about_blank && params.url.starts_with("http") || params.url.starts_with("file://");
465
466        if about_blank || http_check {
467            let method = params.identifier();
468
469            let Some(conn) = self.conn.as_mut() else {
470                let _ = tx.send(Err(CdpError::msg("connection consumed"))).ok();
471                return;
472            };
473            match serde_json::to_value(params) {
474                Ok(params) => match conn.submit_command(method.clone(), None, params) {
475                    Ok(call_id) => {
476                        self.pending_commands.insert(
477                            call_id,
478                            (PendingRequest::CreateTarget(tx), method, Instant::now()),
479                        );
480                    }
481                    Err(err) => {
482                        let _ = tx.send(Err(err.into())).ok();
483                    }
484                },
485                Err(err) => {
486                    let _ = tx.send(Err(err.into())).ok();
487                }
488            }
489        } else {
490            let _ = tx.send(Err(CdpError::NotFound)).ok();
491        }
492    }
493
494    /// Process an incoming event read from the websocket
495    fn on_event(&mut self, event: CdpEventMessage) {
496        if let Some(session_id) = &event.session_id {
497            if let Some(session) = self.sessions.get(session_id.as_str()) {
498                if let Some(target) = self.targets.get_mut(session.target_id()) {
499                    return target.on_event(event);
500                }
501            }
502        }
503        let CdpEventMessage { params, method, .. } = event;
504
505        match params {
506            CdpEvent::TargetTargetCreated(ref ev) => self.on_target_created((**ev).clone()),
507            CdpEvent::TargetAttachedToTarget(ref ev) => self.on_attached_to_target(ev.clone()),
508            CdpEvent::TargetTargetDestroyed(ref ev) => self.on_target_destroyed(ev.clone()),
509            CdpEvent::TargetTargetCrashed(ref ev) => self.on_target_crashed(ev.clone()),
510            CdpEvent::TargetDetachedFromTarget(ref ev) => self.on_detached_from_target(ev.clone()),
511            _ => {}
512        }
513
514        chromiumoxide_cdp::consume_event!(match params {
515            |ev| self.event_listeners.start_send(ev),
516            |json| { let _ = self.event_listeners.try_send_custom(&method, json);}
517        });
518    }
519
520    /// Fired when a new target was created on the chromium instance
521    ///
522    /// Creates a new `Target` instance and keeps track of it
523    fn on_target_created(&mut self, event: EventTargetCreated) {
524        if !self.browser_contexts.is_empty() {
525            if let Some(ref context_id) = event.target_info.browser_context_id {
526                let bc = BrowserContext {
527                    id: Some(context_id.clone()),
528                };
529                if !self.browser_contexts.contains(&bc) {
530                    return;
531                }
532            }
533        }
534        let browser_ctx = event
535            .target_info
536            .browser_context_id
537            .clone()
538            .map(BrowserContext::from)
539            .unwrap_or_else(|| self.default_browser_context.clone());
540        let target = Target::new(
541            event.target_info,
542            TargetConfig {
543                ignore_https_errors: self.config.ignore_https_errors,
544                request_timeout: self.config.request_timeout,
545                viewport: self.config.viewport.clone(),
546                request_intercept: self.config.request_intercept,
547                cache_enabled: self.config.cache_enabled,
548                service_worker_enabled: self.config.service_worker_enabled,
549                ignore_visuals: self.config.ignore_visuals,
550                ignore_stylesheets: self.config.ignore_stylesheets,
551                ignore_javascript: self.config.ignore_javascript,
552                ignore_analytics: self.config.ignore_analytics,
553                ignore_prefetch: self.config.ignore_prefetch,
554                allow_first_party_stylesheets: self.config.allow_first_party_stylesheets,
555                allow_first_party_javascript: self.config.allow_first_party_javascript,
556                allow_first_party_visuals: self.config.allow_first_party_visuals,
557                extra_headers: self.config.extra_headers.clone(),
558                only_html: self.config.only_html && self.config.created_first_target,
559                intercept_manager: self.config.intercept_manager,
560                remote_local_policy: self.config.remote_local_policy,
561                max_bytes_allowed: self.config.max_bytes_allowed,
562                max_redirects: self.config.max_redirects,
563                max_main_frame_navigations: self.config.max_main_frame_navigations,
564                whitelist_patterns: self.config.whitelist_patterns.clone(),
565                blacklist_patterns: self.config.blacklist_patterns.clone(),
566                #[cfg(feature = "adblock")]
567                adblock_filter_rules: self.config.adblock_filter_rules.clone(),
568                page_wake: self.page_wake.clone(),
569                page_channel_capacity: self.config.page_channel_capacity,
570            },
571            browser_ctx,
572        );
573
574        let tid = target.target_id().clone();
575        self.target_ids.push(tid.clone());
576        self.targets.insert(tid, target);
577    }
578
579    /// A new session is attached to a target
580    fn on_attached_to_target(&mut self, event: Box<EventAttachedToTarget>) {
581        let session = Session::new(event.session_id.clone(), event.target_info.target_id);
582        if let Some(target) = self.targets.get_mut(session.target_id()) {
583            target.set_session_id(session.session_id().clone())
584        }
585        self.sessions.insert(event.session_id, session);
586    }
587
588    /// The session was detached from target.
589    /// Can be issued multiple times per target if multiple session have been
590    /// attached to it.
591    fn on_detached_from_target(&mut self, event: EventDetachedFromTarget) {
592        // remove the session
593        if let Some(session) = self.sessions.remove(&event.session_id) {
594            if let Some(target) = self.targets.get_mut(session.target_id()) {
595                target.session_id_mut().take();
596            }
597        }
598    }
599
600    /// Fired when the target was destroyed in the browser
601    fn on_target_destroyed(&mut self, event: EventTargetDestroyed) {
602        self.attached_targets.remove(&event.target_id);
603
604        if let Some(target) = self.targets.remove(&event.target_id) {
605            // TODO shutdown?
606            if let Some(session) = target.session_id() {
607                self.sessions.remove(session);
608            }
609        }
610    }
611
612    /// Fired when a target has crashed (`Target.targetCrashed`).
613    ///
614    /// Unlike `targetDestroyed` (clean teardown), a crash means any
615    /// in-flight commands on that target will never receive a
616    /// response. Without explicit cancellation those commands sit in
617    /// `pending_commands` until the `request_timeout` evicts them,
618    /// which surfaces to callers as long latency tails on what is
619    /// really an immediate failure.
620    ///
621    /// Cancellation policy:
622    /// * `ExternalCommand { target_id: Some(crashed), .. }` — the
623    ///   caller's oneshot resolves with an error carrying the
624    ///   termination `status` + `errorCode` from the crash event.
625    /// * `InternalCommand(crashed)` — dropped silently; these are
626    ///   target-init commands whose caller is the target itself,
627    ///   which we're about to remove.
628    /// * `ExternalCommand { target_id: None, .. }` — left alone;
629    ///   browser-level or pre-attach-race commands aren't bound to
630    ///   this target.
631    /// * `Navigate(_)` and entries in `self.navigations` — left to
632    ///   the normal timeout path; `on_navigation_response` drops
633    ///   late responses once the target is removed below.
634    fn on_target_crashed(&mut self, event: EventTargetCrashed) {
635        let crashed_id = event.target_id.clone();
636        let status = event.status.clone();
637        let error_code = event.error_code;
638
639        // Two-pass cancellation: collect matching call-ids, then
640        // remove + signal. Can't signal inside `iter()` because
641        // `OneshotSender::send` consumes the sender, and the
642        // borrow checker disallows taking ownership from inside
643        // the iterator.
644        let to_cancel: Vec<CallId> = self
645            .pending_commands
646            .iter()
647            .filter_map(|(&call_id, (req, _, _))| match req {
648                PendingRequest::ExternalCommand {
649                    target_id: Some(tid),
650                    ..
651                } if *tid == crashed_id => Some(call_id),
652                PendingRequest::InternalCommand(tid) if *tid == crashed_id => Some(call_id),
653                _ => None,
654            })
655            .collect();
656
657        for call_id in to_cancel {
658            if let Some((req, _, _)) = self.pending_commands.remove(&call_id) {
659                match req {
660                    PendingRequest::ExternalCommand { tx, .. } => {
661                        let _ = tx.send(Err(CdpError::msg(format!(
662                            "target {:?} crashed: {} (errorCode={})",
663                            crashed_id, status, error_code
664                        ))));
665                    }
666                    PendingRequest::InternalCommand(_) => {
667                        // Target-init command — the target is gone,
668                        // nobody is waiting on a user-facing reply.
669                    }
670                    _ => {}
671                }
672            }
673        }
674
675        // Same map cleanup as `on_target_destroyed`.
676        self.attached_targets.remove(&crashed_id);
677        if let Some(target) = self.targets.remove(&crashed_id) {
678            if let Some(session) = target.session_id() {
679                self.sessions.remove(session);
680            }
681        }
682    }
683
684    /// House keeping of commands
685    ///
686    /// Remove all commands where `now` > `timestamp of command starting point +
687    /// request timeout` and notify the senders that their request timed out.
688    fn evict_timed_out_commands(&mut self, now: Instant) {
689        let deadline = match now.checked_sub(self.config.request_timeout) {
690            Some(d) => d,
691            None => return,
692        };
693
694        let timed_out: Vec<_> = self
695            .pending_commands
696            .iter()
697            .filter(|(_, (_, _, timestamp))| *timestamp < deadline)
698            .map(|(k, _)| *k)
699            .collect();
700
701        for call in timed_out {
702            if let Some((req, _, _)) = self.pending_commands.remove(&call) {
703                match req {
704                    PendingRequest::CreateTarget(tx) => {
705                        let _ = tx.send(Err(CdpError::Timeout));
706                    }
707                    PendingRequest::GetTargets(tx) => {
708                        let _ = tx.send(Err(CdpError::Timeout));
709                    }
710                    PendingRequest::Navigate(nav) => {
711                        if let Some(nav) = self.navigations.remove(&nav) {
712                            match nav {
713                                NavigationRequest::Navigate(_, nav) => {
714                                    let _ = nav.tx.send(Err(CdpError::Timeout));
715                                }
716                            }
717                        }
718                    }
719                    PendingRequest::ExternalCommand { tx, .. } => {
720                        let _ = tx.send(Err(CdpError::Timeout));
721                    }
722                    PendingRequest::InternalCommand(_) => {}
723                    PendingRequest::CloseBrowser(tx) => {
724                        let _ = tx.send(Err(CdpError::Timeout));
725                    }
726                }
727            }
728        }
729    }
730
731    pub fn event_listeners_mut(&mut self) -> &mut EventListeners {
732        &mut self.event_listeners
733    }
734
735    // ------------------------------------------------------------------
736    //  Tokio-native async entry point
737    // ------------------------------------------------------------------
738
739    /// Run the handler as a fully async tokio task.
740    ///
741    /// This is the high-performance alternative to polling `Handler` as a
742    /// `Stream`.  Internally it:
743    ///
744    /// * Splits the WebSocket into independent read/write halves — the
745    ///   writer runs in its own tokio task with natural batching.
746    /// * Uses `tokio::select!` to multiplex the browser channel, page
747    ///   notifications, WebSocket reads, the eviction timer, and writer
748    ///   health.
749    /// * Drains every target's page channel via `try_recv()` (non-blocking)
750    ///   after each event, with an `Arc<Notify>` ensuring the select loop
751    ///   wakes up whenever a page sends a message.
752    ///
753    /// # Usage
754    ///
755    /// ```rust,no_run
756    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
757    /// use chromiumoxide::Browser;
758    /// let (browser, handler) = Browser::launch(Default::default()).await?;
759    /// let handler_task = tokio::spawn(handler.run());
760    /// // … use browser …
761    /// # Ok(())
762    /// # }
763    /// ```
764    pub async fn run(mut self) -> Result<()> {
765        use chromiumoxide_types::Message;
766        use tokio::time::MissedTickBehavior;
767        use tokio_tungstenite::tungstenite::{self, error::ProtocolError};
768
769        // --- set up page notification ---
770        let page_wake = Arc::new(Notify::new());
771        self.page_wake = Some(page_wake.clone());
772
773        // --- split WebSocket ---
774        let conn = self
775            .conn
776            .take()
777            .ok_or_else(|| CdpError::msg("Handler::run() called with no connection"))?;
778        let async_conn = conn.into_async();
779        let mut ws_reader = async_conn.reader;
780        let ws_tx = async_conn.cmd_tx;
781        let mut writer_handle = async_conn.writer_handle;
782        let reader_handle = async_conn.reader_handle;
783        let mut next_call_id = async_conn.next_id;
784
785        // Helper to mint call-ids without &mut self.conn.
786        let mut alloc_call_id = || {
787            let id = chromiumoxide_types::CallId::new(next_call_id);
788            next_call_id = next_call_id.wrapping_add(1);
789            id
790        };
791
792        // --- eviction timer ---
793        let mut evict_timer = tokio::time::interval_at(
794            tokio::time::Instant::now() + self.config.request_timeout,
795            self.config.request_timeout,
796        );
797        evict_timer.set_missed_tick_behavior(MissedTickBehavior::Delay);
798
799        // Helper closure: submit a MethodCall through the WS writer.
800        macro_rules! ws_submit {
801            ($method:expr, $session_id:expr, $params:expr) => {{
802                let id = alloc_call_id();
803                let call = chromiumoxide_types::MethodCall {
804                    id,
805                    method: $method,
806                    session_id: $session_id,
807                    params: $params,
808                };
809                match ws_tx.try_send(call) {
810                    Ok(()) => Ok::<_, CdpError>(id),
811                    Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => {
812                        tracing::warn!("WS command channel full — dropping command");
813                        Err(CdpError::msg("WS command channel full"))
814                    }
815                    Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
816                        Err(CdpError::msg("WS writer closed"))
817                    }
818                }
819            }};
820        }
821
822        // ---- main event loop ----
823        //
824        // Modeled as an expression-loop producing `Result<()>` so that every
825        // exit path falls through to the graceful-shutdown block below
826        // (drop ws_tx → writer drains queue + sends WS Close → reader
827        // aborted). This matters for remote browsers (`Browser::connect`)
828        // where there is no child process whose death closes the socket.
829        let run_result: Result<()> = loop {
830            let now = std::time::Instant::now();
831
832            // 1. Drain all target page channels (non-blocking) & advance
833            //    state machines.
834            //
835            // Budget: drain at most 128 messages per target per iteration
836            // so a single chatty page cannot starve the rest.
837            const PER_TARGET_DRAIN_BUDGET: usize = 128;
838
839            for n in (0..self.target_ids.len()).rev() {
840                let target_id = self.target_ids.swap_remove(n);
841
842                if let Some((id, mut target)) = self.targets.remove_entry(&target_id) {
843                    // Drain page channel (non-blocking — waker is the Notify).
844                    {
845                        let mut msgs = Vec::new();
846                        if let Some(handle) = target.page_mut() {
847                            while msgs.len() < PER_TARGET_DRAIN_BUDGET {
848                                match handle.rx.try_recv() {
849                                    Ok(msg) => msgs.push(msg),
850                                    Err(_) => break,
851                                }
852                            }
853                        }
854                        for msg in msgs {
855                            target.on_page_message(msg);
856                        }
857                    }
858
859                    // Advance target state machine & process events.
860                    while let Some(event) = target.advance(now) {
861                        match event {
862                            TargetEvent::Request(req) => {
863                                if let Ok(call_id) =
864                                    ws_submit!(req.method.clone(), req.session_id, req.params)
865                                {
866                                    self.pending_commands.insert(
867                                        call_id,
868                                        (
869                                            PendingRequest::InternalCommand(
870                                                target.target_id().clone(),
871                                            ),
872                                            req.method,
873                                            now,
874                                        ),
875                                    );
876                                }
877                            }
878                            TargetEvent::Command(msg) => {
879                                if msg.is_navigation() {
880                                    let (req, tx) = msg.split();
881                                    let nav_id = self.next_navigation_id();
882                                    target.goto(FrameRequestedNavigation::new(
883                                        nav_id,
884                                        req.clone(),
885                                        self.config.request_timeout,
886                                    ));
887                                    if let Ok(call_id) =
888                                        ws_submit!(req.method.clone(), req.session_id, req.params)
889                                    {
890                                        self.pending_commands.insert(
891                                            call_id,
892                                            (PendingRequest::Navigate(nav_id), req.method, now),
893                                        );
894                                    }
895                                    self.navigations.insert(
896                                        nav_id,
897                                        NavigationRequest::Navigate(
898                                            target.target_id().clone(),
899                                            NavigationInProgress::new(tx),
900                                        ),
901                                    );
902                                } else if let Ok(call_id) = ws_submit!(
903                                    msg.method.clone(),
904                                    msg.session_id.map(Into::into),
905                                    msg.params
906                                ) {
907                                    // `target` is in scope here, so bind
908                                    // the pending command to its target_id
909                                    // directly.
910                                    let target_id = Some(target.target_id().clone());
911                                    self.pending_commands.insert(
912                                        call_id,
913                                        (
914                                            PendingRequest::ExternalCommand {
915                                                tx: msg.sender,
916                                                target_id,
917                                            },
918                                            msg.method,
919                                            now,
920                                        ),
921                                    );
922                                }
923                            }
924                            TargetEvent::NavigationRequest(nav_id, req) => {
925                                if let Ok(call_id) =
926                                    ws_submit!(req.method.clone(), req.session_id, req.params)
927                                {
928                                    self.pending_commands.insert(
929                                        call_id,
930                                        (PendingRequest::Navigate(nav_id), req.method, now),
931                                    );
932                                }
933                            }
934                            TargetEvent::NavigationResult(res) => {
935                                self.on_navigation_lifecycle_completed(res);
936                            }
937                            TargetEvent::BytesConsumed(n) => {
938                                if let Some(rem) = self.remaining_bytes.as_mut() {
939                                    *rem = rem.saturating_sub(n);
940                                    if *rem == 0 {
941                                        self.budget_exhausted = true;
942                                    }
943                                }
944                            }
945                        }
946                    }
947
948                    // Flush event listeners (no Context needed).
949                    target.event_listeners_mut().flush();
950
951                    self.targets.insert(id, target);
952                    self.target_ids.push(target_id);
953                }
954            }
955
956            // Flush handler-level event listeners.
957            self.event_listeners.flush();
958
959            if self.budget_exhausted {
960                for t in self.targets.values_mut() {
961                    t.network_manager.set_block_all(true);
962                }
963            }
964
965            if self.closing {
966                break Ok(());
967            }
968
969            // 2. Multiplex all event sources via tokio::select!
970            tokio::select! {
971                msg = self.from_browser.recv() => {
972                    match msg {
973                        Some(msg) => {
974                            match msg {
975                                HandlerMessage::Command(cmd) => {
976                                    // See `submit_external_command` for
977                                    // the session_id → target_id resolve.
978                                    let target_id = cmd
979                                        .session_id
980                                        .as_ref()
981                                        .and_then(|sid| self.sessions.get(sid.as_ref()))
982                                        .map(|s| s.target_id().clone());
983                                    if let Ok(call_id) = ws_submit!(
984                                        cmd.method.clone(),
985                                        cmd.session_id.map(Into::into),
986                                        cmd.params
987                                    ) {
988                                        self.pending_commands.insert(
989                                            call_id,
990                                            (
991                                                PendingRequest::ExternalCommand {
992                                                    tx: cmd.sender,
993                                                    target_id,
994                                                },
995                                                cmd.method,
996                                                now,
997                                            ),
998                                        );
999                                    }
1000                                }
1001                                HandlerMessage::FetchTargets(tx) => {
1002                                    let msg = TARGET_PARAMS_ID.clone();
1003                                    if let Ok(call_id) = ws_submit!(msg.0.clone(), None, msg.1) {
1004                                        self.pending_commands.insert(
1005                                            call_id,
1006                                            (PendingRequest::GetTargets(tx), msg.0, now),
1007                                        );
1008                                    }
1009                                }
1010                                HandlerMessage::CloseBrowser(tx) => {
1011                                    let close_msg = CLOSE_PARAMS_ID.clone();
1012                                    if let Ok(call_id) = ws_submit!(close_msg.0.clone(), None, close_msg.1) {
1013                                        self.pending_commands.insert(
1014                                            call_id,
1015                                            (PendingRequest::CloseBrowser(tx), close_msg.0, now),
1016                                        );
1017                                    }
1018                                }
1019                                HandlerMessage::CreatePage(params, tx) => {
1020                                    if let Some(ref id) = params.browser_context_id {
1021                                        self.browser_contexts.insert(BrowserContext::from(id.clone()));
1022                                    }
1023                                    self.create_page_async(params, tx, &mut alloc_call_id, &ws_tx, now);
1024                                }
1025                                HandlerMessage::GetPages(tx) => {
1026                                    let pages: Vec<_> = self.targets.values_mut()
1027                                        .filter(|p| p.is_page())
1028                                        .filter_map(|target| target.get_or_create_page())
1029                                        .map(|page| Page::from(page.clone()))
1030                                        .collect();
1031                                    let _ = tx.send(pages);
1032                                }
1033                                HandlerMessage::InsertContext(ctx) => {
1034                                    if self.default_browser_context.id().is_none() {
1035                                        self.default_browser_context = ctx.clone();
1036                                    }
1037                                    self.browser_contexts.insert(ctx);
1038                                }
1039                                HandlerMessage::DisposeContext(ctx) => {
1040                                    self.browser_contexts.remove(&ctx);
1041                                    self.attached_targets.retain(|tid| {
1042                                        self.targets.get(tid)
1043                                            .and_then(|t| t.browser_context_id())
1044                                            .map(|id| Some(id) != ctx.id())
1045                                            .unwrap_or(true)
1046                                    });
1047                                    self.closing = true;
1048                                }
1049                                HandlerMessage::GetPage(target_id, tx) => {
1050                                    let page = self.targets.get_mut(&target_id)
1051                                        .and_then(|target| target.get_or_create_page())
1052                                        .map(|page| Page::from(page.clone()));
1053                                    let _ = tx.send(page);
1054                                }
1055                                HandlerMessage::AddEventListener(req) => {
1056                                    self.event_listeners.add_listener(req);
1057                                }
1058                            }
1059                        }
1060                        None => break Ok(()), // browser handle dropped
1061                    }
1062                }
1063
1064                frame = ws_reader.next_message() => {
1065                    match frame {
1066                        Some(Ok(boxed_msg)) => match *boxed_msg {
1067                            Message::Response(resp) => {
1068                                self.on_response(resp);
1069                            }
1070                            Message::Event(ev) => {
1071                                self.on_event(ev);
1072                            }
1073                        },
1074                        Some(Err(err)) => {
1075                            tracing::error!("WS Connection error: {:?}", err);
1076                            if let CdpError::Ws(ref ws_error) = err {
1077                                match ws_error {
1078                                    tungstenite::Error::AlreadyClosed => break Ok(()),
1079                                    tungstenite::Error::Protocol(detail)
1080                                        if detail == &ProtocolError::ResetWithoutClosingHandshake =>
1081                                    {
1082                                        break Ok(());
1083                                    }
1084                                    _ => break Err(err),
1085                                }
1086                            } else {
1087                                break Err(err);
1088                            }
1089                        }
1090                        None => break Ok(()), // WS closed
1091                    }
1092                }
1093
1094                _ = page_wake.notified() => {
1095                    // A page sent a message — loop back to drain targets.
1096                }
1097
1098                _ = evict_timer.tick() => {
1099                    self.evict_timed_out_commands(now);
1100                    for t in self.targets.values_mut() {
1101                        t.network_manager.evict_stale_entries(now);
1102                        t.frame_manager_mut().evict_stale_context_ids();
1103                    }
1104                }
1105
1106                result = &mut writer_handle => {
1107                    // WS writer exited — propagate error or break.
1108                    match result {
1109                        Ok(Ok(())) => break Ok(()),
1110                        Ok(Err(e)) => break Err(e),
1111                        Err(e) => break Err(CdpError::msg(format!("WS writer panicked: {e}"))),
1112                    }
1113                }
1114            }
1115        };
1116
1117        // ---- graceful shutdown ----
1118        //
1119        // Drop the WS command sender so the writer task's `rx.recv()`
1120        // returns `None`. The writer drains any queued commands, sends a
1121        // WebSocket Close frame to Chrome, and exits. For remote browsers
1122        // this is the only mechanism that closes the WS — there's no child
1123        // process whose death would close the socket.
1124        drop(ws_tx);
1125
1126        // Wait briefly for the writer to send the Close frame. If it's
1127        // already done (e.g. exited via the writer-handle select arm),
1128        // skip the wait. Polling a finished `JoinHandle` again would
1129        // panic.
1130        if !writer_handle.is_finished() {
1131            let _ = tokio::time::timeout(std::time::Duration::from_millis(500), &mut writer_handle)
1132                .await;
1133            if !writer_handle.is_finished() {
1134                writer_handle.abort();
1135            }
1136        }
1137
1138        // Reader may be parked on `stream.next().await` waiting for
1139        // frames from Chrome. Its output channel receiver (`ws_reader`)
1140        // is dropped at function exit, so there is no consumer either
1141        // way — abort directly rather than waiting for the remote to
1142        // ack the Close frame.
1143        reader_handle.abort();
1144
1145        run_result
1146    }
1147
1148    /// `create_page` variant for the `run()` path that submits via `ws_tx`.
1149    fn create_page_async(
1150        &mut self,
1151        params: CreateTargetParams,
1152        tx: OneshotSender<Result<Page>>,
1153        alloc_call_id: &mut impl FnMut() -> chromiumoxide_types::CallId,
1154        ws_tx: &tokio::sync::mpsc::Sender<chromiumoxide_types::MethodCall>,
1155        now: std::time::Instant,
1156    ) {
1157        let about_blank = params.url == "about:blank";
1158        let http_check =
1159            !about_blank && params.url.starts_with("http") || params.url.starts_with("file://");
1160
1161        if about_blank || http_check {
1162            let method = params.identifier();
1163            match serde_json::to_value(params) {
1164                Ok(params) => {
1165                    let id = alloc_call_id();
1166                    let call = chromiumoxide_types::MethodCall {
1167                        id,
1168                        method: method.clone(),
1169                        session_id: None,
1170                        params,
1171                    };
1172                    match ws_tx.try_send(call) {
1173                        Ok(()) => {
1174                            self.pending_commands
1175                                .insert(id, (PendingRequest::CreateTarget(tx), method, now));
1176                        }
1177                        Err(_) => {
1178                            let _ = tx
1179                                .send(Err(CdpError::msg("WS command channel full or closed")))
1180                                .ok();
1181                        }
1182                    }
1183                }
1184                Err(err) => {
1185                    let _ = tx.send(Err(err.into())).ok();
1186                }
1187            }
1188        } else {
1189            let _ = tx.send(Err(CdpError::NotFound)).ok();
1190        }
1191    }
1192
1193    /// Run the handler with one task per attached page (parallel handler).
1194    ///
1195    /// Opt-in via the `parallel-handler` Cargo feature. The single-task
1196    /// `Handler::run()` path is unchanged. See `src/handler/parallel/mod.rs`
1197    /// for the architectural notes and current scope limits.
1198    #[cfg(feature = "parallel-handler")]
1199    pub async fn run_parallel(mut self) -> Result<()> {
1200        // Reuse the existing setup that `run()` did inline: split the WS
1201        // connection, kick the boot `Target.setDiscoverTargets` command,
1202        // and hand everything to the Router.
1203        let conn = self
1204            .conn
1205            .take()
1206            .ok_or_else(|| CdpError::msg("Handler::run_parallel() called with no connection"))?;
1207        let async_conn = conn.into_async();
1208
1209        // The boot command has already been pushed by `Handler::new`; it
1210        // sits at call_id `next_id - 1`.
1211        let next_id = async_conn.next_id;
1212        let boot_call_id = chromiumoxide_types::CallId::new(next_id.saturating_sub(1));
1213        let boot_method = DISCOVER_ID.0.clone();
1214
1215        let router = parallel::Router::new(
1216            self.config,
1217            self.default_browser_context,
1218            self.from_browser,
1219            async_conn.reader,
1220            async_conn.cmd_tx,
1221            boot_call_id,
1222            boot_method,
1223            next_id,
1224        );
1225        let result = router.run().await;
1226
1227        // Make sure the writer drains and the reader task exits cleanly.
1228        async_conn.writer_handle.abort();
1229        async_conn.reader_handle.abort();
1230
1231        result
1232    }
1233}
1234
1235impl Stream for Handler {
1236    type Item = Result<()>;
1237
1238    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
1239        // Budgets prevent a single chatty target or WS flood from
1240        // starving other futures on the runtime. Mirror the caps
1241        // used in `Handler::run()`; on exhaustion, self-wake and
1242        // return Pending so the executor gets a chance to schedule
1243        // other work before we resume.
1244        const BROWSER_MSG_BUDGET: usize = 128;
1245        const PER_TARGET_DRAIN_BUDGET: usize = 128;
1246        const WS_MSG_BUDGET: usize = 512;
1247
1248        let pin = self.get_mut();
1249
1250        let mut dispose = false;
1251        let mut budget_hit = false;
1252
1253        let now = Instant::now();
1254
1255        loop {
1256            // temporary pinning of the browser receiver should be safe as we are pinning
1257            // through the already pinned self. with the receivers we can also
1258            // safely ignore exhaustion as those are fused.
1259            let mut browser_msgs = 0usize;
1260            while let Poll::Ready(Some(msg)) = pin.from_browser.poll_recv(cx) {
1261                match msg {
1262                    HandlerMessage::Command(cmd) => {
1263                        pin.submit_external_command(cmd, now)?;
1264                    }
1265                    HandlerMessage::FetchTargets(tx) => {
1266                        pin.submit_fetch_targets(tx, now);
1267                    }
1268                    HandlerMessage::CloseBrowser(tx) => {
1269                        pin.submit_close(tx, now);
1270                    }
1271                    HandlerMessage::CreatePage(params, tx) => {
1272                        if let Some(ref id) = params.browser_context_id {
1273                            pin.browser_contexts
1274                                .insert(BrowserContext::from(id.clone()));
1275                        }
1276                        pin.create_page(params, tx);
1277                    }
1278                    HandlerMessage::GetPages(tx) => {
1279                        let pages: Vec<_> = pin
1280                            .targets
1281                            .values_mut()
1282                            .filter(|p: &&mut Target| p.is_page())
1283                            .filter_map(|target| target.get_or_create_page())
1284                            .map(|page| Page::from(page.clone()))
1285                            .collect();
1286                        let _ = tx.send(pages);
1287                    }
1288                    HandlerMessage::InsertContext(ctx) => {
1289                        if pin.default_browser_context.id().is_none() {
1290                            pin.default_browser_context = ctx.clone();
1291                        }
1292                        pin.browser_contexts.insert(ctx);
1293                    }
1294                    HandlerMessage::DisposeContext(ctx) => {
1295                        pin.browser_contexts.remove(&ctx);
1296                        pin.attached_targets.retain(|tid| {
1297                            pin.targets
1298                                .get(tid)
1299                                .and_then(|t| t.browser_context_id()) // however you expose it
1300                                .map(|id| Some(id) != ctx.id())
1301                                .unwrap_or(true)
1302                        });
1303                        pin.closing = true;
1304                        dispose = true;
1305                    }
1306                    HandlerMessage::GetPage(target_id, tx) => {
1307                        let page = pin
1308                            .targets
1309                            .get_mut(&target_id)
1310                            .and_then(|target| target.get_or_create_page())
1311                            .map(|page| Page::from(page.clone()));
1312                        let _ = tx.send(page);
1313                    }
1314                    HandlerMessage::AddEventListener(req) => {
1315                        pin.event_listeners.add_listener(req);
1316                    }
1317                }
1318                browser_msgs += 1;
1319                if browser_msgs >= BROWSER_MSG_BUDGET {
1320                    budget_hit = true;
1321                    break;
1322                }
1323            }
1324
1325            for n in (0..pin.target_ids.len()).rev() {
1326                let target_id = pin.target_ids.swap_remove(n);
1327
1328                if let Some((id, mut target)) = pin.targets.remove_entry(&target_id) {
1329                    let mut drained = 0usize;
1330                    while let Some(event) = target.poll(cx, now) {
1331                        match event {
1332                            TargetEvent::Request(req) => {
1333                                let _ = pin.submit_internal_command(
1334                                    target.target_id().clone(),
1335                                    req,
1336                                    now,
1337                                );
1338                            }
1339                            TargetEvent::Command(msg) => {
1340                                pin.on_target_message(&mut target, msg, now);
1341                            }
1342                            TargetEvent::NavigationRequest(id, req) => {
1343                                pin.submit_navigation(id, req, now);
1344                            }
1345                            TargetEvent::NavigationResult(res) => {
1346                                pin.on_navigation_lifecycle_completed(res)
1347                            }
1348                            TargetEvent::BytesConsumed(n) => {
1349                                if let Some(rem) = pin.remaining_bytes.as_mut() {
1350                                    *rem = rem.saturating_sub(n);
1351                                    if *rem == 0 {
1352                                        pin.budget_exhausted = true;
1353                                    }
1354                                }
1355                            }
1356                        }
1357                        drained += 1;
1358                        if drained >= PER_TARGET_DRAIN_BUDGET {
1359                            budget_hit = true;
1360                            break;
1361                        }
1362                    }
1363
1364                    // poll the target's event listeners
1365                    target.event_listeners_mut().poll(cx);
1366
1367                    pin.targets.insert(id, target);
1368                    pin.target_ids.push(target_id);
1369                }
1370            }
1371
1372            // poll the handler-level event listeners once per iteration,
1373            // not once per target.
1374            pin.event_listeners_mut().poll(cx);
1375
1376            let mut done = true;
1377
1378            // Read WS messages into a temporary buffer so the conn borrow
1379            // is released before we process them (which needs &mut pin).
1380            let mut ws_msgs = Vec::new();
1381            let mut ws_err = None;
1382            {
1383                let Some(conn) = pin.conn.as_mut() else {
1384                    return Poll::Ready(Some(Err(CdpError::msg(
1385                        "connection consumed by Handler::run()",
1386                    ))));
1387                };
1388                while let Poll::Ready(Some(ev)) = Pin::new(&mut *conn).poll_next(cx) {
1389                    match ev {
1390                        Ok(msg) => ws_msgs.push(msg),
1391                        Err(err) => {
1392                            ws_err = Some(err);
1393                            break;
1394                        }
1395                    }
1396                    if ws_msgs.len() >= WS_MSG_BUDGET {
1397                        budget_hit = true;
1398                        break;
1399                    }
1400                }
1401            }
1402
1403            for boxed_msg in ws_msgs {
1404                match *boxed_msg {
1405                    Message::Response(resp) => {
1406                        pin.on_response(resp);
1407                        if pin.closing {
1408                            return Poll::Ready(None);
1409                        }
1410                    }
1411                    Message::Event(ev) => {
1412                        pin.on_event(ev);
1413                    }
1414                }
1415                done = false;
1416            }
1417
1418            if let Some(err) = ws_err {
1419                tracing::error!("WS Connection error: {:?}", err);
1420                if let CdpError::Ws(ref ws_error) = err {
1421                    match ws_error {
1422                        Error::AlreadyClosed => {
1423                            pin.closing = true;
1424                            dispose = true;
1425                        }
1426                        Error::Protocol(detail)
1427                            if detail == &ProtocolError::ResetWithoutClosingHandshake =>
1428                        {
1429                            pin.closing = true;
1430                            dispose = true;
1431                        }
1432                        _ => return Poll::Ready(Some(Err(err))),
1433                    }
1434                } else {
1435                    return Poll::Ready(Some(Err(err)));
1436                }
1437            }
1438
1439            if pin.evict_command_timeout.poll_ready(cx) {
1440                // evict all commands that timed out
1441                pin.evict_timed_out_commands(now);
1442                // evict stale network race-condition buffers and
1443                // orphaned context_ids / frame entries
1444                for t in pin.targets.values_mut() {
1445                    t.network_manager.evict_stale_entries(now);
1446                    t.frame_manager_mut().evict_stale_context_ids();
1447                }
1448            }
1449
1450            if pin.budget_exhausted {
1451                for t in pin.targets.values_mut() {
1452                    t.network_manager.set_block_all(true);
1453                }
1454            }
1455
1456            if dispose {
1457                return Poll::Ready(None);
1458            }
1459
1460            if budget_hit {
1461                // yield to the scheduler; self-wake so the remaining
1462                // work resumes on the next tick without waiting for
1463                // a WS event.
1464                cx.waker().wake_by_ref();
1465                return Poll::Pending;
1466            }
1467
1468            if done {
1469                // no events/responses were read from the websocket
1470                return Poll::Pending;
1471            }
1472        }
1473    }
1474}
1475
1476/// How to configure the handler
1477#[derive(Debug, Clone)]
1478pub struct HandlerConfig {
1479    /// Whether the `NetworkManager`s should ignore https errors
1480    pub ignore_https_errors: bool,
1481    /// Window and device settings
1482    pub viewport: Option<Viewport>,
1483    /// Context ids to set from the get go
1484    pub context_ids: Vec<BrowserContextId>,
1485    /// default request timeout to use
1486    pub request_timeout: Duration,
1487    /// Whether to enable request interception
1488    pub request_intercept: bool,
1489    /// Whether to enable cache
1490    pub cache_enabled: bool,
1491    /// Whether to enable Service Workers
1492    pub service_worker_enabled: bool,
1493    /// Whether to ignore visuals.
1494    pub ignore_visuals: bool,
1495    /// Whether to ignore stylesheets.
1496    pub ignore_stylesheets: bool,
1497    /// Whether to ignore Javascript only allowing critical framework or lib based rendering.
1498    pub ignore_javascript: bool,
1499    /// When `ignore_stylesheets` would skip a stylesheet, allow it through if
1500    /// the request URL is first-party (registrable domain matches the page's
1501    /// primary frame). Default `true` so SPAs that load their own CSS via
1502    /// dynamic imports still hydrate. Set `false` for strict block-all.
1503    pub allow_first_party_stylesheets: bool,
1504    /// When a downstream blocker (intercept manager / adblock / blocklists)
1505    /// would skip a script, allow it through if first-party. Default `true`
1506    /// so SPA bootloaders are not collateral damage from third-party rules.
1507    pub allow_first_party_javascript: bool,
1508    /// When `ignore_visuals` would skip an image/media/font, allow it through
1509    /// if the request URL is first-party. Default `true`. Set `false` for
1510    /// strict bandwidth-minimal crawls that drop ALL visuals.
1511    pub allow_first_party_visuals: bool,
1512    /// Whether to ignore analytics.
1513    pub ignore_analytics: bool,
1514    /// Ignore prefetch request. Defaults to true.
1515    pub ignore_prefetch: bool,
1516    /// Whether to ignore ads.
1517    pub ignore_ads: bool,
1518    /// Extra headers.
1519    pub extra_headers: Option<std::collections::HashMap<String, String>>,
1520    /// Only Html.
1521    pub only_html: bool,
1522    /// Created the first target.
1523    pub created_first_target: bool,
1524    /// The network intercept manager.
1525    pub intercept_manager: NetworkInterceptManager,
1526    /// The max bytes to receive.
1527    pub max_bytes_allowed: Option<u64>,
1528    /// Cap on main-frame Document redirect hops (per navigation).
1529    ///
1530    /// `None` disables enforcement (default); `Some(n)` aborts once the chain length
1531    /// exceeds `n` by emitting `net::ERR_TOO_MANY_REDIRECTS` and calling
1532    /// `Page.stopLoading`. Preserves the accumulated `redirect_chain` on the failed
1533    /// request so consumers can inspect it.
1534    pub max_redirects: Option<usize>,
1535    /// Cap on main-frame cross-document navigations per `goto`. Defends against
1536    /// JS / meta-refresh loops that bypass the HTTP redirect guard. `None`
1537    /// disables the guard.
1538    pub max_main_frame_navigations: Option<u32>,
1539    /// Optional per-run/per-site whitelist of URL substrings (scripts/resources).
1540    pub whitelist_patterns: Option<Vec<String>>,
1541    /// Optional per-run/per-site blacklist of URL substrings (scripts/resources).
1542    pub blacklist_patterns: Option<Vec<String>>,
1543    /// Push the interception policy to a capable remote engine once per
1544    /// navigation (`Interception.setPolicy`) so it can resolve block/allow
1545    /// locally instead of round-tripping each `Fetch.requestPaused`. Default
1546    /// `false`; safe to enable against any target (unknown method is ignored).
1547    pub remote_local_policy: bool,
1548    /// Extra ABP/uBO filter rules for the adblock engine.
1549    #[cfg(feature = "adblock")]
1550    pub adblock_filter_rules: Option<Vec<String>>,
1551    /// Capacity of the channel between browser handle and handler.
1552    /// Defaults to 1000.
1553    pub channel_capacity: usize,
1554    /// Capacity of the per-page mpsc channel carrying `TargetMessage`s
1555    /// from each `Page` to the handler.
1556    ///
1557    /// Defaults to `DEFAULT_PAGE_CHANNEL_CAPACITY` (2048) — the previous
1558    /// hard-coded value. Tune upward for pages that burst many commands
1559    /// (heavy `evaluate`/selector use, high-concurrency tasks sharing
1560    /// one page) to avoid pushing each extra command onto the
1561    /// `CommandFuture` async-send fallback path on `TrySendError::Full`.
1562    /// Tune downward to apply back-pressure sooner. Values of `0` are
1563    /// clamped to `1` at channel creation.
1564    pub page_channel_capacity: usize,
1565    /// Number of WebSocket connection retry attempts with exponential backoff.
1566    /// Defaults to 4.
1567    pub connection_retries: u32,
1568}
1569
1570impl Default for HandlerConfig {
1571    fn default() -> Self {
1572        Self {
1573            ignore_https_errors: true,
1574            viewport: Default::default(),
1575            context_ids: Vec::new(),
1576            request_timeout: Duration::from_millis(REQUEST_TIMEOUT),
1577            request_intercept: false,
1578            cache_enabled: true,
1579            service_worker_enabled: true,
1580            ignore_visuals: false,
1581            ignore_stylesheets: false,
1582            ignore_ads: false,
1583            ignore_javascript: false,
1584            allow_first_party_stylesheets: true,
1585            allow_first_party_javascript: true,
1586            allow_first_party_visuals: true,
1587            ignore_analytics: true,
1588            ignore_prefetch: true,
1589            only_html: false,
1590            extra_headers: Default::default(),
1591            created_first_target: false,
1592            intercept_manager: NetworkInterceptManager::Unknown,
1593            max_bytes_allowed: None,
1594            max_redirects: None,
1595            max_main_frame_navigations: None,
1596            whitelist_patterns: None,
1597            blacklist_patterns: None,
1598            remote_local_policy: false,
1599            #[cfg(feature = "adblock")]
1600            adblock_filter_rules: None,
1601            channel_capacity: 4096,
1602            page_channel_capacity: crate::handler::page::DEFAULT_PAGE_CHANNEL_CAPACITY,
1603            connection_retries: crate::conn::DEFAULT_CONNECTION_RETRIES,
1604        }
1605    }
1606}
1607
1608/// Wraps the sender half of the channel who requested a navigation
1609#[derive(Debug)]
1610pub struct NavigationInProgress<T> {
1611    /// Marker to indicate whether a navigation lifecycle has completed
1612    navigated: bool,
1613    /// The response of the issued navigation request
1614    response: Option<Response>,
1615    /// Sender who initiated the navigation request
1616    tx: OneshotSender<T>,
1617}
1618
1619impl<T> NavigationInProgress<T> {
1620    pub(crate) fn new(tx: OneshotSender<T>) -> Self {
1621        Self {
1622            navigated: false,
1623            response: None,
1624            tx,
1625        }
1626    }
1627
1628    /// The response to the cdp request has arrived
1629    pub(crate) fn set_response(&mut self, resp: Response) {
1630        self.response = Some(resp);
1631    }
1632
1633    /// The navigation process has finished, the page finished loading.
1634    pub(crate) fn set_navigated(&mut self) {
1635        self.navigated = true;
1636    }
1637
1638    /// Used by the parallel handler when reconciling Page.navigate response
1639    /// vs. lifecycle completion order — the existing serial handler reads
1640    /// the field directly so these accessors are otherwise inert.
1641    #[cfg_attr(not(feature = "parallel-handler"), allow(dead_code))]
1642    pub(crate) fn is_navigated(&self) -> bool {
1643        self.navigated
1644    }
1645
1646    #[cfg_attr(not(feature = "parallel-handler"), allow(dead_code))]
1647    pub(crate) fn take_response(&mut self) -> Option<Response> {
1648        self.response.take()
1649    }
1650
1651    #[cfg_attr(not(feature = "parallel-handler"), allow(dead_code))]
1652    pub(crate) fn into_tx(self) -> OneshotSender<T> {
1653        self.tx
1654    }
1655}
1656
1657/// Request type for navigation
1658#[derive(Debug)]
1659enum NavigationRequest {
1660    /// Represents a simple `NavigateParams` ("Page.navigate")
1661    Navigate(TargetId, NavigationInProgress<Result<Response>>),
1662    // TODO are there more?
1663}
1664
1665/// Different kind of submitted request submitted from the  `Handler` to the
1666/// `Connection` and being waited on for the response.
1667#[derive(Debug)]
1668enum PendingRequest {
1669    /// A Request to create a new `Target` that results in the creation of a
1670    /// `Page` that represents a browser page.
1671    CreateTarget(OneshotSender<Result<Page>>),
1672    /// A Request to fetch old `Target`s created before connection
1673    GetTargets(OneshotSender<Result<Vec<TargetInfo>>>),
1674    /// A Request to navigate a specific `Target`.
1675    ///
1676    /// Navigation requests are not automatically completed once the response to
1677    /// the raw cdp navigation request (like `NavigateParams`) arrives, but only
1678    /// after the `Target` notifies the `Handler` that the `Page` has finished
1679    /// loading, which comes after the response.
1680    Navigate(NavigationId),
1681    /// A common request received via a channel (`Page`).
1682    ///
1683    /// `target_id` is resolved at submit time from the caller's
1684    /// `session_id` against `self.sessions`, so `on_target_crashed`
1685    /// can cancel in-flight user commands immediately. `None` when
1686    /// the command has no session (browser-level) or was sent
1687    /// before the attach event arrived — those fall back to the
1688    /// normal `request_timeout` eviction.
1689    ExternalCommand {
1690        tx: OneshotSender<Result<Response>>,
1691        target_id: Option<TargetId>,
1692    },
1693    /// Requests that are initiated directly from a `Target` (all the
1694    /// initialization commands).
1695    InternalCommand(TargetId),
1696    // A Request to close the browser.
1697    CloseBrowser(OneshotSender<Result<CloseReturns>>),
1698}
1699
1700/// Events used internally to communicate with the handler, which are executed
1701/// in the background
1702// TODO rename to BrowserMessage
1703#[derive(Debug)]
1704pub(crate) enum HandlerMessage {
1705    CreatePage(CreateTargetParams, OneshotSender<Result<Page>>),
1706    FetchTargets(OneshotSender<Result<Vec<TargetInfo>>>),
1707    InsertContext(BrowserContext),
1708    DisposeContext(BrowserContext),
1709    GetPages(OneshotSender<Vec<Page>>),
1710    Command(CommandMessage),
1711    GetPage(TargetId, OneshotSender<Option<Page>>),
1712    AddEventListener(EventListenerRequest),
1713    CloseBrowser(OneshotSender<Result<CloseReturns>>),
1714}
1715
1716#[cfg(test)]
1717mod tests {
1718    use super::*;
1719    use chromiumoxide_cdp::cdp::browser_protocol::target::{AttachToTargetReturns, TargetInfo};
1720
1721    #[test]
1722    fn attach_to_target_response_sets_session_id_before_event_arrives() {
1723        let info = TargetInfo::builder()
1724            .target_id("target-1".to_string())
1725            .r#type("page")
1726            .title("")
1727            .url("about:blank")
1728            .attached(false)
1729            .can_access_opener(false)
1730            .build()
1731            .expect("target info");
1732        let mut target = Target::new(info, TargetConfig::default(), BrowserContext::default());
1733        let method: MethodId = AttachToTargetParams::IDENTIFIER.into();
1734        let result = serde_json::to_value(AttachToTargetReturns::new("session-1".to_string()))
1735            .expect("attach result");
1736        let resp = Response {
1737            id: CallId::new(1),
1738            result: Some(result),
1739            error: None,
1740        };
1741
1742        maybe_store_attach_session_id(&mut target, &method, &resp);
1743
1744        assert_eq!(
1745            target.session_id().map(AsRef::as_ref),
1746            Some("session-1"),
1747            "attach response should seed the flat session id even before Target.attachedToTarget"
1748        );
1749    }
1750
1751    /// Regression guard: `page_channel_capacity` must default to 2048
1752    /// everywhere, so existing callers see identical behavior to the
1753    /// previous hard-coded value. If this test ever fails, every caller
1754    /// that relied on the implicit 2048-slot channel silently changed.
1755    #[test]
1756    fn page_channel_capacity_defaults_to_2048_across_configs() {
1757        use crate::browser::BrowserConfigBuilder;
1758        use crate::handler::page::DEFAULT_PAGE_CHANNEL_CAPACITY;
1759        use crate::handler::target::TargetConfig;
1760
1761        assert_eq!(DEFAULT_PAGE_CHANNEL_CAPACITY, 2048);
1762        assert_eq!(
1763            HandlerConfig::default().page_channel_capacity,
1764            DEFAULT_PAGE_CHANNEL_CAPACITY,
1765            "HandlerConfig default must match the historical 2048 slot count"
1766        );
1767        assert_eq!(
1768            TargetConfig::default().page_channel_capacity,
1769            DEFAULT_PAGE_CHANNEL_CAPACITY,
1770            "TargetConfig default must match the historical 2048 slot count"
1771        );
1772        // BrowserConfigBuilder default → build a builder (no executable
1773        // check needed: we only inspect the numeric field, not `build()`).
1774        let builder = BrowserConfigBuilder::default();
1775        let bc = format!("{:?}", builder);
1776        assert!(
1777            bc.contains("page_channel_capacity: 2048"),
1778            "BrowserConfigBuilder must default page_channel_capacity to 2048, got: {bc}",
1779        );
1780    }
1781}