Skip to main content

px_native/infrastructure/
native_first.rs

1//! `NativeFirstHandler` — decorator that tries a native handler and
2//! falls back to a browser-based one on error.
3
4use std::sync::Arc;
5
6use async_trait::async_trait;
7use px_errors::AppError;
8use px_pipeline::{ChallengeHandler, HandlerName, HandlerOutcome, HandlerStatus, PageHtml};
9
10pub struct NativeFirstHandler {
11    native: Arc<dyn ChallengeHandler>,
12    fallback: Arc<dyn ChallengeHandler>,
13    name: HandlerName,
14}
15
16impl NativeFirstHandler {
17    pub fn new(native: Arc<dyn ChallengeHandler>, fallback: Arc<dyn ChallengeHandler>) -> Self {
18        Self {
19            native,
20            fallback,
21            name: "perimeterx-native-first",
22        }
23    }
24}
25
26#[async_trait]
27impl ChallengeHandler for NativeFirstHandler {
28    fn name(&self) -> HandlerName {
29        self.name
30    }
31
32    async fn detects(&self, page: &PageHtml) -> Result<bool, AppError> {
33        self.fallback.detects(page).await
34    }
35
36    async fn solve(&self, page: &PageHtml) -> Result<HandlerOutcome, AppError> {
37        match self.native.solve(page).await {
38            Ok(out) if matches!(out.status, HandlerStatus::Solved) => Ok(out),
39            Ok(out) => {
40                tracing::info!(
41                    target: "px_native",
42                    status = ?out.status,
43                    "native handler not solved, falling back"
44                );
45                self.fallback.solve(page).await
46            }
47            Err(e) => {
48                tracing::warn!(
49                    target: "px_native",
50                    error = %e,
51                    "native handler error, falling back"
52                );
53                self.fallback.solve(page).await
54            }
55        }
56    }
57}
58
59#[cfg(test)]
60#[allow(clippy::expect_used, clippy::unwrap_used)]
61mod tests {
62    use super::*;
63    use px_core::CookieJarDelta;
64    use px_pipeline::HandlerMetrics;
65
66    struct SolvedHandler(&'static str);
67    struct FailingHandler;
68
69    #[async_trait]
70    impl ChallengeHandler for SolvedHandler {
71        fn name(&self) -> HandlerName {
72            self.0
73        }
74        async fn detects(&self, _page: &PageHtml) -> Result<bool, AppError> {
75            Ok(true)
76        }
77        async fn solve(&self, _page: &PageHtml) -> Result<HandlerOutcome, AppError> {
78            Ok(HandlerOutcome::solved_with_ua(
79                self.0,
80                CookieJarDelta::default(),
81                Vec::new(),
82                HandlerMetrics::default(),
83                "ua",
84            ))
85        }
86    }
87
88    #[async_trait]
89    impl ChallengeHandler for FailingHandler {
90        fn name(&self) -> HandlerName {
91            "failing"
92        }
93        async fn detects(&self, _page: &PageHtml) -> Result<bool, AppError> {
94            Ok(true)
95        }
96        async fn solve(&self, _page: &PageHtml) -> Result<HandlerOutcome, AppError> {
97            Err(AppError::InternalError("synthetic".into()))
98        }
99    }
100
101    #[tokio::test]
102    async fn prefers_native_when_ok() {
103        let h = NativeFirstHandler::new(
104            Arc::new(SolvedHandler("native")),
105            Arc::new(SolvedHandler("fallback")),
106        );
107        let out = h
108            .solve(&PageHtml::new("https://x/", ""))
109            .await
110            .expect("solve");
111        assert_eq!(out.handler, "native");
112    }
113
114    #[tokio::test]
115    async fn falls_back_on_error() {
116        let h = NativeFirstHandler::new(Arc::new(FailingHandler), Arc::new(SolvedHandler("fb")));
117        let out = h
118            .solve(&PageHtml::new("https://x/", ""))
119            .await
120            .expect("solve");
121        assert_eq!(out.handler, "fb");
122    }
123}