diff --git a/src/app.rs b/src/app.rs
index 614dda02..353ce274 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -12,7 +12,7 @@ use crate::services::authentication::{DbUserAuthenticationRepository, JsonWebTok
 use crate::services::category::{self, DbCategoryRepository};
 use crate::services::tag::{self, DbTagRepository};
 use crate::services::torrent::{
-    DbTorrentAnnounceUrlRepository, DbTorrentFileRepository, DbTorrentInfoHashRepository, DbTorrentInfoRepository,
+    DbCanonicalInfoHashGroupRepository, DbTorrentAnnounceUrlRepository, DbTorrentFileRepository, DbTorrentInfoRepository,
     DbTorrentListingGenerator, DbTorrentRepository, DbTorrentTagRepository,
 };
 use crate::services::user::{self, DbBannedUserList, DbUserProfileRepository, DbUserRepository};
@@ -68,7 +68,7 @@ pub async fn run(configuration: Configuration, api_version: &Version) -> Running
     let user_authentication_repository = Arc::new(DbUserAuthenticationRepository::new(database.clone()));
     let user_profile_repository = Arc::new(DbUserProfileRepository::new(database.clone()));
     let torrent_repository = Arc::new(DbTorrentRepository::new(database.clone()));
-    let torrent_info_hash_repository = Arc::new(DbTorrentInfoHashRepository::new(database.clone()));
+    let canonical_info_hash_group_repository = Arc::new(DbCanonicalInfoHashGroupRepository::new(database.clone()));
     let torrent_info_repository = Arc::new(DbTorrentInfoRepository::new(database.clone()));
     let torrent_file_repository = Arc::new(DbTorrentFileRepository::new(database.clone()));
     let torrent_announce_url_repository = Arc::new(DbTorrentAnnounceUrlRepository::new(database.clone()));
@@ -93,7 +93,7 @@ pub async fn run(configuration: Configuration, api_version: &Version) -> Running
         user_repository.clone(),
         category_repository.clone(),
         torrent_repository.clone(),
-        torrent_info_hash_repository.clone(),
+        canonical_info_hash_group_repository.clone(),
         torrent_info_repository.clone(),
         torrent_file_repository.clone(),
         torrent_announce_url_repository.clone(),
@@ -137,7 +137,7 @@ pub async fn run(configuration: Configuration, api_version: &Version) -> Running
         user_authentication_repository,
         user_profile_repository,
         torrent_repository,
-        torrent_info_hash_repository,
+        canonical_info_hash_group_repository,
         torrent_info_repository,
         torrent_file_repository,
         torrent_announce_url_repository,
diff --git a/src/common.rs b/src/common.rs
index 09255678..bf16889a 100644
--- a/src/common.rs
+++ b/src/common.rs
@@ -7,7 +7,7 @@ use crate::services::authentication::{DbUserAuthenticationRepository, JsonWebTok
 use crate::services::category::{self, DbCategoryRepository};
 use crate::services::tag::{self, DbTagRepository};
 use crate::services::torrent::{
-    DbTorrentAnnounceUrlRepository, DbTorrentFileRepository, DbTorrentInfoHashRepository, DbTorrentInfoRepository,
+    DbCanonicalInfoHashGroupRepository, DbTorrentAnnounceUrlRepository, DbTorrentFileRepository, DbTorrentInfoRepository,
     DbTorrentListingGenerator, DbTorrentRepository, DbTorrentTagRepository,
 };
 use crate::services::user::{self, DbBannedUserList, DbUserProfileRepository, DbUserRepository};
@@ -34,7 +34,7 @@ pub struct AppData {
     pub user_authentication_repository: Arc<DbUserAuthenticationRepository>,
     pub user_profile_repository: Arc<DbUserProfileRepository>,
     pub torrent_repository: Arc<DbTorrentRepository>,
-    pub torrent_info_hash_repository: Arc<DbTorrentInfoHashRepository>,
+    pub torrent_info_hash_repository: Arc<DbCanonicalInfoHashGroupRepository>,
     pub torrent_info_repository: Arc<DbTorrentInfoRepository>,
     pub torrent_file_repository: Arc<DbTorrentFileRepository>,
     pub torrent_announce_url_repository: Arc<DbTorrentAnnounceUrlRepository>,
@@ -70,7 +70,7 @@ impl AppData {
         user_authentication_repository: Arc<DbUserAuthenticationRepository>,
         user_profile_repository: Arc<DbUserProfileRepository>,
         torrent_repository: Arc<DbTorrentRepository>,
-        torrent_info_hash_repository: Arc<DbTorrentInfoHashRepository>,
+        torrent_info_hash_repository: Arc<DbCanonicalInfoHashGroupRepository>,
         torrent_info_repository: Arc<DbTorrentInfoRepository>,
         torrent_file_repository: Arc<DbTorrentFileRepository>,
         torrent_announce_url_repository: Arc<DbTorrentAnnounceUrlRepository>,
diff --git a/src/databases/database.rs b/src/databases/database.rs
index 84b506a5..45fbdb3f 100644
--- a/src/databases/database.rs
+++ b/src/databases/database.rs
@@ -12,7 +12,7 @@ use crate::models::torrent_file::{DbTorrentInfo, Torrent, TorrentFile};
 use crate::models::torrent_tag::{TagId, TorrentTag};
 use crate::models::tracker_key::TrackerKey;
 use crate::models::user::{User, UserAuthentication, UserCompact, UserId, UserProfile};
-use crate::services::torrent::OriginalInfoHashes;
+use crate::services::torrent::CanonicalInfoHashGroup;
 
 /// Database tables to be truncated when upgrading from v1.0.0 to v2.0.0.
 /// They must be in the correct order to avoid foreign key errors.
@@ -231,19 +231,30 @@ pub trait Database: Sync + Send {
         ))
     }
 
-    /// Returns the list of all infohashes producing the same canonical infohash.
-    ///
-    /// When you upload a torrent the infohash migth change because the Index
-    /// remove the non-standard fields in the `info` dictionary. That makes the
-    /// infohash change. The canonical infohash is the resulting infohash.
-    /// This function returns the original infohashes of a canonical infohash.
+    /// It returns the list of all infohashes producing the same canonical
+    /// infohash.
     ///
     /// If the original infohash was unknown, it returns the canonical infohash.
     ///
-    /// The relationship is 1 canonical infohash -> N original infohashes.
-    async fn get_torrent_canonical_info_hash_group(&self, canonical: &InfoHash) -> Result<OriginalInfoHashes, Error>;
+    /// # Errors
+    ///
+    /// Returns an error is there was a problem with the database.
+    async fn get_torrent_canonical_info_hash_group(&self, canonical: &InfoHash) -> Result<CanonicalInfoHashGroup, Error>;
 
-    async fn insert_torrent_info_hash(&self, original: &InfoHash, canonical: &InfoHash) -> Result<(), Error>;
+    /// It returns the [`CanonicalInfoHashGroup`] the info-hash belongs to, if
+    /// the info-hash belongs to a group. Otherwise, returns `None`.
+    ///
+    /// # Errors
+    ///
+    /// Returns an error is there was a problem with the database.
+    async fn find_canonical_info_hash_for(&self, info_hash: &InfoHash) -> Result<Option<InfoHash>, Error>;
+
+    /// It adds a new info-hash to the canonical info-hash group.
+    ///
+    /// # Errors
+    ///
+    /// Returns an error is there was a problem with the database.
+    async fn add_info_hash_to_canonical_info_hash_group(&self, original: &InfoHash, canonical: &InfoHash) -> Result<(), Error>;
 
     /// Get torrent's info as `DbTorrentInfo` from `torrent_id`.
     async fn get_torrent_info_from_id(&self, torrent_id: i64) -> Result<DbTorrentInfo, Error>;
diff --git a/src/databases/mysql.rs b/src/databases/mysql.rs
index 503d30b5..b3d18ac3 100644
--- a/src/databases/mysql.rs
+++ b/src/databases/mysql.rs
@@ -17,7 +17,7 @@ use crate::models::torrent_file::{DbTorrentAnnounceUrl, DbTorrentFile, DbTorrent
 use crate::models::torrent_tag::{TagId, TorrentTag};
 use crate::models::tracker_key::TrackerKey;
 use crate::models::user::{User, UserAuthentication, UserCompact, UserId, UserProfile};
-use crate::services::torrent::{DbTorrentInfoHash, OriginalInfoHashes};
+use crate::services::torrent::{CanonicalInfoHashGroup, DbTorrentInfoHash};
 use crate::utils::clock;
 use crate::utils::hex::from_bytes;
 
@@ -590,7 +590,10 @@ impl Database for Mysql {
         }
     }
 
-    async fn get_torrent_canonical_info_hash_group(&self, canonical: &InfoHash) -> Result<OriginalInfoHashes, database::Error> {
+    async fn get_torrent_canonical_info_hash_group(
+        &self,
+        canonical: &InfoHash,
+    ) -> Result<CanonicalInfoHashGroup, database::Error> {
         let db_info_hashes = query_as::<_, DbTorrentInfoHash>(
             "SELECT info_hash, canonical_info_hash, original_is_known FROM torrust_torrent_info_hashes WHERE canonical_info_hash = ?",
         )
@@ -607,13 +610,35 @@ impl Database for Mysql {
             })
             .collect();
 
-        Ok(OriginalInfoHashes {
+        Ok(CanonicalInfoHashGroup {
             canonical_info_hash: *canonical,
             original_info_hashes: info_hashes,
         })
     }
 
-    async fn insert_torrent_info_hash(&self, info_hash: &InfoHash, canonical: &InfoHash) -> Result<(), database::Error> {
+    async fn find_canonical_info_hash_for(&self, info_hash: &InfoHash) -> Result<Option<InfoHash>, database::Error> {
+        let maybe_db_torrent_info_hash = query_as::<_, DbTorrentInfoHash>(
+            "SELECT info_hash, canonical_info_hash, original_is_known FROM torrust_torrent_info_hashes WHERE info_hash = ?",
+        )
+        .bind(info_hash.to_hex_string())
+        .fetch_optional(&self.pool)
+        .await
+        .map_err(|err| database::Error::ErrorWithText(err.to_string()))?;
+
+        match maybe_db_torrent_info_hash {
+            Some(db_torrent_info_hash) => Ok(Some(
+                InfoHash::from_str(&db_torrent_info_hash.canonical_info_hash)
+                    .unwrap_or_else(|_| panic!("Invalid info-hash in database: {}", db_torrent_info_hash.canonical_info_hash)),
+            )),
+            None => Ok(None),
+        }
+    }
+
+    async fn add_info_hash_to_canonical_info_hash_group(
+        &self,
+        info_hash: &InfoHash,
+        canonical: &InfoHash,
+    ) -> Result<(), database::Error> {
         query("INSERT INTO torrust_torrent_info_hashes (info_hash, canonical_info_hash, original_is_known) VALUES (?, ?, ?)")
             .bind(info_hash.to_hex_string())
             .bind(canonical.to_hex_string())
diff --git a/src/databases/sqlite.rs b/src/databases/sqlite.rs
index 085b3960..6b2ebbd8 100644
--- a/src/databases/sqlite.rs
+++ b/src/databases/sqlite.rs
@@ -17,7 +17,7 @@ use crate::models::torrent_file::{DbTorrentAnnounceUrl, DbTorrentFile, DbTorrent
 use crate::models::torrent_tag::{TagId, TorrentTag};
 use crate::models::tracker_key::TrackerKey;
 use crate::models::user::{User, UserAuthentication, UserCompact, UserId, UserProfile};
-use crate::services::torrent::{DbTorrentInfoHash, OriginalInfoHashes};
+use crate::services::torrent::{CanonicalInfoHashGroup, DbTorrentInfoHash};
 use crate::utils::clock;
 use crate::utils::hex::from_bytes;
 
@@ -580,7 +580,10 @@ impl Database for Sqlite {
         }
     }
 
-    async fn get_torrent_canonical_info_hash_group(&self, canonical: &InfoHash) -> Result<OriginalInfoHashes, database::Error> {
+    async fn get_torrent_canonical_info_hash_group(
+        &self,
+        canonical: &InfoHash,
+    ) -> Result<CanonicalInfoHashGroup, database::Error> {
         let db_info_hashes = query_as::<_, DbTorrentInfoHash>(
             "SELECT info_hash, canonical_info_hash, original_is_known FROM torrust_torrent_info_hashes WHERE canonical_info_hash = ?",
         )
@@ -597,13 +600,35 @@ impl Database for Sqlite {
             })
             .collect();
 
-        Ok(OriginalInfoHashes {
+        Ok(CanonicalInfoHashGroup {
             canonical_info_hash: *canonical,
             original_info_hashes: info_hashes,
         })
     }
 
-    async fn insert_torrent_info_hash(&self, original: &InfoHash, canonical: &InfoHash) -> Result<(), database::Error> {
+    async fn find_canonical_info_hash_for(&self, info_hash: &InfoHash) -> Result<Option<InfoHash>, database::Error> {
+        let maybe_db_torrent_info_hash = query_as::<_, DbTorrentInfoHash>(
+            "SELECT info_hash, canonical_info_hash, original_is_known FROM torrust_torrent_info_hashes WHERE info_hash = ?",
+        )
+        .bind(info_hash.to_hex_string())
+        .fetch_optional(&self.pool)
+        .await
+        .map_err(|err| database::Error::ErrorWithText(err.to_string()))?;
+
+        match maybe_db_torrent_info_hash {
+            Some(db_torrent_info_hash) => Ok(Some(
+                InfoHash::from_str(&db_torrent_info_hash.canonical_info_hash)
+                    .unwrap_or_else(|_| panic!("Invalid info-hash in database: {}", db_torrent_info_hash.canonical_info_hash)),
+            )),
+            None => Ok(None),
+        }
+    }
+
+    async fn add_info_hash_to_canonical_info_hash_group(
+        &self,
+        original: &InfoHash,
+        canonical: &InfoHash,
+    ) -> Result<(), database::Error> {
         query("INSERT INTO torrust_torrent_info_hashes (info_hash, canonical_info_hash, original_is_known) VALUES (?, ?, ?)")
             .bind(original.to_hex_string())
             .bind(canonical.to_hex_string())
diff --git a/src/services/torrent.rs b/src/services/torrent.rs
index 635e6016..7dce0db1 100644
--- a/src/services/torrent.rs
+++ b/src/services/torrent.rs
@@ -29,7 +29,7 @@ pub struct Index {
     user_repository: Arc<DbUserRepository>,
     category_repository: Arc<DbCategoryRepository>,
     torrent_repository: Arc<DbTorrentRepository>,
-    torrent_info_hash_repository: Arc<DbTorrentInfoHashRepository>,
+    torrent_info_hash_repository: Arc<DbCanonicalInfoHashGroupRepository>,
     torrent_info_repository: Arc<DbTorrentInfoRepository>,
     torrent_file_repository: Arc<DbTorrentFileRepository>,
     torrent_announce_url_repository: Arc<DbTorrentAnnounceUrlRepository>,
@@ -85,7 +85,7 @@ impl Index {
         user_repository: Arc<DbUserRepository>,
         category_repository: Arc<DbCategoryRepository>,
         torrent_repository: Arc<DbTorrentRepository>,
-        torrent_info_hash_repository: Arc<DbTorrentInfoHashRepository>,
+        torrent_info_hash_repository: Arc<DbCanonicalInfoHashGroupRepository>,
         torrent_info_repository: Arc<DbTorrentInfoRepository>,
         torrent_file_repository: Arc<DbTorrentFileRepository>,
         torrent_announce_url_repository: Arc<DbTorrentAnnounceUrlRepository>,
@@ -189,7 +189,7 @@ impl Index {
 
             // Add the new associated original infohash to the canonical one.
             self.torrent_info_hash_repository
-                .add(&original_info_hash, &canonical_info_hash)
+                .add_info_hash_to_canonical_info_hash_group(&original_info_hash, &canonical_info_hash)
                 .await?;
             return Err(ServiceError::CanonicalInfoHashAlreadyExists);
         }
@@ -555,16 +555,24 @@ pub struct DbTorrentInfoHash {
     pub original_is_known: bool,
 }
 
-pub struct DbTorrentInfoHashRepository {
-    database: Arc<Box<dyn Database>>,
-}
-
-pub struct OriginalInfoHashes {
+/// All the infohashes associated to a canonical one.
+///
+/// When you upload a torrent the info-hash migth change because the Index
+/// remove the non-standard fields in the `info` dictionary. That makes the
+/// infohash change. The canonical infohash is the resulting infohash.
+/// This function returns the original infohashes of a canonical infohash.
+///
+/// The relationship is 1 canonical infohash -> N original infohashes.
+pub struct CanonicalInfoHashGroup {
     pub canonical_info_hash: InfoHash,
+    /// The list of original infohashes associated to the canonical one.
     pub original_info_hashes: Vec<InfoHash>,
 }
+pub struct DbCanonicalInfoHashGroupRepository {
+    database: Arc<Box<dyn Database>>,
+}
 
-impl OriginalInfoHashes {
+impl CanonicalInfoHashGroup {
     #[must_use]
     pub fn is_empty(&self) -> bool {
         self.original_info_hashes.is_empty()
@@ -576,7 +584,7 @@ impl OriginalInfoHashes {
     }
 }
 
-impl DbTorrentInfoHashRepository {
+impl DbCanonicalInfoHashGroupRepository {
     #[must_use]
     pub fn new(database: Arc<Box<dyn Database>>) -> Self {
         Self { database }
@@ -587,31 +595,42 @@ impl DbTorrentInfoHashRepository {
     /// # Errors
     ///
     /// This function will return an error there is a database error.
-    pub async fn get_canonical_info_hash_group(&self, info_hash: &InfoHash) -> Result<OriginalInfoHashes, Error> {
+    ///
+    /// # Errors
+    ///
+    /// Returns an error is there was a problem with the database.
+    pub async fn get_canonical_info_hash_group(&self, info_hash: &InfoHash) -> Result<CanonicalInfoHashGroup, Error> {
         self.database.get_torrent_canonical_info_hash_group(info_hash).await
     }
 
-    /// Inserts a new infohash for the torrent. Torrents can be associated to
-    /// different infohashes because the Index might change the original infohash.
-    /// The index track the final infohash used (canonical) and all the original
-    /// ones.
+    /// It returns the list of all infohashes producing the same canonical
+    /// infohash.
+    ///
+    /// If the original infohash was unknown, it returns the canonical infohash.
     ///
     /// # Errors
     ///
-    /// This function will return an error there is a database error.
-    pub async fn add(&self, original_info_hash: &InfoHash, canonical_info_hash: &InfoHash) -> Result<(), Error> {
-        self.database
-            .insert_torrent_info_hash(original_info_hash, canonical_info_hash)
-            .await
+    /// Returns an error is there was a problem with the database.
+    pub async fn find_canonical_info_hash_for(&self, info_hash: &InfoHash) -> Result<Option<InfoHash>, Error> {
+        self.database.find_canonical_info_hash_for(info_hash).await
     }
 
-    /// Deletes the entire torrent in the database.
+    /// It returns the list of all infohashes producing the same canonical
+    /// infohash.
+    ///
+    /// If the original infohash was unknown, it returns the canonical infohash.
     ///
     /// # Errors
     ///
-    /// This function will return an error there is a database error.
-    pub async fn delete(&self, torrent_id: &TorrentId) -> Result<(), Error> {
-        self.database.delete_torrent(*torrent_id).await
+    /// Returns an error is there was a problem with the database.
+    pub async fn add_info_hash_to_canonical_info_hash_group(
+        &self,
+        original_info_hash: &InfoHash,
+        canonical_info_hash: &InfoHash,
+    ) -> Result<(), Error> {
+        self.database
+            .add_info_hash_to_canonical_info_hash_group(original_info_hash, canonical_info_hash)
+            .await
     }
 }
 
diff --git a/src/web/api/v1/contexts/torrent/handlers.rs b/src/web/api/v1/contexts/torrent/handlers.rs
index 6f9c158a..72a14655 100644
--- a/src/web/api/v1/contexts/torrent/handlers.rs
+++ b/src/web/api/v1/contexts/torrent/handlers.rs
@@ -5,7 +5,7 @@ use std::str::FromStr;
 use std::sync::Arc;
 
 use axum::extract::{self, Multipart, Path, Query, State};
-use axum::response::{IntoResponse, Response};
+use axum::response::{IntoResponse, Redirect, Response};
 use axum::Json;
 use serde::Deserialize;
 use uuid::Uuid;
@@ -78,21 +78,25 @@ pub async fn download_torrent_handler(
         return errors::Request::InvalidInfoHashParam.into_response();
     };
 
-    let opt_user_id = match get_optional_logged_in_user(maybe_bearer_token, app_data.clone()).await {
-        Ok(opt_user_id) => opt_user_id,
-        Err(error) => return error.into_response(),
-    };
+    if let Some(redirect_response) = guard_that_canonical_info_hash_is_used_or_redirect(&app_data, &info_hash).await {
+        redirect_response
+    } else {
+        let opt_user_id = match get_optional_logged_in_user(maybe_bearer_token, app_data.clone()).await {
+            Ok(opt_user_id) => opt_user_id,
+            Err(error) => return error.into_response(),
+        };
 
-    let torrent = match app_data.torrent_service.get_torrent(&info_hash, opt_user_id).await {
-        Ok(torrent) => torrent,
-        Err(error) => return error.into_response(),
-    };
+        let torrent = match app_data.torrent_service.get_torrent(&info_hash, opt_user_id).await {
+            Ok(torrent) => torrent,
+            Err(error) => return error.into_response(),
+        };
 
-    let Ok(bytes) = parse_torrent::encode_torrent(&torrent) else {
-        return ServiceError::InternalServerError.into_response();
-    };
+        let Ok(bytes) = parse_torrent::encode_torrent(&torrent) else {
+            return ServiceError::InternalServerError.into_response();
+        };
 
-    torrent_file_response(bytes, &format!("{}.torrent", torrent.info.name), &torrent.info_hash_hex())
+        torrent_file_response(bytes, &format!("{}.torrent", torrent.info.name), &torrent.info_hash_hex())
+    }
 }
 
 /// It returns a list of torrents matching the search criteria.
@@ -128,14 +132,37 @@ pub async fn get_torrent_info_handler(
         return errors::Request::InvalidInfoHashParam.into_response();
     };
 
-    let opt_user_id = match get_optional_logged_in_user(maybe_bearer_token, app_data.clone()).await {
-        Ok(opt_user_id) => opt_user_id,
-        Err(error) => return error.into_response(),
-    };
+    if let Some(redirect_response) = guard_that_canonical_info_hash_is_used_or_redirect(&app_data, &info_hash).await {
+        redirect_response
+    } else {
+        let opt_user_id = match get_optional_logged_in_user(maybe_bearer_token, app_data.clone()).await {
+            Ok(opt_user_id) => opt_user_id,
+            Err(error) => return error.into_response(),
+        };
+
+        match app_data.torrent_service.get_torrent_info(&info_hash, opt_user_id).await {
+            Ok(torrent_response) => Json(OkResponseData { data: torrent_response }).into_response(),
+            Err(error) => error.into_response(),
+        }
+    }
+}
 
-    match app_data.torrent_service.get_torrent_info(&info_hash, opt_user_id).await {
-        Ok(torrent_response) => Json(OkResponseData { data: torrent_response }).into_response(),
-        Err(error) => error.into_response(),
+async fn guard_that_canonical_info_hash_is_used_or_redirect(app_data: &Arc<AppData>, info_hash: &InfoHash) -> Option<Response> {
+    match app_data
+        .torrent_info_hash_repository
+        .find_canonical_info_hash_for(info_hash)
+        .await
+    {
+        Ok(Some(canonical_info_hash)) => {
+            if canonical_info_hash != *info_hash {
+                return Some(
+                    Redirect::temporary(&format!("/v1/torrent/{}", canonical_info_hash.to_hex_string())).into_response(),
+                );
+            }
+            None
+        }
+        Ok(None) => None,
+        Err(error) => Some(error.into_response()),
     }
 }