Merge "Document when using the code-owners plugin is not recommended"
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/CodeOwnersExceptionHook.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersExceptionHook.java
index c7da676..5eb316c 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersExceptionHook.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersExceptionHook.java
@@ -52,9 +52,7 @@
 
   @Override
   public boolean skipRetryWithTrace(String actionType, String actionName, Throwable throwable) {
-    return isInvalidPluginConfigurationException(throwable)
-        || isInvalidCodeOwnerConfigException(throwable)
-        || isInvalidPathException(throwable);
+    return isCausedByConfigurationError(throwable);
   }
 
   @Override
@@ -102,9 +100,7 @@
 
   @Override
   public Optional<Status> getStatus(Throwable throwable) {
-    if (isInvalidPluginConfigurationException(throwable)
-        || isInvalidCodeOwnerConfigException(throwable)
-        || isInvalidPathException(throwable)) {
+    if (isCausedByConfigurationError(throwable)) {
       return Optional.of(Status.create(409, "Conflict"));
     }
     return Optional.empty();
@@ -115,6 +111,28 @@
     return getCause(CodeOwnersInternalServerErrorException.class, throwable);
   }
 
+  public static boolean isCausedByConfigurationError(Throwable throwable) {
+    return isInvalidPluginConfigurationException(throwable)
+        || isInvalidCodeOwnerConfigException(throwable)
+        || isInvalidPathException(throwable);
+  }
+
+  public static Optional<? extends Exception> getCauseOfConfigurationError(Throwable throwable) {
+    Optional<InvalidPathException> invalidPathException =
+        CodeOwnersExceptionHook.getInvalidPathException(throwable);
+    if (invalidPathException.isPresent()) {
+      return invalidPathException;
+    }
+
+    Optional<InvalidPluginConfigurationException> invalidPluginConfigurationException =
+        CodeOwnersExceptionHook.getInvalidPluginConfigurationCause(throwable);
+    if (invalidPluginConfigurationException.isPresent()) {
+      return invalidPluginConfigurationException;
+    }
+
+    return CodeOwners.getInvalidCodeOwnerConfigCause(throwable);
+  }
+
   private static boolean isInvalidPluginConfigurationException(Throwable throwable) {
     return getInvalidPluginConfigurationCause(throwable).isPresent();
   }
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/CodeOwnersOnAddReviewer.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnAddReviewer.java
index 4e81f46..ae1f477 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnAddReviewer.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnersOnAddReviewer.java
@@ -165,9 +165,17 @@
               })
           .call();
     } catch (Exception e) {
-      logger.atSevere().withCause(e).log(
-          "Failed to post code-owners change message for reviewer on change %s in project %s.",
-          changeId, projectName);
+      Optional<? extends Exception> configurationError =
+          CodeOwnersExceptionHook.getCauseOfConfigurationError(e);
+      if (configurationError.isPresent()) {
+        logger.atWarning().log(
+            "Failed to post code-owners change message for reviewer on change %s in project %s: %s",
+            changeId, projectName, configurationError.get().getMessage());
+      } else {
+        logger.atSevere().withCause(e).log(
+            "Failed to post code-owners change message for reviewer on change %s in project %s.",
+            changeId, projectName);
+      }
     }
   }
 
diff --git a/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerApproval.java b/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerApproval.java
index b538ebd..907f065 100644
--- a/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerApproval.java
+++ b/java/com/google/gerrit/plugins/codeowners/backend/OnCodeOwnerApproval.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkState;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.metrics.Timer0;
 import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
@@ -51,6 +52,8 @@
  */
 @Singleton
 class OnCodeOwnerApproval implements OnPostReview {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
   private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
   private final CodeOwnerApprovalCheck codeOwnerApprovalCheck;
   private final CodeOwnerMetrics codeOwnerMetrics;
@@ -103,6 +106,23 @@
           approvals,
           requiredApproval,
           maxPathsInChangeMessage);
+    } catch (Exception e) {
+      Optional<? extends Exception> configurationError =
+          CodeOwnersExceptionHook.getCauseOfConfigurationError(e);
+      if (configurationError.isPresent()) {
+        logger.atWarning().log(
+            "Failed to post code-owners change message for code owner approval on change %s"
+                + " in project %s: %s",
+            changeNotes.getChangeId(),
+            changeNotes.getProjectName(),
+            configurationError.get().getMessage());
+      } else {
+        logger.atSevere().withCause(e).log(
+            "Failed to post code-owners change message for code owner approval on change %s"
+                + " in project %s.",
+            changeNotes.getChangeId(), changeNotes.getProjectName());
+      }
+      return Optional.empty();
     }
   }
 
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/acceptance/api/CodeOwnersOnAddReviewerIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersOnAddReviewerIT.java
index bfc0283..70fb164 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersOnAddReviewerIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CodeOwnersOnAddReviewerIT.java
@@ -83,6 +83,18 @@
   }
 
   @Test
+  public void noChangeMessageAddedIfInvalidCodeOwnerConfigFilesExist() throws Exception {
+    createNonParseableCodeOwnerConfig(getCodeOwnerConfigFileName());
+
+    String changeId = createChange("Test Change", "foo/bar.baz", "file content").getChangeId();
+
+    gApi.changes().id(changeId).addReviewer(user.email());
+
+    Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+    assertThat(Iterables.getLast(messages).message).isEqualTo("Uploaded patch set 1.");
+  }
+
+  @Test
   public void changeMessageListsOwnedPaths() throws Exception {
     codeOwnerConfigOperations
         .newCodeOwnerConfig()
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/OnCodeOwnerApprovalIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/OnCodeOwnerApprovalIT.java
index efd2166..d3fe19a 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/OnCodeOwnerApprovalIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/OnCodeOwnerApprovalIT.java
@@ -65,6 +65,19 @@
   }
 
   @Test
+  public void changeMessageNotExtendedIfInvalidCodeOwnerConfigFilesExist() throws Exception {
+    createNonParseableCodeOwnerConfig(getCodeOwnerConfigFileName());
+
+    String path = "foo/bar.baz";
+    String changeId = createChange("Test Change", path, "file content").getChangeId();
+
+    recommend(changeId);
+
+    Collection<ChangeMessageInfo> messages = gApi.changes().id(changeId).get().messages;
+    assertThat(Iterables.getLast(messages).message).isEqualTo("Patch Set 1: Code-Review+1");
+  }
+
+  @Test
   public void changeMessageListsNewlyApprovedPaths() throws Exception {
     codeOwnerConfigOperations
         .newCodeOwnerConfig()
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));
+  }
+}