diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -4381,6 +4381,7 @@
       'PhabricatorSubscribableInterface',
       'PhabricatorFlaggableInterface',
       'PhabricatorPolicyInterface',
+      'PhabricatorDestructibleInterface',
     ),
     'PhabricatorFileCommentController' => 'PhabricatorFileController',
     'PhabricatorFileComposeController' => 'PhabricatorFileController',
diff --git a/src/applications/differential/controller/DifferentialChangesetViewController.php b/src/applications/differential/controller/DifferentialChangesetViewController.php
--- a/src/applications/differential/controller/DifferentialChangesetViewController.php
+++ b/src/applications/differential/controller/DifferentialChangesetViewController.php
@@ -347,8 +347,7 @@
       unset($unguard);
     }
 
-    return id(new AphrontRedirectResponse())
-      ->setURI($file->getBestURI());
+    return $file->getRedirectResponse();
   }
 
   private function buildLintInlineComments($changeset) {
diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php
--- a/src/applications/differential/controller/DifferentialRevisionViewController.php
+++ b/src/applications/differential/controller/DifferentialRevisionViewController.php
@@ -918,7 +918,7 @@
         $revision->getPHID());
     unset($unguarded);
 
-    return id(new AphrontRedirectResponse())->setURI($file->getBestURI());
+    return $file->getRedirectResponse();
   }
 
   private function buildTransactions(
diff --git a/src/applications/diffusion/controller/DiffusionBrowseFileController.php b/src/applications/diffusion/controller/DiffusionBrowseFileController.php
--- a/src/applications/diffusion/controller/DiffusionBrowseFileController.php
+++ b/src/applications/diffusion/controller/DiffusionBrowseFileController.php
@@ -887,7 +887,7 @@
 
   private function buildRawResponse($path, $data) {
     $file = $this->loadFileForData($path, $data);
-    return id(new AphrontRedirectResponse())->setURI($file->getBestURI());
+    return $file->getRedirectResponse();
   }
 
   private function buildImageCorpus($file_uri) {
diff --git a/src/applications/diffusion/controller/DiffusionCommitController.php b/src/applications/diffusion/controller/DiffusionCommitController.php
--- a/src/applications/diffusion/controller/DiffusionCommitController.php
+++ b/src/applications/diffusion/controller/DiffusionCommitController.php
@@ -1042,7 +1042,7 @@
         $drequest->getRepository()->getPHID());
     unset($unguarded);
 
-    return id(new AphrontRedirectResponse())->setURI($file->getBestURI());
+    return $file->getRedirectResponse();
   }
 
   private function renderAuditStatusView(array $audit_requests) {
diff --git a/src/applications/files/controller/PhabricatorFileTransformController.php b/src/applications/files/controller/PhabricatorFileTransformController.php
--- a/src/applications/files/controller/PhabricatorFileTransformController.php
+++ b/src/applications/files/controller/PhabricatorFileTransformController.php
@@ -148,22 +148,8 @@
 
     // TODO: We could just delegate to the file view controller instead,
     // which would save the client a roundtrip, but is slightly more complex.
-    $uri = $file->getBestURI();
 
-    // TODO: This is a bit iffy. Sometimes, getBestURI() returns a CDN URI
-    // (if the file is a viewable image) and sometimes a local URI (if not).
-    // For now, just detect which one we got and configure the response
-    // appropriately. In the long run, if this endpoint is served from a CDN
-    // domain, we can't issue a local redirect to an info URI (which is not
-    // present on the CDN domain). We probably never actually issue local
-    // redirects here anyway, since we only ever transform viewable images
-    // right now.
-
-    $is_external = strlen(id(new PhutilURI($uri))->getDomain());
-
-    return id(new AphrontRedirectResponse())
-      ->setIsExternal($is_external)
-      ->setURI($uri);
+    return $file->getRedirectResponse();
   }
 
   private function executePreviewTransform(PhabricatorFile $file, $size) {
diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php
--- a/src/applications/files/storage/PhabricatorFile.php
+++ b/src/applications/files/storage/PhabricatorFile.php
@@ -967,6 +967,25 @@
     return $this;
   }
 
+  public function getRedirectResponse() {
+    $uri = $this->getBestURI();
+
+    // TODO: This is a bit iffy. Sometimes, getBestURI() returns a CDN URI
+    // (if the file is a viewable image) and sometimes a local URI (if not).
+    // For now, just detect which one we got and configure the response
+    // appropriately. In the long run, if this endpoint is served from a CDN
+    // domain, we can't issue a local redirect to an info URI (which is not
+    // present on the CDN domain). We probably never actually issue local
+    // redirects here anyway, since we only ever transform viewable images
+    // right now.
+
+    $is_external = strlen(id(new PhutilURI($uri))->getDomain());
+
+    return id(new AphrontRedirectResponse())
+      ->setIsExternal($is_external)
+      ->setURI($uri);
+  }
+
 
 /* -(  PhabricatorPolicyInterface Implementation  )-------------------------- */