Skip to content

Commit 35b079f

Browse files
committed
refactor: [#420] decouple tracker API errors from app errors
We need the specifix error requesting the tracker API to include the error in the logs.
1 parent a471a5b commit 35b079f

File tree

3 files changed

+136
-59
lines changed

3 files changed

+136
-59
lines changed

src/errors.rs

+37-6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use hyper::StatusCode;
66

77
use crate::databases::database;
88
use crate::models::torrent::MetadataError;
9+
use crate::tracker::service::TrackerAPIError;
910
use crate::utils::parse_torrent::DecodeTorrentFileError;
1011

1112
pub type ServiceResult<V> = Result<V, ServiceError>;
@@ -84,9 +85,6 @@ pub enum ServiceError {
8485
/// token invalid
8586
TokenInvalid,
8687

87-
#[display(fmt = "Torrent not found.")]
88-
TorrentNotFound,
89-
9088
#[display(fmt = "Uploaded torrent is not valid.")]
9189
InvalidTorrentFile,
9290

@@ -120,9 +118,6 @@ pub enum ServiceError {
120118
#[display(fmt = "This torrent title has already been used.")]
121119
TorrentTitleAlreadyExists,
122120

123-
#[display(fmt = "Sorry, we have an error with our tracker connection.")]
124-
TrackerOffline,
125-
126121
#[display(fmt = "Could not whitelist torrent.")]
127122
WhitelistingError,
128123

@@ -141,6 +136,9 @@ pub enum ServiceError {
141136
#[display(fmt = "Tag name cannot be empty.")]
142137
TagNameEmpty,
143138

139+
#[display(fmt = "Torrent not found.")]
140+
TorrentNotFound,
141+
144142
#[display(fmt = "Category not found.")]
145143
CategoryNotFound,
146144

@@ -149,6 +147,19 @@ pub enum ServiceError {
149147

150148
#[display(fmt = "Database error.")]
151149
DatabaseError,
150+
151+
// Tracker errors
152+
#[display(fmt = "Sorry, we have an error with our tracker connection.")]
153+
TrackerOffline,
154+
155+
#[display(fmt = "Tracker response error. The operation could not be performed.")]
156+
TrackerResponseError,
157+
158+
#[display(fmt = "Tracker unknown response. Unexpected response from tracker. For example, if it can be parsed.")]
159+
TrackerUnknownResponse,
160+
161+
#[display(fmt = "Torrent not found in tracker.")]
162+
TorrentNotFoundInTracker,
152163
}
153164

154165
impl From<sqlx::Error> for ServiceError {
@@ -228,6 +239,23 @@ impl From<DecodeTorrentFileError> for ServiceError {
228239
}
229240
}
230241

242+
impl From<TrackerAPIError> for ServiceError {
243+
fn from(e: TrackerAPIError) -> Self {
244+
eprintln!("{e}");
245+
match e {
246+
TrackerAPIError::TrackerOffline => ServiceError::TrackerOffline,
247+
TrackerAPIError::AddToWhitelistError
248+
| TrackerAPIError::RemoveFromWhitelistError
249+
| TrackerAPIError::RetrieveUserKeyError => ServiceError::TrackerResponseError,
250+
TrackerAPIError::TorrentNotFound => ServiceError::TorrentNotFoundInTracker,
251+
TrackerAPIError::MissingResponseBody | TrackerAPIError::FailedToParseTrackerResponse { body: _ } => {
252+
ServiceError::TrackerUnknownResponse
253+
}
254+
TrackerAPIError::CannotSaveUserKey => ServiceError::DatabaseError,
255+
}
256+
}
257+
}
258+
231259
#[must_use]
232260
pub fn http_status_code_for_service_error(error: &ServiceError) -> StatusCode {
233261
#[allow(clippy::match_same_arms)]
@@ -276,6 +304,9 @@ pub fn http_status_code_for_service_error(error: &ServiceError) -> StatusCode {
276304
ServiceError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR,
277305
ServiceError::CategoryNotFound => StatusCode::NOT_FOUND,
278306
ServiceError::TagNotFound => StatusCode::NOT_FOUND,
307+
ServiceError::TrackerResponseError => StatusCode::INTERNAL_SERVER_ERROR,
308+
ServiceError::TrackerUnknownResponse => StatusCode::INTERNAL_SERVER_ERROR,
309+
ServiceError::TorrentNotFoundInTracker => StatusCode::NOT_FOUND,
279310
}
280311
}
281312

src/services/torrent.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ impl Index {
165165
{
166166
// If the torrent can't be whitelisted somehow, remove the torrent from database
167167
drop(self.torrent_repository.delete(&torrent_id).await);
168-
return Err(e);
168+
return Err(e.into());
169169
}
170170

171171
// Build response
@@ -304,7 +304,8 @@ impl Index {
304304
self.torrent_repository.delete(&torrent_listing.torrent_id).await?;
305305

306306
// Remove info-hash from tracker whitelist
307-
let _ = self
307+
// todo: handle the error when the tracker is offline or not well configured.
308+
let _unused = self
308309
.tracker_service
309310
.remove_info_hash_from_whitelist(info_hash.to_string())
310311
.await;

src/tracker/service.rs

+96-51
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::sync::Arc;
22

3+
use derive_more::{Display, Error};
34
use hyper::StatusCode;
45
use log::error;
56
use reqwest::Response;
@@ -8,10 +9,37 @@ use serde::{Deserialize, Serialize};
89
use super::api::{Client, ConnectionInfo};
910
use crate::config::Configuration;
1011
use crate::databases::database::Database;
11-
use crate::errors::ServiceError;
1212
use crate::models::tracker_key::TrackerKey;
1313
use crate::models::user::UserId;
1414

15+
#[derive(Debug, Display, PartialEq, Eq, Error)]
16+
#[allow(dead_code)]
17+
pub enum TrackerAPIError {
18+
#[display(fmt = "Error with tracker connection.")]
19+
TrackerOffline,
20+
21+
#[display(fmt = "Could not whitelist torrent.")]
22+
AddToWhitelistError,
23+
24+
#[display(fmt = "Could not remove torrent from whitelist.")]
25+
RemoveFromWhitelistError,
26+
27+
#[display(fmt = "Could not retrieve a new user key.")]
28+
RetrieveUserKeyError,
29+
30+
#[display(fmt = "Could not save the newly generated user key into the database.")]
31+
CannotSaveUserKey,
32+
33+
#[display(fmt = "Torrent not found.")]
34+
TorrentNotFound,
35+
36+
#[display(fmt = "Expected body in tracker response, received empty body.")]
37+
MissingResponseBody,
38+
39+
#[display(fmt = "Expected body in tracker response, received empty body.")]
40+
FailedToParseTrackerResponse { body: String },
41+
}
42+
1543
#[derive(Debug, Serialize, Deserialize, PartialEq)]
1644
pub struct TorrentInfo {
1745
pub info_hash: String,
@@ -69,18 +97,18 @@ impl Service {
6997
///
7098
/// Will return an error if the HTTP request failed (for example if the
7199
/// tracker API is offline) or if the tracker API returned an error.
72-
pub async fn whitelist_info_hash(&self, info_hash: String) -> Result<(), ServiceError> {
100+
pub async fn whitelist_info_hash(&self, info_hash: String) -> Result<(), TrackerAPIError> {
73101
let response = self.api_client.whitelist_torrent(&info_hash).await;
74102

75103
match response {
76104
Ok(response) => {
77105
if response.status().is_success() {
78106
Ok(())
79107
} else {
80-
Err(ServiceError::WhitelistingError)
108+
Err(TrackerAPIError::AddToWhitelistError)
81109
}
82110
}
83-
Err(_) => Err(ServiceError::TrackerOffline),
111+
Err(_) => Err(TrackerAPIError::TrackerOffline),
84112
}
85113
}
86114

@@ -90,18 +118,18 @@ impl Service {
90118
///
91119
/// Will return an error if the HTTP request failed (for example if the
92120
/// tracker API is offline) or if the tracker API returned an error.
93-
pub async fn remove_info_hash_from_whitelist(&self, info_hash: String) -> Result<(), ServiceError> {
121+
pub async fn remove_info_hash_from_whitelist(&self, info_hash: String) -> Result<(), TrackerAPIError> {
94122
let response = self.api_client.remove_torrent_from_whitelist(&info_hash).await;
95123

96124
match response {
97125
Ok(response) => {
98126
if response.status().is_success() {
99127
Ok(())
100128
} else {
101-
Err(ServiceError::InternalServerError)
129+
Err(TrackerAPIError::RemoveFromWhitelistError)
102130
}
103131
}
104-
Err(_) => Err(ServiceError::InternalServerError),
132+
Err(_) => Err(TrackerAPIError::TrackerOffline),
105133
}
106134
}
107135

@@ -116,14 +144,14 @@ impl Service {
116144
///
117145
/// Will return an error if the HTTP request to get generated a new
118146
/// user tracker key failed.
119-
pub async fn get_personal_announce_url(&self, user_id: UserId) -> Result<String, ServiceError> {
147+
pub async fn get_personal_announce_url(&self, user_id: UserId) -> Result<String, TrackerAPIError> {
120148
let tracker_key = self.database.get_user_tracker_key(user_id).await;
121149

122150
match tracker_key {
123-
Some(v) => Ok(self.announce_url_with_key(&v)),
151+
Some(tracker_key) => Ok(self.announce_url_with_key(&tracker_key)),
124152
None => match self.retrieve_new_tracker_key(user_id).await {
125-
Ok(v) => Ok(self.announce_url_with_key(&v)),
126-
Err(_) => Err(ServiceError::TrackerOffline),
153+
Ok(new_tracker_key) => Ok(self.announce_url_with_key(&new_tracker_key)),
154+
Err(_) => Err(TrackerAPIError::TrackerOffline),
127155
},
128156
}
129157
}
@@ -134,14 +162,19 @@ impl Service {
134162
///
135163
/// Will return an error if the HTTP request to get torrent info fails or
136164
/// if the response cannot be parsed.
137-
pub async fn get_torrent_info(&self, info_hash: &str) -> Result<TorrentInfo, ServiceError> {
138-
let response = self
139-
.api_client
140-
.get_torrent_info(info_hash)
141-
.await
142-
.map_err(|_| ServiceError::InternalServerError)?;
143-
144-
map_torrent_info_response(response).await
165+
pub async fn get_torrent_info(&self, info_hash: &str) -> Result<TorrentInfo, TrackerAPIError> {
166+
let response = self.api_client.get_torrent_info(info_hash).await;
167+
168+
match response {
169+
Ok(response) => {
170+
if response.status().is_success() {
171+
map_torrent_info_response(response).await
172+
} else {
173+
Err(TrackerAPIError::RemoveFromWhitelistError)
174+
}
175+
}
176+
Err(_) => Err(TrackerAPIError::TrackerOffline),
177+
}
145178
}
146179

147180
/// It builds the announce url appending the user tracker key.
@@ -150,51 +183,59 @@ impl Service {
150183
format!("{}/{}", self.tracker_url, tracker_key.key)
151184
}
152185

153-
/// Issue a new tracker key from tracker and save it in database,
154-
/// tied to a user
155-
async fn retrieve_new_tracker_key(&self, user_id: i64) -> Result<TrackerKey, ServiceError> {
156-
// Request new tracker key from tracker
157-
let response = self
158-
.api_client
159-
.retrieve_new_tracker_key(self.token_valid_seconds)
160-
.await
161-
.map_err(|_| ServiceError::InternalServerError)?;
162-
163-
// Parse tracker key from response
164-
let tracker_key = response
165-
.json::<TrackerKey>()
166-
.await
167-
.map_err(|_| ServiceError::InternalServerError)?;
168-
169-
// Add tracker key to database (tied to a user)
170-
self.database.add_tracker_key(user_id, &tracker_key).await?;
171-
172-
// return tracker key
173-
Ok(tracker_key)
186+
/// Issue a new tracker key from tracker.
187+
async fn retrieve_new_tracker_key(&self, user_id: i64) -> Result<TrackerKey, TrackerAPIError> {
188+
let response = self.api_client.retrieve_new_tracker_key(self.token_valid_seconds).await;
189+
190+
match response {
191+
Ok(response) => {
192+
if response.status().is_success() {
193+
let body = response.text().await.map_err(|_| {
194+
error!("Tracker API response without body");
195+
TrackerAPIError::MissingResponseBody
196+
})?;
197+
198+
// Parse tracker key from response
199+
let tracker_key =
200+
serde_json::from_str(&body).map_err(|_| TrackerAPIError::FailedToParseTrackerResponse { body })?;
201+
202+
// Add tracker key to database (tied to a user)
203+
self.database
204+
.add_tracker_key(user_id, &tracker_key)
205+
.await
206+
.map_err(|_| TrackerAPIError::CannotSaveUserKey)?;
207+
208+
Ok(tracker_key)
209+
} else {
210+
Err(TrackerAPIError::RetrieveUserKeyError)
211+
}
212+
}
213+
Err(_) => Err(TrackerAPIError::TrackerOffline),
214+
}
174215
}
175216
}
176217

177-
async fn map_torrent_info_response(response: Response) -> Result<TorrentInfo, ServiceError> {
218+
async fn map_torrent_info_response(response: Response) -> Result<TorrentInfo, TrackerAPIError> {
178219
if response.status() == StatusCode::NOT_FOUND {
179-
return Err(ServiceError::TorrentNotFound);
220+
return Err(TrackerAPIError::TorrentNotFound);
180221
}
181222

182223
let body = response.text().await.map_err(|_| {
183224
error!("Tracker API response without body");
184-
ServiceError::InternalServerError
225+
TrackerAPIError::MissingResponseBody
185226
})?;
186227

187228
if body == "\"torrent not known\"" {
188229
// todo: temporary fix. the service should return a 404 (StatusCode::NOT_FOUND).
189-
return Err(ServiceError::TorrentNotFound);
230+
return Err(TrackerAPIError::TorrentNotFound);
190231
}
191232

192233
serde_json::from_str(&body).map_err(|e| {
193234
error!(
194235
"Failed to parse torrent info from tracker response. Body: {}, Error: {}",
195236
body, e
196237
);
197-
ServiceError::InternalServerError
238+
TrackerAPIError::FailedToParseTrackerResponse { body }
198239
})
199240
}
200241

@@ -204,16 +245,15 @@ mod tests {
204245
mod getting_the_torrent_info_from_the_tracker {
205246
use hyper::{Response, StatusCode};
206247

207-
use crate::errors::ServiceError;
208-
use crate::tracker::service::{map_torrent_info_response, TorrentInfo};
248+
use crate::tracker::service::{map_torrent_info_response, TorrentInfo, TrackerAPIError};
209249

210250
#[tokio::test]
211251
async fn it_should_return_a_torrent_not_found_response_when_the_tracker_returns_the_current_torrent_not_known_response() {
212252
let tracker_response = Response::new("\"torrent not known\"");
213253

214254
let result = map_torrent_info_response(tracker_response.into()).await.unwrap_err();
215255

216-
assert_eq!(result, ServiceError::TorrentNotFound);
256+
assert_eq!(result, TrackerAPIError::TorrentNotFound);
217257
}
218258

219259
#[tokio::test]
@@ -225,7 +265,7 @@ mod tests {
225265

226266
let result = map_torrent_info_response(tracker_response.into()).await.unwrap_err();
227267

228-
assert_eq!(result, ServiceError::TorrentNotFound);
268+
assert_eq!(result, TrackerAPIError::TorrentNotFound);
229269
}
230270

231271
#[tokio::test]
@@ -266,9 +306,14 @@ mod tests {
266306

267307
let tracker_response = Response::new(invalid_json_body_for_torrent_info);
268308

269-
let result = map_torrent_info_response(tracker_response.into()).await.unwrap_err();
309+
let err = map_torrent_info_response(tracker_response.into()).await.unwrap_err();
270310

271-
assert_eq!(result, ServiceError::InternalServerError);
311+
assert_eq!(
312+
err,
313+
TrackerAPIError::FailedToParseTrackerResponse {
314+
body: invalid_json_body_for_torrent_info.to_string()
315+
}
316+
);
272317
}
273318
}
274319
}

0 commit comments

Comments
 (0)