Merge changes I0faffdd1,I795da1fb,Id926b849,I2f584fd0,Ic653584f
* changes:
Record correct status in REST metric if ExceptionHook changed the status
ExceptionHook: Pass trace ID into getUserMessages method
ExceptionHook: Allow to return multiple user messages
Control response code for LOCK_FAILUREs from ExceptionHookImpl
ExceptionHook: Document the behavior when multiple exception hooks are registered
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 51a6191..d9bb3b9 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -45,7 +45,6 @@
import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED;
-import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.CharMatcher;
@@ -105,7 +104,6 @@
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.ExceptionHook;
-import com.google.gerrit.server.ExceptionHookImpl;
import com.google.gerrit.server.OptionUtil;
import com.google.gerrit.server.RequestInfo;
import com.google.gerrit.server.RequestListener;
@@ -318,7 +316,7 @@
long auditStartTs = TimeUtil.nowMs();
res.setHeader("Content-Disposition", "attachment");
res.setHeader("X-Content-Type-Options", "nosniff");
- int status = SC_OK;
+ int statusCode = SC_OK;
long responseBytes = -1;
Optional<Exception> cause = Optional.empty();
Response<?> response = null;
@@ -587,10 +585,10 @@
return;
}
- status = response.statusCode();
+ statusCode = response.statusCode();
configureCaching(req, res, traceContext, rsrc, viewData, response.caching());
- res.setStatus(status);
- logger.atFinest().log("REST call succeeded: %d", status);
+ res.setStatus(statusCode);
+ logger.atFinest().log("REST call succeeded: %d", statusCode);
}
if (response != Response.none()) {
@@ -605,44 +603,48 @@
cause = Optional.of(e);
responseBytes =
replyError(
- req, res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request", e);
+ req, res, statusCode = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request", e);
} catch (BadRequestException e) {
cause = Optional.of(e);
responseBytes =
replyError(
- req, res, status = SC_BAD_REQUEST, messageOr(e, "Bad Request"), e.caching(), e);
+ req, res, statusCode = SC_BAD_REQUEST, messageOr(e, "Bad Request"), e.caching(), e);
} catch (AuthException e) {
cause = Optional.of(e);
responseBytes =
- replyError(req, res, status = SC_FORBIDDEN, messageOr(e, "Forbidden"), e.caching(), e);
+ replyError(
+ req, res, statusCode = SC_FORBIDDEN, messageOr(e, "Forbidden"), e.caching(), e);
} catch (AmbiguousViewException e) {
cause = Optional.of(e);
- responseBytes = replyError(req, res, status = SC_NOT_FOUND, messageOr(e, "Ambiguous"), e);
+ responseBytes =
+ replyError(req, res, statusCode = SC_NOT_FOUND, messageOr(e, "Ambiguous"), e);
} catch (ResourceNotFoundException e) {
cause = Optional.of(e);
responseBytes =
- replyError(req, res, status = SC_NOT_FOUND, messageOr(e, "Not Found"), e.caching(), e);
+ replyError(
+ req, res, statusCode = SC_NOT_FOUND, messageOr(e, "Not Found"), e.caching(), e);
} catch (MethodNotAllowedException e) {
cause = Optional.of(e);
responseBytes =
replyError(
req,
res,
- status = SC_METHOD_NOT_ALLOWED,
+ statusCode = SC_METHOD_NOT_ALLOWED,
messageOr(e, "Method Not Allowed"),
e.caching(),
e);
} catch (ResourceConflictException e) {
cause = Optional.of(e);
responseBytes =
- replyError(req, res, status = SC_CONFLICT, messageOr(e, "Conflict"), e.caching(), e);
+ replyError(
+ req, res, statusCode = SC_CONFLICT, messageOr(e, "Conflict"), e.caching(), e);
} catch (PreconditionFailedException e) {
cause = Optional.of(e);
responseBytes =
replyError(
req,
res,
- status = SC_PRECONDITION_FAILED,
+ statusCode = SC_PRECONDITION_FAILED,
messageOr(e, "Precondition Failed"),
e.caching(),
e);
@@ -652,7 +654,7 @@
replyError(
req,
res,
- status = SC_UNPROCESSABLE_ENTITY,
+ statusCode = SC_UNPROCESSABLE_ENTITY,
messageOr(e, "Unprocessable Entity"),
e.caching(),
e);
@@ -660,34 +662,43 @@
cause = Optional.of(e);
logger.atSevere().withCause(e).log("Error in %s %s", req.getMethod(), uriForLogging(req));
responseBytes =
- replyError(req, res, status = SC_NOT_IMPLEMENTED, messageOr(e, "Not Implemented"), e);
+ replyError(
+ req, res, statusCode = SC_NOT_IMPLEMENTED, messageOr(e, "Not Implemented"), e);
} catch (QuotaException e) {
cause = Optional.of(e);
responseBytes =
replyError(
req,
res,
- status = SC_TOO_MANY_REQUESTS,
+ statusCode = SC_TOO_MANY_REQUESTS,
messageOr(e, "Quota limit reached"),
e.caching(),
e);
} catch (Exception e) {
cause = Optional.of(e);
+ statusCode = SC_INTERNAL_SERVER_ERROR;
- if (ExceptionHookImpl.isLockFailure(e)) {
- logger.atSevere().withCause(e.getCause()).log(
- "Error in %s %s", req.getMethod(), uriForLogging(req));
- responseBytes = replyError(req, res, status = SC_SERVICE_UNAVAILABLE, "Lock failure", e);
+ if (res.isCommitted()) {
+ logger.atSevere().withCause(e).log(
+ "Error in %s %s, response already committed", req.getMethod(), uriForLogging(req));
+ responseBytes = 0;
+ } else {
+ res.reset();
+ traceContext.getTraceId().ifPresent(traceId -> res.addHeader(X_GERRIT_TRACE, traceId));
+
+ Optional<ExceptionHook.Status> status = getStatus(e);
+ if (status.isPresent()) {
+ statusCode = status.get().statusCode();
+ responseBytes = reply(req, res, e, status.get(), getUserMessages(traceContext, e));
+ }
+ responseBytes = replyInternalServerError(req, res, e, getUserMessages(traceContext, e));
}
-
- status = SC_INTERNAL_SERVER_ERROR;
- responseBytes = handleException(traceContext, e, req, res);
} finally {
String metric = getViewName(viewData);
String formattedCause = cause.map(globals.retryHelper::formatCause).orElse("_none");
globals.metrics.count.increment(metric);
- if (status >= SC_BAD_REQUEST) {
- globals.metrics.errorCount.increment(metric, status, formattedCause);
+ if (statusCode >= SC_BAD_REQUEST) {
+ globals.metrics.errorCount.increment(metric, statusCode, formattedCause);
}
if (responseBytes != -1) {
globals.metrics.responseBytes.record(metric, responseBytes);
@@ -702,7 +713,7 @@
auditStartTs,
qp != null ? qp.params() : ImmutableListMultimap.of(),
inputRequestBody,
- status,
+ statusCode,
response,
rsrc,
viewData == null ? null : viewData.view));
@@ -1736,50 +1747,60 @@
}
}
- private long handleException(
- TraceContext traceContext, Throwable err, HttpServletRequest req, HttpServletResponse res)
+ private Optional<ExceptionHook.Status> getStatus(Throwable err) {
+ return globals.exceptionHooks.stream()
+ .map(h -> h.getStatus(err))
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .findFirst();
+ }
+
+ private ImmutableList<String> getUserMessages(TraceContext traceContext, Throwable err) {
+ return globals.exceptionHooks.stream()
+ .flatMap(h -> h.getUserMessages(err, traceContext.getTraceId().orElse(null)).stream())
+ .collect(toImmutableList());
+ }
+
+ private static long reply(
+ HttpServletRequest req,
+ HttpServletResponse res,
+ Throwable err,
+ ExceptionHook.Status status,
+ ImmutableList<String> userMessages)
throws IOException {
- if (res.isCommitted()) {
- logger.atSevere().withCause(err).log(
- "Error in %s %s, response already committed", req.getMethod(), uriForLogging(req));
- return 0;
+ res.setStatus(status.statusCode());
+
+ StringBuilder msg = new StringBuilder(status.statusMessage());
+ if (!userMessages.isEmpty()) {
+ msg.append("\n");
+ userMessages.forEach(m -> msg.append("\n* ").append(m));
}
- res.reset();
- traceContext.getTraceId().ifPresent(traceId -> res.addHeader(X_GERRIT_TRACE, traceId));
- ImmutableList<String> userMessages =
- globals.exceptionHooks.stream()
- .map(h -> h.getUserMessage(err))
- .filter(Optional::isPresent)
- .map(Optional::get)
- .collect(toImmutableList());
-
- Optional<Integer> statusCode =
- globals.exceptionHooks.stream()
- .map(h -> h.getStatusCode(err))
- .filter(Optional::isPresent)
- .map(Optional::get)
- .findFirst();
- if (statusCode.isPresent() && statusCode.get() < 400) {
- StringBuilder msg = new StringBuilder();
- if (userMessages.size() == 1) {
- msg.append(userMessages.get(0));
- } else {
- userMessages.forEach(m -> msg.append("\n* ").append(m));
- }
-
- res.setStatus(statusCode.get());
- logger.atFinest().withCause(err).log("REST call finished: %d", statusCode.get().intValue());
+ if (status.statusCode() < SC_BAD_REQUEST) {
+ logger.atFinest().withCause(err).log("REST call finished: %d", status.statusCode());
return replyText(req, res, true, msg.toString());
}
+ if (status.statusCode() >= SC_INTERNAL_SERVER_ERROR) {
+ logger.atSevere().withCause(err).log("Error in %s %s", req.getMethod(), uriForLogging(req));
+ }
+ return replyError(req, res, status.statusCode(), msg.toString(), err);
+ }
+ private static long replyInternalServerError(
+ HttpServletRequest req,
+ HttpServletResponse res,
+ Throwable err,
+ ImmutableList<String> userMessages)
+ throws IOException {
logger.atSevere().withCause(err).log("Error in %s %s", req.getMethod(), uriForLogging(req));
StringBuilder msg = new StringBuilder("Internal server error");
if (!userMessages.isEmpty()) {
+ msg.append("\n");
userMessages.forEach(m -> msg.append("\n* ").append(m));
}
- return replyError(req, res, statusCode.orElse(SC_INTERNAL_SERVER_ERROR), msg.toString(), err);
+
+ return replyError(req, res, SC_INTERNAL_SERVER_ERROR, msg.toString(), err);
}
private static String uriForLogging(HttpServletRequest req) {
diff --git a/java/com/google/gerrit/server/ExceptionHook.java b/java/com/google/gerrit/server/ExceptionHook.java
index 7a1bdde..89a0ab7 100644
--- a/java/com/google/gerrit/server/ExceptionHook.java
+++ b/java/com/google/gerrit/server/ExceptionHook.java
@@ -14,6 +14,11 @@
package com.google.gerrit.server;
+import static java.util.Objects.requireNonNull;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.annotations.ExtensionPoint;
import java.util.Optional;
@@ -40,6 +45,9 @@
* <p>If {@code false} is returned the operation is still retried once to capture a trace, unless
* {@link #skipRetryWithTrace(String, String, Throwable)} skips the auto-retry.
*
+ * <p>If multiple exception hooks are registered, the operation is retried if any of them returns
+ * {@code true} from this method.
+ *
* @param throwable throwable that was thrown while executing the operation
* @param actionType the type of the action for which the exception occurred
* @param actionName the name of the action for which the exception occurred
@@ -69,6 +77,9 @@
* <p>This method is only invoked if retry with tracing is enabled on the server ({@code
* retry.retryWithTraceOnFailure} in {@code gerrit.config} is set to {@code true}).
*
+ * <p>If multiple exception hooks are registered, retrying with tracing is skipped if any of them
+ * returns {@code true} from this method.
+ *
* @param throwable throwable that was thrown while executing the operation
* @param actionType the type of the action for which the exception occurred
* @param actionName the name of the action for which the exception occurred
@@ -85,6 +96,9 @@
* <p>This method allows implementors to group exceptions that have the same cause into one metric
* bucket.
*
+ * <p>If multiple exception hooks return a value from this method, the value from the exception
+ * hook that is registered first is used.
+ *
* @param throwable the exception cause
* @return formatted cause or {@link Optional#empty()} if no formatting was done
*/
@@ -93,36 +107,56 @@
}
/**
- * Returns a message that should be returned to the user.
+ * Returns messages that should be returned to the user.
*
- * <p>This message is included into the HTTP response that is sent to the user.
+ * <p>These messages are included into the HTTP response that is sent to the user.
+ *
+ * <p>If multiple exception hooks return a value from this method, all the values are included
+ * into the HTTP response (in the order in which the exception hooks are registered).
*
* @param throwable throwable that was thrown while executing an operation
- * @return error message that should be returned to the user, {@link Optional#empty()} if no
+ * @param traceId ID of the trace if this request was traced, otherwise {@code null}
+ * @return error messages that should be returned to the user, {@link Optional#empty()} if no
* message should be returned to the user
*/
- default Optional<String> getUserMessage(Throwable throwable) {
- return Optional.empty();
+ default ImmutableList<String> getUserMessages(Throwable throwable, @Nullable String traceId) {
+ return ImmutableList.of();
}
/**
- * Returns the HTTP status code that should be returned to the user.
+ * Returns the HTTP status that should be returned to the user.
*
- * <p>If no value is returned ({@link Optional#empty()}) the HTTP status code defaults to {@code
- * 500 Internal Server Error}.
+ * <p>Implementors may use this method to change the status for certain exceptions (e.g. using
+ * this method it would be possible to return {@code 503 Lock failure} for {@link
+ * com.google.gerrit.git.LockFailureException}s instead of {@code 500 Internal server error}).
*
- * <p>{@link #getUserMessage(Throwable)} allows to define which message should be included into
- * the body of the HTTP response.
+ * <p>If no value is returned ({@link Optional#empty()}) it means that this exception hook doesn't
+ * want to change the default response code for the given exception which is {@code 500 Internal
+ * Server Error}, but is fine if other exception hook implementation do so.
*
- * <p>Implementors may use this method to change the status code for certain exceptions (e.g.
- * using this method it would be possible to return {@code 409 Conflict} for {@link
- * com.google.gerrit.git.LockFailureException}s instead of {@code 500 Internal Server Error}).
+ * <p>If multiple exception hooks return a value from this method, the value from exception hook
+ * that is registered first is used.
+ *
+ * <p>{@link #getUserMessages(Throwable, String)} allows to define which additional messages
+ * should be included into the body of the HTTP response.
*
* @param throwable throwable that was thrown while executing an operation
- * @return HTTP status code that should be returned to the user, {@link Optional#empty()} if the
+ * @return HTTP status that should be returned to the user, {@link Optional#empty()} if the
* exception should result in {@code 500 Internal Server Error}
*/
- default Optional<Integer> getStatusCode(Throwable throwable) {
+ default Optional<Status> getStatus(Throwable throwable) {
return Optional.empty();
}
+
+ @AutoValue
+ public abstract class Status {
+ public abstract int statusCode();
+
+ public abstract String statusMessage();
+
+ public static Status create(int statusCode, String statusMessage) {
+ return new AutoValue_ExceptionHook_Status(
+ statusCode, requireNonNull(statusMessage, "statusMessage"));
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/ExceptionHookImpl.java b/java/com/google/gerrit/server/ExceptionHookImpl.java
index 0cc2545..8393451 100644
--- a/java/com/google/gerrit/server/ExceptionHookImpl.java
+++ b/java/com/google/gerrit/server/ExceptionHookImpl.java
@@ -16,6 +16,8 @@
import com.google.common.base.Predicate;
import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.git.LockFailureException;
import java.util.Optional;
import org.eclipse.jgit.lib.RefUpdate;
@@ -44,14 +46,22 @@
}
@Override
- public Optional<String> getUserMessage(Throwable throwable) {
+ public ImmutableList<String> getUserMessages(Throwable throwable, @Nullable String traceId) {
if (isLockFailure(throwable)) {
- return Optional.of(LOCK_FAILURE_USER_MESSAGE);
+ return ImmutableList.of(LOCK_FAILURE_USER_MESSAGE);
+ }
+ return ImmutableList.of();
+ }
+
+ @Override
+ public Optional<Status> getStatus(Throwable throwable) {
+ if (isLockFailure(throwable)) {
+ return Optional.of(Status.create(503, "Lock failure"));
}
return Optional.empty();
}
- public static boolean isLockFailure(Throwable throwable) {
+ private static boolean isLockFailure(Throwable throwable) {
return isMatching(throwable, t -> t instanceof LockFailureException);
}