Add an exception to signal internal server errors in the code-owners plugin

With the new exception we return "Internal server error in code-owners
plugin" as message with 500 responses, instead of just "Internal
server error". This way callers can know if an error is caused by an
issue with the code-owners plugin, which speeds up any issue
investigation.

Signed-off-by: Edwin Kempin <ekempin@google.com>
Change-Id: Iee42e118d0984e1e7b2c9328a5cca1f93f1c581d
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java b/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java
index f392696..34f4e0d 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java
@@ -20,7 +20,6 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Project;
-import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
@@ -122,10 +121,10 @@
         }
       }
     } catch (IOException e) {
-      throw new StorageException(
+      throw new CodeOwnersInternalServerErrorException(
           String.format("failed to load code owner config %s", codeOwnerConfigKey), e);
     } catch (ConfigInvalidException e) {
-      throw new StorageException(
+      throw new CodeOwnersInternalServerErrorException(
           String.format(
               "invalid code owner config file %s (project = %s, branch = %s)",
               codeOwnerConfigKey.filePath(defaultFileName),
@@ -207,7 +206,8 @@
           .call();
     } catch (Exception e) {
       Throwables.throwIfUnchecked(e);
-      throw new StorageException(e);
+      throw new CodeOwnersInternalServerErrorException(
+          String.format("failed to upsert code owner config %s", codeOwnerConfigKey), e);
     }
   }
 
@@ -231,7 +231,7 @@
 
       return codeOwnerConfigFile.getLoadedCodeOwnerConfig();
     } catch (IOException | ConfigInvalidException e) {
-      throw new StorageException(
+      throw new CodeOwnersInternalServerErrorException(
           String.format("failed to upsert code owner config %s", codeOwnerConfigKey), e);
     }
   }
@@ -255,7 +255,7 @@
     } catch (Throwable t) {
       metaDataUpdate.close();
       Throwables.throwIfUnchecked(t);
-      throw new StorageException("Failed to create MetaDataUpdate", t);
+      throw new CodeOwnersInternalServerErrorException("Failed to create MetaDataUpdate", t);
     }
   }
 }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
index f980b92..ae354de 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
@@ -31,7 +31,6 @@
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.entities.Project;
-import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.metrics.Timer0;
@@ -142,7 +141,11 @@
           .sorted(comparing(Path::toString))
           .collect(toImmutableList());
     } catch (IOException | PatchListNotAvailableException e) {
-      throw new StorageException(e);
+      throw new CodeOwnersInternalServerErrorException(
+          String.format(
+              "failed to compute owned paths of patch set %s for account %d",
+              patchSet.id(), accountId.get()),
+          e);
     }
   }
 
@@ -346,7 +349,7 @@
       return changeNotes.getChange().getRevertOf() != null
           && pureRevertCache.isPureRevert(changeNotes);
     } catch (BadRequestException e) {
-      throw new StorageException(
+      throw new CodeOwnersInternalServerErrorException(
           String.format(
               "failed to check if change %s in project %s is a pure revert",
               changeNotes.getChangeId(), changeNotes.getProjectName()),
@@ -697,7 +700,7 @@
             absolutePath);
     }
 
-    throw new StorageException(
+    throw new CodeOwnersInternalServerErrorException(
         String.format("unknown fallback code owners configured: %s", fallbackCodeOwners));
   }
 
@@ -784,7 +787,7 @@
       }
       return isProjectOwner;
     } catch (PermissionBackendException e) {
-      throw new StorageException(
+      throw new CodeOwnersInternalServerErrorException(
           String.format(
               "failed to check owner permission of project %s for account %d",
               project.get(), accountId.get()),
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScanner.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScanner.java
index aa11eb8..60d1220 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScanner.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScanner.java
@@ -19,7 +19,6 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.BranchNameKey;
-import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.git.RefUpdateUtil;
 import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -138,7 +137,7 @@
       updateBranch(branchNameKey.branch(), repository, revision, commitId);
       return Optional.of(rw.parseCommit(commitId));
     } catch (IOException e) {
-      throw new StorageException(
+      throw new CodeOwnersInternalServerErrorException(
           String.format(
               "Failed to scan for code owner configs in branch %s of project %s",
               branchNameKey.branch(), branchNameKey.project()),
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigHierarchy.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigHierarchy.java
index 6f5f7bc..473cdc5 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigHierarchy.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigHierarchy.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -200,7 +199,8 @@
         logger.atFine().log("code owner config %s not found", metaCodeOwnerConfigKey);
       }
     } catch (IOException e) {
-      throw new StorageException(String.format("failed to read %s", metaCodeOwnerConfigKey), e);
+      throw new CodeOwnersInternalServerErrorException(
+          String.format("failed to read %s", metaCodeOwnerConfigKey), e);
     }
   }
 }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScanner.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScanner.java
index 95a2885..e28625e 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScanner.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScanner.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
@@ -141,12 +140,12 @@
         CodeOwnerConfig codeOwnerConfig;
         try {
           codeOwnerConfig = treeWalk.getCodeOwnerConfig();
-        } catch (StorageException storageException) {
+        } catch (CodeOwnersInternalServerErrorException codeOwnersInternalServerErrorException) {
           Optional<ConfigInvalidException> configInvalidException =
-              getInvalidConfigCause(storageException);
+              getInvalidConfigCause(codeOwnersInternalServerErrorException);
           if (!configInvalidException.isPresent()) {
             // Propagate any failure that is not related to the contents of the code owner config.
-            throw storageException;
+            throw codeOwnersInternalServerErrorException;
           }
 
           // The code owner config is invalid and cannot be parsed.
@@ -161,7 +160,7 @@
         }
       }
     } catch (IOException e) {
-      throw new StorageException(
+      throw new CodeOwnersInternalServerErrorException(
           String.format(
               "Failed to scan for code owner configs in branch %s of project %s",
               branchNameKey.branch(), branchNameKey.project()),
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
index 4da2a52..5cb187f 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
@@ -24,7 +24,6 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Project;
-import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.metrics.Timer0;
 import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
 import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics;
@@ -357,7 +356,8 @@
     try {
       extIds = externalIds.byEmail(email);
     } catch (IOException e) {
-      throw new StorageException(String.format("cannot resolve code owner email %s", email), e);
+      throw new CodeOwnersInternalServerErrorException(
+          String.format("cannot resolve code owner email %s", email), e);
     }
 
     if (extIds.isEmpty()) {
@@ -481,7 +481,7 @@
                   email, accountState.account().id(), currentUser.get().getLoggableName()));
         }
       } catch (PermissionBackendException e) {
-        throw new StorageException(
+        throw new CodeOwnersInternalServerErrorException(
             String.format(
                 "failed to test the %s global capability", GlobalPermission.MODIFY_ACCOUNT),
             e);
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersExceptionHook.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersExceptionHook.java
index 9f07ea2..c8b3043 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersExceptionHook.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersExceptionHook.java
@@ -63,6 +63,14 @@
       return ImmutableList.of(invalidPathException.get().getMessage());
     }
 
+    // This must be done last since some of the exceptions we handle above may be wrapped in a
+    // CodeOwnersInternalServerErrorException.
+    Optional<CodeOwnersInternalServerErrorException> codeOwnersInternalServerErrorException =
+        getCodeOwnersInternalServerErrorException(throwable);
+    if (codeOwnersInternalServerErrorException.isPresent()) {
+      return ImmutableList.of(codeOwnersInternalServerErrorException.get().getUserVisibleMessage());
+    }
+
     return ImmutableList.of();
   }
 
@@ -76,13 +84,18 @@
     return Optional.empty();
   }
 
+  private static Optional<CodeOwnersInternalServerErrorException>
+      getCodeOwnersInternalServerErrorException(Throwable throwable) {
+    return getCause(CodeOwnersInternalServerErrorException.class, throwable);
+  }
+
   private static boolean isInvalidPluginConfigurationException(Throwable throwable) {
     return getInvalidPluginConfigurationCause(throwable).isPresent();
   }
 
   private static Optional<InvalidPluginConfigurationException> getInvalidPluginConfigurationCause(
       Throwable throwable) {
-    return getInvalidPluginConfigurationCause(InvalidPluginConfigurationException.class, throwable);
+    return getCause(InvalidPluginConfigurationException.class, throwable);
   }
 
   private static boolean isInvalidPathException(Throwable throwable) {
@@ -90,10 +103,10 @@
   }
 
   public static Optional<InvalidPathException> getInvalidPathException(Throwable throwable) {
-    return getInvalidPluginConfigurationCause(InvalidPathException.class, throwable);
+    return getCause(InvalidPathException.class, throwable);
   }
 
-  private static <T extends Throwable> Optional<T> getInvalidPluginConfigurationCause(
+  private static <T extends Throwable> Optional<T> getCause(
       Class<T> exceptionClass, Throwable throwable) {
     return Throwables.getCausalChain(throwable).stream()
         .filter(exceptionClass::isInstance)
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersInternalServerErrorException.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersInternalServerErrorException.java
new file mode 100644
index 0000000..13b1a9d
--- /dev/null
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersInternalServerErrorException.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.plugins.codeowners.backend;
+
+/** Exception signaling an internal server error in the code-owners plugin. */
+public class CodeOwnersInternalServerErrorException extends RuntimeException {
+  private static final long serialVersionUID = 1L;
+
+  private static final String USER_MESSAGE = "Internal server in code-owners plugin";
+
+  public CodeOwnersInternalServerErrorException(String message) {
+    super(message);
+  }
+
+  public CodeOwnersInternalServerErrorException(String message, Throwable cause) {
+    super(message, cause);
+  }
+
+  public String getUserVisibleMessage() {
+    return USER_MESSAGE;
+  }
+}
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java b/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
index d66ef51..990ecb7 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.client.ListAccountsOption;
 import com.google.gerrit.extensions.client.ListOption;
 import com.google.gerrit.extensions.common.AccountVisibility;
@@ -39,6 +38,7 @@
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerResolverResult;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerScore;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerScoring;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnersInternalServerErrorException;
 import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
 import com.google.gerrit.server.account.AccountControl;
 import com.google.gerrit.server.account.AccountDirectory.FillOptions;
@@ -449,7 +449,7 @@
 
       throw new IllegalStateException("unknown account visibility setting: " + accountVisibility);
     } catch (IOException | PermissionBackendException e) {
-      throw new StorageException("failed to get visible users", e);
+      throw new CodeOwnersInternalServerErrorException("failed to get visible users", e);
     }
   }
 
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java
index 7167b28..d01d0fc 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java
@@ -18,7 +18,6 @@
 
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.IdString;
@@ -32,6 +31,7 @@
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerReference;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerResolver;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwners;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnersInternalServerErrorException;
 import com.google.gerrit.plugins.codeowners.backend.FallbackCodeOwners;
 import com.google.gerrit.plugins.codeowners.backend.OptionalResultWithMessages;
 import com.google.gerrit.plugins.codeowners.backend.PathCodeOwners;
@@ -275,7 +275,7 @@
         | AuthException
         | IOException
         | ConfigInvalidException e) {
-      throw new StorageException(
+      throw new CodeOwnersInternalServerErrorException(
           String.format("failed if email %s is owner of project %s", email, projectName.get()), e);
     }
   }
diff --git a/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java b/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
index 2b8d479..74dae62 100644
--- a/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
+++ b/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
@@ -28,7 +28,6 @@
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.Project;
-import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.plugins.codeowners.backend.ChangedFiles;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend;
@@ -37,6 +36,7 @@
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigReference;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerReference;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerResolver;
+import com.google.gerrit.plugins.codeowners.backend.CodeOwnersInternalServerErrorException;
 import com.google.gerrit.plugins.codeowners.backend.PathCodeOwners;
 import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
 import com.google.gerrit.plugins.codeowners.backend.config.InvalidPluginConfigurationException;
@@ -397,7 +397,7 @@
                   + " (project = %s, branch = %s)",
               revCommit.getName(), branchNameKey.project(), branchNameKey.branch());
       logger.atSevere().withCause(e).log(errorMessage);
-      throw new StorageException(errorMessage, e);
+      throw new CodeOwnersInternalServerErrorException(errorMessage, e);
     }
   }
 
@@ -435,7 +435,8 @@
     CodeOwnerConfig codeOwnerConfig;
     try {
       // Load the code owner config. If the code owner config is not parsable this will fail with a
-      // InvalidConfigException (wrapped in a StorageException) that we handle below.
+      // InvalidConfigException (wrapped in a CodeOwnersInternalServerErrorException) that we handle
+      // below.
       CodeOwnerConfig.Key codeOwnerConfigKey =
           createCodeOwnerConfigKey(branchNameKey, changedFile.newPath().get());
       codeOwnerConfig =
@@ -451,13 +452,13 @@
                           String.format(
                               "code owner config %s not found in revision %s",
                               codeOwnerConfigKey, revCommit.name())));
-    } catch (StorageException storageException) {
+    } catch (CodeOwnersInternalServerErrorException codeOwnersInternalServerErrorException) {
       // Loading the code owner config has failed.
       Optional<ConfigInvalidException> configInvalidException =
-          getInvalidConfigCause(storageException);
+          getInvalidConfigCause(codeOwnersInternalServerErrorException);
       if (!configInvalidException.isPresent()) {
         // Propagate any failure that is not related to the contents of the code owner config.
-        throw storageException;
+        throw codeOwnersInternalServerErrorException;
       }
 
       // The exception was caused by a ConfigInvalidException. This means loading the code owner
@@ -481,8 +482,8 @@
     try {
       baseCodeOwnerConfig =
           getBaseCodeOwnerConfig(codeOwnerBackend, branchNameKey, changedFile, revWalk, revCommit);
-    } catch (StorageException storageException) {
-      if (getInvalidConfigCause(storageException).isPresent()) {
+    } catch (CodeOwnersInternalServerErrorException codeOwnersInternalServerErrorException) {
+      if (getInvalidConfigCause(codeOwnersInternalServerErrorException).isPresent()) {
         // The base code owner config is non-parseable. Since the update makes the code owner
         // config parseable, it is a good update even if the code owner config still contains
         // issues. Hence in this case we downgrade all validation errors in the new version to
@@ -493,7 +494,7 @@
       }
 
       // Propagate any exception that was not caused by the content of the code owner config.
-      throw storageException;
+      throw codeOwnersInternalServerErrorException;
     }
 
     // Validate the parsed code owner config.
@@ -529,8 +530,9 @@
   /**
    * Loads and returns the base code owner config if it exists.
    *
-   * <p>Throws a {@link ConfigInvalidException} (wrapped in a {@link StorageException} if the base
-   * code owner config exists, but is not parseable.
+   * <p>Throws a {@link ConfigInvalidException} (wrapped in a {@link
+   * CodeOwnersInternalServerErrorException} if the base code owner config exists, but is not
+   * parseable.
    *
    * @param codeOwnerBackend the code owner backend from which the base code owner config can be
    *     loaded
@@ -614,16 +616,16 @@
         // is introduced by the new commit and we should block uploading it, which we achieve by
         // setting the validation message type to fatal.
         return ValidationMessage.Type.FATAL;
-      } catch (StorageException storageException) {
+      } catch (CodeOwnersInternalServerErrorException codeOwnersInternalServerErrorException) {
         // Loading the base code owner config has failed.
-        if (getInvalidConfigCause(storageException).isPresent()) {
+        if (getInvalidConfigCause(codeOwnersInternalServerErrorException).isPresent()) {
           // The code owner config was already non-parseable before, hence we do not need to
           // block the upload if the code owner config is still non-parseable.
           // Using warning as type means that uploads are not blocked.
           return ValidationMessage.Type.WARNING;
         }
         // Propagate any failure that is not related to the contents of the code owner config.
-        throw storageException;
+        throw codeOwnersInternalServerErrorException;
       }
     }
 
@@ -952,8 +954,8 @@
                 keyOfImportedCodeOwnerConfig.branchNameKey().shortName(),
                 revision.get().name()));
       }
-    } catch (StorageException storageException) {
-      if (getInvalidConfigCause(storageException).isPresent()) {
+    } catch (CodeOwnersInternalServerErrorException codeOwnersInternalServerErrorException) {
+      if (getInvalidConfigCause(codeOwnersInternalServerErrorException).isPresent()) {
         // The imported code owner config is non-parseable.
         return nonResolvableImport(
             project,
@@ -967,7 +969,7 @@
       }
 
       // Propagate any exception that was not caused by the content of the code owner config.
-      throw storageException;
+      throw codeOwnersInternalServerErrorException;
     }
 
     // no issue found
@@ -996,7 +998,7 @@
           .project(keyOfImportedCodeOwnerConfig.project())
           .test(ProjectPermission.ACCESS);
     } catch (PermissionBackendException e) {
-      throw new StorageException(
+      throw new CodeOwnersInternalServerErrorException(
           "failed to check read permission for project of imported code owner config", e);
     }
   }
@@ -1009,7 +1011,7 @@
           .ref(keyOfImportedCodeOwnerConfig.ref())
           .test(RefPermission.READ);
     } catch (PermissionBackendException e) {
-      throw new StorageException(
+      throw new CodeOwnersInternalServerErrorException(
           "failed to check read permission for branch of imported code owner config", e);
     }
   }
@@ -1030,7 +1032,8 @@
       return Optional.ofNullable(repo.exactRef(keyOfImportedCodeOwnerConfig.ref()))
           .map(Ref::getObjectId);
     } catch (IOException e) {
-      throw new StorageException("failed to read revision of import code owner config", e);
+      throw new CodeOwnersInternalServerErrorException(
+          "failed to read revision of import code owner config", e);
     }
   }
 
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackendTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackendTest.java
index fbb6c47..1bbce45 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackendTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackendTest.java
@@ -21,7 +21,6 @@
 
 import com.google.gerrit.acceptance.config.GerritConfig;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersTest;
 import com.google.gerrit.plugins.codeowners.testing.backend.TestCodeOwnerConfigStorage;
 import com.google.gerrit.plugins.codeowners.util.JgitPath;
@@ -194,9 +193,9 @@
   @Test
   public void cannotGetCodeOwnerConfigFromNonExistingRevision() throws Exception {
     CodeOwnerConfig.Key codeOwnerConfigKey = CodeOwnerConfig.Key.create(project, "master", "/");
-    StorageException exception =
+    CodeOwnersInternalServerErrorException exception =
         assertThrows(
-            StorageException.class,
+            CodeOwnersInternalServerErrorException.class,
             () ->
                 codeOwnerBackend.getCodeOwnerConfig(
                     codeOwnerConfigKey,
@@ -428,9 +427,9 @@
     }
 
     // Try to update the code owner config.
-    StorageException exception =
+    CodeOwnersInternalServerErrorException exception =
         assertThrows(
-            StorageException.class,
+            CodeOwnersInternalServerErrorException.class,
             () ->
                 codeOwnerBackend.upsertCodeOwnerConfig(
                     codeOwnerConfigKey,
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnersExceptionHookTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnersExceptionHookTest.java
index c727080..7b73c83 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnersExceptionHookTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnersExceptionHookTest.java
@@ -48,6 +48,12 @@
     assertThat(skipRetryWithTrace(newInvalidPathException())).isTrue();
     assertThat(skipRetryWithTrace(newExceptionWithCause(newInvalidPathException()))).isTrue();
 
+    assertThat(skipRetryWithTrace(new CodeOwnersInternalServerErrorException("msg"))).isFalse();
+    assertThat(
+            skipRetryWithTrace(
+                newExceptionWithCause(new CodeOwnersInternalServerErrorException("msg"))))
+        .isFalse();
+
     assertThat(skipRetryWithTrace(new Exception())).isFalse();
     assertThat(skipRetryWithTrace(newExceptionWithCause(new Exception()))).isFalse();
   }
@@ -73,6 +79,13 @@
     assertThat(getUserMessages(newExceptionWithCause(invalidPathException)))
         .containsExactly(invalidPathException.getMessage());
 
+    CodeOwnersInternalServerErrorException codeOwnersInternalServerErrorException =
+        new CodeOwnersInternalServerErrorException("msg");
+    assertThat(getUserMessages(codeOwnersInternalServerErrorException))
+        .containsExactly(codeOwnersInternalServerErrorException.getUserVisibleMessage());
+    assertThat(getUserMessages(newExceptionWithCause(codeOwnersInternalServerErrorException)))
+        .containsExactly(codeOwnersInternalServerErrorException.getUserVisibleMessage());
+
     assertThat(getUserMessages(new Exception())).isEmpty();
     assertThat(getUserMessages(newExceptionWithCause(new Exception()))).isEmpty();
   }
@@ -99,6 +112,10 @@
 
     assertThat(getStatus(new Exception())).isEmpty();
     assertThat(getStatus(newExceptionWithCause(new Exception()))).isEmpty();
+
+    assertThat(getStatus(new CodeOwnersInternalServerErrorException("msg"))).isEmpty();
+    assertThat(getStatus(newExceptionWithCause(new CodeOwnersInternalServerErrorException("msg"))))
+        .isEmpty();
   }
 
   private boolean skipRetryWithTrace(Exception exception) {