Skip to main content

chromiumoxide/handler/
httpfuture.rs

1use crate::handler::commandfuture::CommandFuture;
2use crate::handler::http::HttpRequest;
3use crate::handler::sender::PageSender;
4use crate::handler::target_message_future::TargetMessageFuture;
5use crate::{ArcHttpRequest, Result};
6use chromiumoxide_cdp::cdp::browser_protocol::page::NavigateReturns;
7use chromiumoxide_types::Command;
8use futures_util::future::{Fuse, FusedFuture};
9use futures_util::FutureExt;
10use pin_project_lite::pin_project;
11use std::future::Future;
12use std::pin::Pin;
13use std::sync::Arc;
14use std::task::{Context, Poll};
15
16type ArcRequest = ArcHttpRequest;
17
18const ERR_ABORTED: &str = "net::ERR_ABORTED";
19
20/// ERR_ABORTED can mean a redirect, download, or second navigate superseded
21/// this navigation; keep waiting for its replacement. HTTP response code
22/// failures can still commit a 4xx/5xx body and fire lifecycle events; navi
23/// appends the status (e.g. " (403)"), so that exception uses a prefix match.
24pub fn navigation_continues(err: &str) -> bool {
25    err == ERR_ABORTED || err.starts_with("net::ERR_HTTP_RESPONSE_CODE_FAILURE")
26}
27
28/// Failure probe for `Page.navigate`: the ack's `errorText`.
29pub fn navigate_error_text(response: &NavigateReturns) -> Option<&str> {
30    response.error_text.as_deref()
31}
32
33pin_project! {
34    /// Executes a command and waits for navigation, unless an optional failure
35    /// probe resolves early with a synthetic failed HTTP request.
36    pub struct HttpFuture<T: Command> {
37        #[pin]
38        command: Fuse<CommandFuture<T>>,
39        #[pin]
40        navigation: TargetMessageFuture<ArcHttpRequest>,
41        // Reads a failure text out of the command response. `None` for the
42        // generic constructor: the future then always waits for navigation.
43        failure_check: Option<fn(&T::Response) -> Option<&str>>,
44        // URL stamped onto the synthetic failed request.
45        url: Option<String>,
46    }
47}
48
49impl<T: Command> HttpFuture<T> {
50    pub fn new(
51        sender: PageSender,
52        command: CommandFuture<T>,
53        request_timeout: std::time::Duration,
54    ) -> Self {
55        Self {
56            command: command.fuse(),
57            navigation: TargetMessageFuture::<T>::wait_for_navigation(sender, request_timeout),
58            failure_check: None,
59            url: None,
60        }
61    }
62
63    pub fn with_failure_check(
64        sender: PageSender,
65        command: CommandFuture<T>,
66        request_timeout: std::time::Duration,
67        failure_check: fn(&T::Response) -> Option<&str>,
68        url: Option<String>,
69    ) -> Self {
70        Self {
71            command: command.fuse(),
72            navigation: TargetMessageFuture::<T>::wait_for_navigation(sender, request_timeout),
73            failure_check: Some(failure_check),
74            url,
75        }
76    }
77}
78
79impl<T> Future for HttpFuture<T>
80where
81    T: Command,
82{
83    type Output = Result<ArcRequest>;
84
85    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
86        let mut this = self.project();
87
88        // 1. First complete command request future
89        // 2. Switch polls navigation
90        if this.command.is_terminated() {
91            this.navigation.poll(cx)
92        } else {
93            match this.command.poll(cx) {
94                Poll::Ready(Ok(command_response)) => {
95                    if let Some(check) = *this.failure_check {
96                        if let Some(err) = check(&command_response.result) {
97                            if !err.is_empty() && !navigation_continues(err) {
98                                let req = HttpRequest {
99                                    failure_text: Some(err.to_owned()),
100                                    is_navigation_request: true,
101                                    url: this.url.take(),
102                                    ..Default::default()
103                                };
104                                return Poll::Ready(Ok(Some(Arc::new(req))));
105                            }
106                        }
107                    }
108                    // Command succeeded — reset the navigation timer so it
109                    // gets a full request_timeout from NOW, not from when
110                    // HttpFuture was constructed, then immediately start
111                    // polling navigation (avoids a full wake round-trip).
112                    this.navigation.as_mut().reset_deadline();
113                    this.navigation.poll(cx)
114                }
115                Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
116                Poll::Pending => Poll::Pending,
117            }
118        }
119    }
120}