Do not throw code owners ISE if cause is DiffNotAvailableException

For internal server errors that occur in the code-owners plugin we throw
a specific RuntimeException (CodeOwnersInternalServerErrorException). If
this exception is thrown we let the user know that there was an internal
server error in the code-owners plugin, instead of just saying that
there was an internal server error. This was done to root cause
exceptions that were caused by the code-owners plugin more quickly to
the code-owners plugin. However not all exceptions that happen in the
code-owners plugin are caused by the code-owners plugin. E.g. if the
code-owners plugin invokes functionality in Gerrit core and a failure
happens there then the exception is caused by Gerrit core and not the
code-owners plugin. So far these false positives were not a big issue
since by looking at the cause detecting them was easy. However lately
there are often failures caused by corrupted data in Git that cause
DiffNotAvailableException's in Gerrit core when trying to get the file
diff for a commit. This is functionality that the code-owners plugin
invokes frequently and hence the code-owners plugin is often affected by
this. In these cases users and also developers investigating the issue
blame the code-owners plugin for the error. This is bad since it delays
investigating the actual root cause. So for this particular case we stop
throwing CodeOwnersInternalServerErrorException, but instead throw
StorageException, so that this becomes a regular Gerrit internal server
error, which is what it is. This doesn't change the stacktrace of the
exception, but only the exception type. So from the stacktrace you can
still see that the error affected the code-owners plugin.

Signed-off-by: Edwin Kempin <ekempin@google.com>
Change-Id: Ie191c7f0137656199b8fa5d19ab49e9bd0344a6f
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java b/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java
index 2bf9bff..a0df99d 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackend.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.plugins.codeowners.backend;
 
+import static com.google.gerrit.plugins.codeowners.backend.CodeOwnersInternalServerErrorException.newInternalServerError;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.annotations.VisibleForTesting;
@@ -113,10 +114,10 @@
             fileName, codeOwnerConfigParser, revWalk, revision, codeOwnerConfigKey);
       }
     } catch (IOException e) {
-      throw new CodeOwnersInternalServerErrorException(
+      throw newInternalServerError(
           String.format("failed to load code owner config %s", codeOwnerConfigKey), e);
     } catch (ConfigInvalidException e) {
-      throw new CodeOwnersInternalServerErrorException(
+      throw newInternalServerError(
           String.format(
               "invalid code owner config file %s (project = %s, branch = %s)",
               codeOwnerConfigKey.filePath(defaultFileName),
@@ -209,7 +210,7 @@
           .call();
     } catch (Exception e) {
       Throwables.throwIfUnchecked(e);
-      throw new CodeOwnersInternalServerErrorException(
+      throw newInternalServerError(
           String.format("failed to upsert code owner config %s", codeOwnerConfigKey), e);
     }
   }
@@ -257,7 +258,7 @@
 
       return codeOwnerConfigFile.getLoadedCodeOwnerConfig();
     } catch (IOException | ConfigInvalidException e) {
-      throw new CodeOwnersInternalServerErrorException(
+      throw newInternalServerError(
           String.format("failed to upsert code owner config %s", codeOwnerConfigKey), e);
     }
   }
@@ -279,7 +280,7 @@
       }
       return metaDataUpdate;
     } catch (Exception e) {
-      throw new CodeOwnersInternalServerErrorException("Failed to create MetaDataUpdate", e);
+      throw newInternalServerError("Failed to create MetaDataUpdate", e);
     } finally {
       metaDataUpdate.close();
     }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
index 97ece59..a04c1ad 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerApprovalCheck.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.gerrit.plugins.codeowners.backend.CodeOwnersInternalServerErrorException.newInternalServerError;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.annotations.VisibleForTesting;
@@ -172,7 +173,7 @@
                           .orElse(null)))
           .collect(toImmutableList());
     } catch (IOException | DiffNotAvailableException e) {
-      throw new CodeOwnersInternalServerErrorException(
+      throw newInternalServerError(
           String.format(
               "failed to compute owned paths of patch set %s for account %d",
               patchSet.id(), accountId.get()),
@@ -474,7 +475,7 @@
       return changeNotes.getChange().getRevertOf() != null
           && pureRevertCache.isPureRevert(changeNotes);
     } catch (BadRequestException e) {
-      throw new CodeOwnersInternalServerErrorException(
+      throw newInternalServerError(
           String.format(
               "failed to check if change %s in project %s is a pure revert",
               changeNotes.getChangeId(), changeNotes.getProjectName()),
@@ -740,7 +741,7 @@
             implicitApprover, reviewerAccountIds, approverAccountIds, absolutePath, reason);
     }
 
-    throw new CodeOwnersInternalServerErrorException(
+    throw newInternalServerError(
         String.format("unknown fallback code owners configured: %s", fallbackCodeOwners));
   }
 
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScanner.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScanner.java
index 81e6ab5..d0bd003 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScanner.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigFileUpdateScanner.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.plugins.codeowners.backend;
 
+import static com.google.gerrit.plugins.codeowners.backend.CodeOwnersInternalServerErrorException.newInternalServerError;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Objects.requireNonNull;
 
@@ -140,7 +141,7 @@
       updateBranch(branchNameKey.branch(), repository, revision, commitId);
       return Optional.of(rw.parseCommit(commitId));
     } catch (IOException e) {
-      throw new CodeOwnersInternalServerErrorException(
+      throw newInternalServerError(
           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 8fb6d09..28dd5e1 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigHierarchy.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigHierarchy.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.plugins.codeowners.backend;
 
 import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.plugins.codeowners.backend.CodeOwnersInternalServerErrorException.newInternalServerError;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.flogger.FluentLogger;
@@ -293,8 +294,7 @@
         logger.atFine().log("code owner config %s not found", metaCodeOwnerConfigKey);
       }
     } catch (IOException e) {
-      throw new CodeOwnersInternalServerErrorException(
-          String.format("failed to read %s", metaCodeOwnerConfigKey), e);
+      throw newInternalServerError(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 20d7a4c..d55ff8f 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScanner.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerConfigScanner.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.plugins.codeowners.backend;
 
 import static com.google.gerrit.plugins.codeowners.backend.CodeOwners.getInvalidCodeOwnerConfigCause;
+import static com.google.gerrit.plugins.codeowners.backend.CodeOwnersInternalServerErrorException.newInternalServerError;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.flogger.FluentLogger;
@@ -162,7 +163,7 @@
         }
       }
     } catch (IOException e) {
-      throw new CodeOwnersInternalServerErrorException(
+      throw newInternalServerError(
           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 254b3a6..20cba7d 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.collect.ImmutableMap.toImmutableMap;
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.gerrit.plugins.codeowners.backend.CodeOwnersInternalServerErrorException.newInternalServerError;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.annotations.VisibleForTesting;
@@ -595,7 +596,7 @@
               });
       return extIdsByEmail;
     } catch (IOException e) {
-      throw new CodeOwnersInternalServerErrorException(
+      throw newInternalServerError(
           String.format("cannot resolve code owner emails: %s", emails), e);
     }
   }
@@ -868,7 +869,7 @@
           return true;
         }
       } catch (PermissionBackendException ex) {
-        throw new CodeOwnersInternalServerErrorException(
+        throw newInternalServerError(
             String.format(
                 "failed to test the %s global capability", GlobalPermission.MODIFY_ACCOUNT),
             ex);
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerSubmitRule.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerSubmitRule.java
index 58a500e..80300d6 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerSubmitRule.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerSubmitRule.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.plugins.codeowners.backend;
 
+import static com.google.gerrit.plugins.codeowners.backend.CodeOwnersInternalServerErrorException.newInternalServerError;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.collect.ImmutableList;
@@ -150,7 +151,7 @@
         logger.atWarning().log("%s", errorMessage);
         return Optional.of(ruleError(errorMessage));
       }
-      throw new CodeOwnersInternalServerErrorException(errorMessage, e);
+      throw newInternalServerError(errorMessage, e);
     }
   }
 
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersInternalServerErrorException.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersInternalServerErrorException.java
index 13b1a9d..580414a 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersInternalServerErrorException.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersInternalServerErrorException.java
@@ -14,21 +14,65 @@
 
 package com.google.gerrit.plugins.codeowners.backend;
 
+import com.google.common.base.Throwables;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.server.patch.DiffNotAvailableException;
+
 /** 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) {
+  /**
+   * Creates a {@link CodeOwnersInternalServerErrorException} to signal an internal server error
+   * caused by an issue in the code-owners plugin.
+   *
+   * @param message the exception message
+   * @return the created exception
+   */
+  public static CodeOwnersInternalServerErrorException newInternalServerError(String message) {
+    return new CodeOwnersInternalServerErrorException(message);
+  }
+
+  /**
+   * Creates a {@link RuntimeException} to signal an internal server error.
+   *
+   * <p>By default it is assumed that the internal server error is caused by an issue in the
+   * code-owners plugin and a {@link CodeOwnersInternalServerErrorException} is returned.
+   *
+   * <p>However for some known causes that are unrelated to code owners a {@link StorageException}
+   * is thrown. This is to avoid that the code-owners plugin is mistakenly assumed to be the cause
+   * of these errors.
+   *
+   * @param message the exception message
+   * @param cause the exception cause
+   * @return the created exception
+   */
+  public static RuntimeException newInternalServerError(String message, Throwable cause) {
+    if (isNonCodeOwnersCause(cause)) {
+      return new StorageException(message, cause);
+    }
+    return new CodeOwnersInternalServerErrorException(message, cause);
+  }
+
+  private CodeOwnersInternalServerErrorException(String message) {
     super(message);
   }
 
-  public CodeOwnersInternalServerErrorException(String message, Throwable cause) {
+  private CodeOwnersInternalServerErrorException(String message, Throwable cause) {
     super(message, cause);
   }
 
   public String getUserVisibleMessage() {
     return USER_MESSAGE;
   }
+
+  private static boolean isNonCodeOwnersCause(Throwable throwable) {
+    return hasCause(DiffNotAvailableException.class, throwable);
+  }
+
+  private static boolean hasCause(Class<?> exceptionClass, Throwable throwable) {
+    return Throwables.getCausalChain(throwable).stream().anyMatch(exceptionClass::isInstance);
+  }
 }
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/TransientCodeOwnerConfigCache.java b/java/com/google/gerrit/plugins/codeowners/backend/TransientCodeOwnerConfigCache.java
index e8a1ab7..b1a38fa 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/TransientCodeOwnerConfigCache.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/TransientCodeOwnerConfigCache.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.plugins.codeowners.backend;
 
+import static com.google.gerrit.plugins.codeowners.backend.CodeOwnersInternalServerErrorException.newInternalServerError;
+
 import com.google.auto.value.AutoValue;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
@@ -124,7 +126,7 @@
       }
       return Optional.of(ref.getObjectId());
     } catch (IOException e) {
-      throw new CodeOwnersInternalServerErrorException(
+      throw newInternalServerError(
           String.format(
               "failed to get revision of branch %s in project %s",
               branchNameKey.shortName(), branchNameKey.project()),
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginProjectConfigSnapshot.java b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginProjectConfigSnapshot.java
index dea86af..f3b6418 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginProjectConfigSnapshot.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/config/CodeOwnersPluginProjectConfigSnapshot.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.plugins.codeowners.backend.config;
 
+import static com.google.gerrit.plugins.codeowners.backend.CodeOwnersInternalServerErrorException.newInternalServerError;
 import static com.google.gerrit.server.project.ProjectCache.illegalState;
 import static java.util.Objects.requireNonNull;
 
@@ -30,7 +31,6 @@
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerReference;
-import com.google.gerrit.plugins.codeowners.backend.CodeOwnersInternalServerErrorException;
 import com.google.gerrit.plugins.codeowners.backend.EnableImplicitApprovals;
 import com.google.gerrit.plugins.codeowners.backend.FallbackCodeOwners;
 import com.google.gerrit.plugins.codeowners.backend.PathExpressions;
@@ -332,7 +332,7 @@
 
       return ImmutableSet.copyOf(exemptedAccounts.values());
     } catch (IOException e) {
-      throw new CodeOwnersInternalServerErrorException(
+      throw newInternalServerError(
           String.format(
               "Failed to resolve exempted users %s on project %s", exemptedUsers, projectName),
           e);
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java b/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
index 56aa627..677e67c 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/AbstractGetCodeOwnersForPath.java
@@ -18,6 +18,7 @@
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static com.google.gerrit.plugins.codeowners.backend.CodeOwnerScore.IS_EXPLICITLY_MENTIONED_SCORING_VALUE;
 import static com.google.gerrit.plugins.codeowners.backend.CodeOwnerScore.NOT_EXPLICITLY_MENTIONED_SCORING_VALUE;
+import static com.google.gerrit.plugins.codeowners.backend.CodeOwnersInternalServerErrorException.newInternalServerError;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
@@ -46,7 +47,6 @@
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerScore;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerScoring;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerScorings;
-import com.google.gerrit.plugins.codeowners.backend.CodeOwnersInternalServerErrorException;
 import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
 import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics;
 import com.google.gerrit.server.account.AccountControl;
@@ -580,7 +580,7 @@
 
       throw new IllegalStateException("unknown account visibility setting: " + accountVisibility);
     } catch (IOException | PermissionBackendException e) {
-      throw new CodeOwnersInternalServerErrorException("failed to get visible users", e);
+      throw newInternalServerError("failed to get visible users", e);
     }
   }
 
diff --git a/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java b/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
index 07828b9..c4fd816 100644
--- a/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
+++ b/java/com/google/gerrit/plugins/codeowners/validation/CodeOwnerConfigValidator.java
@@ -18,6 +18,7 @@
 import static com.google.common.collect.ImmutableList.toImmutableList;
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static com.google.gerrit.plugins.codeowners.backend.CodeOwners.getInvalidCodeOwnerConfigCause;
+import static com.google.gerrit.plugins.codeowners.backend.CodeOwnersInternalServerErrorException.newInternalServerError;
 import static java.util.Objects.requireNonNull;
 
 import com.google.auto.value.AutoValue;
@@ -462,7 +463,7 @@
               "failed to validate code owner config files in revision %s"
                   + " (project = %s, branch = %s)",
               revCommit.getName(), branchNameKey.project(), branchNameKey.branch());
-      throw new CodeOwnersInternalServerErrorException(errorMessage, e);
+      throw newInternalServerError(errorMessage, e);
     }
   }
 
@@ -891,7 +892,7 @@
           .filter(Optional::isPresent)
           .map(Optional::get);
     } catch (IOException e) {
-      throw new CodeOwnersInternalServerErrorException(
+      throw newInternalServerError(
           String.format("Failed to validate imports for %s in ", codeOwnerConfig.key()), e);
     }
   }
@@ -1061,7 +1062,7 @@
           .project(keyOfImportedCodeOwnerConfig.project())
           .test(ProjectPermission.ACCESS);
     } catch (PermissionBackendException e) {
-      throw new CodeOwnersInternalServerErrorException(
+      throw newInternalServerError(
           "failed to check read permission for project of imported code owner config", e);
     }
   }
@@ -1074,7 +1075,7 @@
           .ref(keyOfImportedCodeOwnerConfig.ref())
           .test(RefPermission.READ);
     } catch (PermissionBackendException e) {
-      throw new CodeOwnersInternalServerErrorException(
+      throw newInternalServerError(
           "failed to check read permission for branch of imported code owner config", e);
     }
   }
@@ -1095,8 +1096,7 @@
       return Optional.ofNullable(repo.exactRef(keyOfImportedCodeOwnerConfig.ref()))
           .map(Ref::getObjectId);
     } catch (IOException e) {
-      throw new CodeOwnersInternalServerErrorException(
-          "failed to read revision of import code owner config", e);
+      throw newInternalServerError("failed to read revision of import code owner config", e);
     }
   }
 
diff --git a/java/com/google/gerrit/plugins/codeowners/validation/SkipCodeOwnerConfigValidationPushOption.java b/java/com/google/gerrit/plugins/codeowners/validation/SkipCodeOwnerConfigValidationPushOption.java
index bc7ac6b..4417295 100644
--- a/java/com/google/gerrit/plugins/codeowners/validation/SkipCodeOwnerConfigValidationPushOption.java
+++ b/java/com/google/gerrit/plugins/codeowners/validation/SkipCodeOwnerConfigValidationPushOption.java
@@ -14,11 +14,12 @@
 
 package com.google.gerrit.plugins.codeowners.validation;
 
+import static com.google.gerrit.plugins.codeowners.backend.CodeOwnersInternalServerErrorException.newInternalServerError;
+
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.plugins.codeowners.backend.CodeOwnersInternalServerErrorException;
 import com.google.gerrit.server.git.receive.PluginPushOption;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -103,7 +104,7 @@
           .currentUser()
           .check(skipCodeOwnerConfigValidationCapability.getPermission());
     } catch (PermissionBackendException e) {
-      throw new CodeOwnersInternalServerErrorException(
+      throw newInternalServerError(
           String.format(
               "Failed to check %s capability", SkipCodeOwnerConfigValidationCapability.ID),
           e);
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnersExceptionHookTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnersExceptionHookTest.java
index 1c0f9c1..e1786d3 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnersExceptionHookTest.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnersExceptionHookTest.java
@@ -49,10 +49,14 @@
     assertThat(skipRetryWithTrace(newInvalidPathException())).isTrue();
     assertThat(skipRetryWithTrace(newExceptionWithCause(newInvalidPathException()))).isTrue();
 
-    assertThat(skipRetryWithTrace(new CodeOwnersInternalServerErrorException("msg"))).isFalse();
     assertThat(
             skipRetryWithTrace(
-                newExceptionWithCause(new CodeOwnersInternalServerErrorException("msg"))))
+                CodeOwnersInternalServerErrorException.newInternalServerError("msg")))
+        .isFalse();
+    assertThat(
+            skipRetryWithTrace(
+                newExceptionWithCause(
+                    CodeOwnersInternalServerErrorException.newInternalServerError("msg"))))
         .isFalse();
 
     assertThat(skipRetryWithTrace(new Exception())).isFalse();
@@ -82,7 +86,7 @@
         .containsExactly(invalidPathException.getMessage());
 
     CodeOwnersInternalServerErrorException codeOwnersInternalServerErrorException =
-        new CodeOwnersInternalServerErrorException("msg");
+        CodeOwnersInternalServerErrorException.newInternalServerError("msg");
     assertThat(getUserMessages(codeOwnersInternalServerErrorException))
         .containsExactly(codeOwnersInternalServerErrorException.getUserVisibleMessage());
     assertThat(getUserMessages(newExceptionWithCause(codeOwnersInternalServerErrorException)))
@@ -130,8 +134,12 @@
     assertThat(getStatus(new Exception())).isEmpty();
     assertThat(getStatus(newExceptionWithCause(new Exception()))).isEmpty();
 
-    assertThat(getStatus(new CodeOwnersInternalServerErrorException("msg"))).isEmpty();
-    assertThat(getStatus(newExceptionWithCause(new CodeOwnersInternalServerErrorException("msg"))))
+    assertThat(getStatus(CodeOwnersInternalServerErrorException.newInternalServerError("msg")))
+        .isEmpty();
+    assertThat(
+            getStatus(
+                newExceptionWithCause(
+                    CodeOwnersInternalServerErrorException.newInternalServerError("msg"))))
         .isEmpty();
   }
 
diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnersInternalServerErrorExceptionTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnersInternalServerErrorExceptionTest.java
new file mode 100644
index 0000000..ac1e583
--- /dev/null
+++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnersInternalServerErrorExceptionTest.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2022 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.plugins.codeowners.backend.CodeOwnersInternalServerErrorException.newInternalServerError;
+
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersTest;
+import com.google.gerrit.server.patch.DiffNotAvailableException;
+import org.junit.Test;
+
+/** Tests for {@link CodeOwnersInternalServerErrorException}. */
+public class CodeOwnersInternalServerErrorExceptionTest extends AbstractCodeOwnersTest {
+  @Test
+  public void codeOwnersInternalServerErrorExceptionIsCreatedByDefault() {
+    assertThat(newInternalServerError("foo", new NullPointerException("bar")))
+        .isInstanceOf(CodeOwnersInternalServerErrorException.class);
+    assertThat(
+            newInternalServerError("foo", newExceptionWithCause(new NullPointerException("bar"))))
+        .isInstanceOf(CodeOwnersInternalServerErrorException.class);
+  }
+
+  @Test
+  public void storageExceptionIsCreatedForNonCodeOwnerErrors() {
+    assertThat(newInternalServerError("foo", new DiffNotAvailableException("bar")))
+        .isInstanceOf(StorageException.class);
+    assertThat(
+            newInternalServerError(
+                "foo", newExceptionWithCause(new DiffNotAvailableException("bar"))))
+        .isInstanceOf(StorageException.class);
+  }
+
+  private Exception newExceptionWithCause(Exception cause) {
+    return new Exception("exception1", new Exception("exception2", cause));
+  }
+}