Merge "Add throwable cause of REST API error to request"
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestUtil.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestUtil.java
index c47ca30..04aa44a 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestUtil.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestUtil.java
@@ -18,6 +18,18 @@
 
 /** Utilities for manipulating HTTP request objects. */
 public class RequestUtil {
+  /** HTTP request attribute for storing the Throwable that caused an error condition. */
+  private static final String ATTRIBUTE_ERROR_TRACE =
+      RequestUtil.class.getName() + "/ErrorTraceThrowable";
+
+  public static void setErrorTraceAttribute(HttpServletRequest req, Throwable t) {
+    req.setAttribute(ATTRIBUTE_ERROR_TRACE, t);
+  }
+
+  public static Throwable getErrorTraceAttribute(HttpServletRequest req) {
+    return (Throwable) req.getAttribute(ATTRIBUTE_ERROR_TRACE);
+  }
+
   /**
    * @return the same value as {@link HttpServletRequest#getPathInfo()}, but
    *     without decoding URL-encoded characters.
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
index 3418354..59f96b9 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.httpd;
 
+import static com.google.gerrit.httpd.restapi.RestApiServlet.replyError;
 import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
 import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
 
@@ -77,17 +78,19 @@
     String runas = req.getHeader(RUN_AS);
     if (runas != null) {
       if (!enabled) {
-        RestApiServlet.replyError(req, res,
+        replyError(req, res,
             SC_FORBIDDEN,
-            RUN_AS + " disabled by auth.enableRunAs = false");
+            RUN_AS + " disabled by auth.enableRunAs = false",
+            null);
         return;
       }
 
       CurrentUser self = session.get().getCurrentUser();
       if (!self.getCapabilities().canRunAs()) {
-        RestApiServlet.replyError(req, res,
+        replyError(req, res,
             SC_FORBIDDEN,
-            "not permitted to use " + RUN_AS);
+            "not permitted to use " + RUN_AS,
+            null);
         return;
       }
 
@@ -96,15 +99,17 @@
         target = accountResolver.find(runas);
       } catch (OrmException e) {
         log.warn("cannot resolve account for " + RUN_AS, e);
-        RestApiServlet.replyError(req, res,
+        replyError(req, res,
             SC_INTERNAL_SERVER_ERROR,
-            "cannot resolve " + RUN_AS);
+            "cannot resolve " + RUN_AS,
+            e);
         return;
       }
       if (target == null) {
-        RestApiServlet.replyError(req, res,
+        replyError(req, res,
             SC_FORBIDDEN,
-            "no account matches " + RUN_AS);
+            "no account matches " + RUN_AS,
+            null);
         return;
       }
       session.get().setUserAccountId(target.getId());
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
index 9183d5c..46843fc 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
@@ -68,7 +68,7 @@
       clp.parseOptionMap(in);
     } catch (CmdLineException e) {
       if (!clp.wasHelpRequestedByOption()) {
-        replyError(req, res, SC_BAD_REQUEST, e.getMessage());
+        replyError(req, res, SC_BAD_REQUEST, e.getMessage(), e);
         return false;
       }
     }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 979990a..4fb6e24 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -345,27 +345,27 @@
         }
       }
     } catch (AuthException e) {
-      replyError(req, res, status = SC_FORBIDDEN, e.getMessage(), e.caching());
+      replyError(req, res, status = SC_FORBIDDEN, e.getMessage(), e.caching(), e);
     } catch (BadRequestException e) {
-      replyError(req, res, status = SC_BAD_REQUEST, e.getMessage(), e.caching());
+      replyError(req, res, status = SC_BAD_REQUEST, e.getMessage(), e.caching(), e);
     } catch (MethodNotAllowedException e) {
-      replyError(req, res, status = SC_METHOD_NOT_ALLOWED, "Method not allowed", e.caching());
+      replyError(req, res, status = SC_METHOD_NOT_ALLOWED, "Method not allowed", e.caching(), e);
     } catch (ResourceConflictException e) {
-      replyError(req, res, status = SC_CONFLICT, e.getMessage(), e.caching());
+      replyError(req, res, status = SC_CONFLICT, e.getMessage(), e.caching(), e);
     } catch (PreconditionFailedException e) {
       replyError(req, res, status = SC_PRECONDITION_FAILED,
-          MoreObjects.firstNonNull(e.getMessage(), "Precondition failed"), e.caching());
+          MoreObjects.firstNonNull(e.getMessage(), "Precondition failed"), e.caching(), e);
     } catch (ResourceNotFoundException e) {
-      replyError(req, res, status = SC_NOT_FOUND, "Not found", e.caching());
+      replyError(req, res, status = SC_NOT_FOUND, "Not found", e.caching(), e);
     } catch (UnprocessableEntityException e) {
       replyError(req, res, status = 422,
-          MoreObjects.firstNonNull(e.getMessage(), "Unprocessable Entity"), e.caching());
+          MoreObjects.firstNonNull(e.getMessage(), "Unprocessable Entity"), e.caching(), e);
     } catch (AmbiguousViewException e) {
-      replyError(req, res, status = SC_NOT_FOUND, e.getMessage());
+      replyError(req, res, status = SC_NOT_FOUND, e.getMessage(), e);
     } catch (MalformedJsonException e) {
-      replyError(req, res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request");
+      replyError(req, res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request", e);
     } catch (JsonParseException e) {
-      replyError(req, res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request");
+      replyError(req, res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request", e);
     } catch (Exception e) {
       status = SC_INTERNAL_SERVER_ERROR;
       handleException(e, req, res);
@@ -928,21 +928,24 @@
 
     if (!res.isCommitted()) {
       res.reset();
-      replyError(req, res, SC_INTERNAL_SERVER_ERROR, "Internal server error");
+      replyError(req, res, SC_INTERNAL_SERVER_ERROR, "Internal server error", err);
     }
   }
 
-  public static void replyError(HttpServletRequest req,
-      HttpServletResponse res, int statusCode, String msg) throws IOException {
-    replyError(req, res, statusCode, msg, CacheControl.NONE);
+  public static void replyError(HttpServletRequest req, HttpServletResponse res,
+      int statusCode, String msg, @Nullable Throwable err) throws IOException {
+    replyError(req, res, statusCode, msg, CacheControl.NONE, err);
   }
 
   public static void replyError(HttpServletRequest req,
       HttpServletResponse res, int statusCode, String msg,
-      CacheControl c) throws IOException {
+      CacheControl c, @Nullable Throwable err) throws IOException {
     res.setStatus(statusCode);
     configureCaching(req, res, null, c);
     replyText(req, res, msg);
+    if (err != null) {
+      RequestUtil.setErrorTraceAttribute(req, err);
+    }
   }
 
   static void replyText(@Nullable HttpServletRequest req,