use std::sync::Arc;
use crate::client::StatusCode;
use crate::configuration::Configuration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum EngineMethod {
#[default]
Get,
}
#[derive(Debug)]
pub struct EngineRequest<'a> {
pub url: &'a str,
pub method: EngineMethod,
pub configuration: &'a Configuration,
pub only_html: bool,
pub attempt: u32,
pub conditional_headers: Option<&'a [(String, String)]>,
}
#[derive(Debug, Default)]
pub struct EngineResponse {
pub status_code: StatusCode,
pub final_url: Option<String>,
pub headers: reqwest::header::HeaderMap,
#[cfg(feature = "remote_addr")]
pub remote_addr: Option<core::net::SocketAddr>,
#[cfg(feature = "cookies")]
pub response_cookies: Option<reqwest::header::HeaderMap>,
pub body: Vec<u8>,
pub declared_content_length: Option<u64>,
pub anti_bot_tech: Option<crate::page::AntiBotTech>,
pub served: bool,
}
#[derive(Debug, Clone)]
pub enum EngineError {
Timeout,
TlsHandshake,
Dns,
ConnectRefused,
ConnectAborted,
ConnectReset,
AddressUnreachable,
ProxyTunnel,
Body,
Request,
Status(u16),
Other(String),
}
impl EngineError {
pub fn to_status_code(&self) -> StatusCode {
match self {
EngineError::Timeout => *crate::page::CONNECTION_TIMEOUT_ERROR,
EngineError::TlsHandshake => *crate::page::ADDRESS_UNREACHABLE_ERROR,
EngineError::Dns => *crate::page::DNS_RESOLVE_ERROR,
EngineError::ConnectRefused => *crate::page::CONNECTION_REFUSED_ERROR,
EngineError::ConnectAborted => *crate::page::CONNECTION_ABORTED_ERROR,
EngineError::ConnectReset => *crate::page::CONNECTION_RESET_ERROR,
EngineError::AddressUnreachable => *crate::page::ADDRESS_UNREACHABLE_ERROR,
EngineError::ProxyTunnel => *crate::page::UNREACHABLE_REQUEST_ERROR,
EngineError::Body => *crate::page::BODY_DECODE_ERROR,
EngineError::Request => StatusCode::BAD_REQUEST,
EngineError::Status(code) => {
StatusCode::from_u16(*code).unwrap_or(*crate::page::UNKNOWN_STATUS_ERROR)
}
EngineError::Other(_) => *crate::page::UNKNOWN_STATUS_ERROR,
}
}
pub fn is_handshake_failure(&self) -> bool {
matches!(self, EngineError::TlsHandshake)
}
pub fn is_proxy_tunnel(&self) -> bool {
matches!(self, EngineError::ProxyTunnel)
}
}
impl std::fmt::Display for EngineError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EngineError::Other(msg) => write!(f, "engine error: {msg}"),
other => write!(f, "engine error: {other:?}"),
}
}
}
impl std::error::Error for EngineError {}
#[async_trait::async_trait]
pub trait HttpFetchEngine: Send + Sync + 'static {
async fn fetch(&self, req: EngineRequest<'_>) -> Result<EngineResponse, EngineError>;
fn should_fetch(&self, _url: &str) -> bool {
true
}
}
pub type SharedHttpFetchEngine = Arc<dyn HttpFetchEngine>;
#[derive(Copy, Clone)]
pub struct EngineFetchCtx<'a> {
pub engine: &'a SharedHttpFetchEngine,
pub configuration: &'a Configuration,
}
impl<'a> EngineFetchCtx<'a> {
pub fn new(engine: &'a SharedHttpFetchEngine, configuration: &'a Configuration) -> Self {
Self {
engine,
configuration,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn engine_error_status_mapping() {
assert_eq!(EngineError::Timeout.to_status_code().as_u16(), 524);
assert_eq!(EngineError::TlsHandshake.to_status_code().as_u16(), 526);
assert_eq!(EngineError::Dns.to_status_code().as_u16(), 525);
assert_eq!(EngineError::ConnectRefused.to_status_code().as_u16(), 521);
assert_eq!(EngineError::ConnectAborted.to_status_code().as_u16(), 522);
assert_eq!(EngineError::ConnectReset.to_status_code().as_u16(), 523);
assert_eq!(
EngineError::AddressUnreachable.to_status_code().as_u16(),
526
);
assert_eq!(EngineError::ProxyTunnel.to_status_code().as_u16(), 503);
assert_eq!(EngineError::Body.to_status_code().as_u16(), 400);
assert_eq!(EngineError::Request.to_status_code().as_u16(), 400);
assert_eq!(EngineError::Status(200).to_status_code().as_u16(), 200);
assert_eq!(EngineError::Status(429).to_status_code().as_u16(), 429);
assert_eq!(
EngineError::Other("boom".into()).to_status_code().as_u16(),
599
);
}
#[test]
fn engine_error_invalid_status_falls_back() {
assert_eq!(EngineError::Status(999).to_status_code().as_u16(), 999);
assert_eq!(EngineError::Status(0).to_status_code().as_u16(), 599);
}
#[test]
fn handshake_and_tunnel_predicates() {
assert!(EngineError::TlsHandshake.is_handshake_failure());
assert!(!EngineError::Timeout.is_handshake_failure());
assert!(EngineError::ProxyTunnel.is_proxy_tunnel());
assert!(!EngineError::Dns.is_proxy_tunnel());
}
#[test]
fn engine_method_default_is_get() {
assert_eq!(EngineMethod::default(), EngineMethod::Get);
}
struct MockEngine {
accept: bool,
}
#[async_trait::async_trait]
impl HttpFetchEngine for MockEngine {
async fn fetch(&self, req: EngineRequest<'_>) -> Result<EngineResponse, EngineError> {
Ok(EngineResponse {
status_code: StatusCode::OK,
final_url: Some(req.url.to_string()),
headers: reqwest::header::HeaderMap::new(),
body: b"<html>ok</html>".to_vec(),
served: true,
..Default::default()
})
}
fn should_fetch(&self, _url: &str) -> bool {
self.accept
}
}
#[test]
fn mock_engine_is_object_safe_and_shareable() {
let engine: SharedHttpFetchEngine = std::sync::Arc::new(MockEngine { accept: true });
assert!(engine.should_fetch("https://example.com"));
let declined: SharedHttpFetchEngine = std::sync::Arc::new(MockEngine { accept: false });
assert!(!declined.should_fetch("https://example.com"));
}
#[tokio::test]
async fn mock_engine_fetch_returns_body() {
let engine = MockEngine { accept: true };
let cfg = Configuration::default();
let resp = engine
.fetch(EngineRequest {
url: "https://example.com",
method: EngineMethod::Get,
configuration: &cfg,
only_html: true,
attempt: 0,
conditional_headers: None,
})
.await
.expect("engine returns ok");
assert_eq!(resp.status_code, StatusCode::OK);
assert_eq!(resp.body, b"<html>ok</html>");
assert_eq!(resp.final_url.as_deref(), Some("https://example.com"));
assert!(resp.served);
}
}