px_native/infrastructure/
native_first.rs1use 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}