use super::{Client, TransportError, TransportErrorKind}; use crate::tracker::{ TrackerRequestModel, TrackerResponseModel, TrackerScrapeModel, TrackerTransport, }; /// `reqwest`-backed HTTP tracker transport. #[derive(Clone, Debug)] pub struct ReqwestTrackerTransport { /// Shared blocking HTTP client used for announce and scrape requests. client: Client, } impl ReqwestTrackerTransport { /// Creates a tracker transport backed by a default blocking `reqwest` client. /// /// # Errors /// /// Returns an error when the HTTP client cannot be constructed. pub fn new() -> Result { let client = Client::builder() .build() .map_err(|error| tracker_transport_error(TransportErrorKind::Io, error.to_string()))?; Ok(Self { client }) } /// Fetches the raw response bytes for one tracker announce or scrape URL. fn fetch_bytes(&self, url: &str) -> Result, TransportError> { let response = self .client .get(url) .send() .map_err(map_reqwest_tracker_error)?; let status = response.status(); if !status.is_success() { return Err(tracker_transport_error( TransportErrorKind::ProtocolViolation, format!( "tracker request failed with http status {}", status.as_u16() ), )); } response .bytes() .map(Vec::from) .map_err(map_reqwest_tracker_error) } } impl Default for ReqwestTrackerTransport { fn default() -> Self { Self::new().expect("reqwest tracker transport should build") } } impl TrackerTransport for ReqwestTrackerTransport { fn announce( &self, request: &TrackerRequestModel, ) -> Result { let url = request.announce_url().map_err(|error| { tracker_transport_error(TransportErrorKind::ProtocolViolation, error.to_string()) })?; let bytes = self.fetch_bytes(&url)?; TrackerResponseModel::from_announce_bytes(&bytes).map_err(|error| { tracker_transport_error( TransportErrorKind::ProtocolViolation, format!("invalid tracker announce payload: {error}"), ) }) } fn scrape(&self, announce_url: &str) -> Result { let url = super::parsing::tracker_scrape_url(announce_url); let bytes = self.fetch_bytes(&url)?; TrackerResponseModel::from_scrape_bytes(&bytes).map_err(|error| { tracker_transport_error( TransportErrorKind::ProtocolViolation, format!("invalid tracker scrape payload: {error}"), ) }) } } /// Builds a transport error with tracker-specific context already normalized. fn tracker_transport_error(kind: TransportErrorKind, message: impl Into) -> TransportError { TransportError { kind, message: message.into(), source: None, context: None, } } /// Maps a reqwest tracker fetch failure into the transport error model. fn map_reqwest_tracker_error(error: reqwest::Error) -> TransportError { let kind = if error.is_timeout() { TransportErrorKind::Timeout } else if error.is_connect() { TransportErrorKind::NotConnected } else if error.is_decode() { TransportErrorKind::ProtocolViolation } else { TransportErrorKind::Io }; tracker_transport_error(kind, error.to_string()) }