This is an automated email from the ASF dual-hosted git repository.

dspavlov pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite-teamcity-bot.git


The following commit(s) were added to refs/heads/master by this push:
     new 293dc2c1 Show root cause for server errors (#218)
293dc2c1 is described below

commit 293dc2c18d9cb511e415d870226a5f345804b407
Author: ignitetcbot <[email protected]>
AuthorDate: Sat May 9 14:13:30 2026 +0300

    Show root cause for server errors (#218)
    
    Codex co-authored-by: Dmitriy Pavlov <[email protected]>
---
 .../web/rest/exception/ExeptionsTraceLogger.java   | 90 +++++++++++++++++++++-
 .../src/main/webapp/js/common-1.7.js               |  6 +-
 .../rest/exception/ExeptionsTraceLoggerTest.java   | 47 +++++++++++
 3 files changed, 140 insertions(+), 3 deletions(-)

diff --git 
a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/exception/ExeptionsTraceLogger.java
 
b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/exception/ExeptionsTraceLogger.java
index bf37b809..a0ed8218 100644
--- 
a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/exception/ExeptionsTraceLogger.java
+++ 
b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/exception/ExeptionsTraceLogger.java
@@ -16,6 +16,10 @@
  */
 package org.apache.ignite.ci.web.rest.exception;
 
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.Set;
+import javax.xml.bind.JAXBException;
 import javax.ws.rs.ext.ExceptionMapper;
 import javax.ws.rs.ext.Provider;
 import javax.ws.rs.core.Response;
@@ -31,6 +35,9 @@ public class ExeptionsTraceLogger implements 
ExceptionMapper<Throwable> {
     /** Logger. */
     private static final Logger logger = 
LoggerFactory.getLogger(ExeptionsTraceLogger.class);
 
+    /** Max causes to include in HTTP response. */
+    private static final int MAX_CAUSE_DEPTH = 8;
+
     /** {@inheritDoc} */
     @Override public Response toResponse(Throwable t) {
         logger.error("Error during processing request (Internal Server Error 
[500]). Caused by: ", t);
@@ -38,6 +45,85 @@ public class ExeptionsTraceLogger implements 
ExceptionMapper<Throwable> {
         if 
(Boolean.valueOf(System.getProperty(TcBotSystemProperties.DEV_MODE)))
             t.printStackTrace();
 
-        return Response.serverError().entity(t.getMessage()).build();
+        return Response.serverError().entity(errorMessage(t)).build();
+    }
+
+    /**
+     * @param t Exception.
+     * @return Short user-facing error text with the useful root cause 
preserved.
+     */
+    private static String errorMessage(Throwable t) {
+        StringBuilder res = new StringBuilder("Internal Server Error [500].");
+        Throwable root = rootCause(t);
+        String rootMsg = formatCause(root);
+
+        if (!rootMsg.isEmpty())
+            res.append("\nReason: ").append(rootMsg);
+
+        res.append("\n\nCause chain:");
+
+        Set<Throwable> seen = Collections.newSetFromMap(new 
IdentityHashMap<>());
+        Throwable cur = t;
+        int depth = 0;
+
+        while (cur != null && seen.add(cur) && depth < MAX_CAUSE_DEPTH) {
+            res.append("\n- ").append(formatCause(cur));
+
+            cur = nextCause(cur);
+            depth++;
+        }
+
+        if (cur != null)
+            res.append("\n- ...");
+
+        return res.toString();
+    }
+
+    /**
+     * @param t Exception.
+     * @return Root cause, including JAXB linked exceptions.
+     */
+    private static Throwable rootCause(Throwable t) {
+        Set<Throwable> seen = Collections.newSetFromMap(new 
IdentityHashMap<>());
+        Throwable cur = t;
+        Throwable next;
+
+        while (cur != null && seen.add(cur) && (next = nextCause(cur)) != null)
+            cur = next;
+
+        return cur == null ? t : cur;
+    }
+
+    /**
+     * @param t Exception.
+     * @return Next cause.
+     */
+    private static Throwable nextCause(Throwable t) {
+        Throwable cause = t.getCause();
+
+        if (cause != null)
+            return cause;
+
+        if (t instanceof JAXBException)
+            return ((JAXBException)t).getLinkedException();
+
+        return null;
+    }
+
+    /**
+     * @param t Exception.
+     * @return Cause text.
+     */
+    private static String formatCause(Throwable t) {
+        if (t == null)
+            return "";
+
+        String msg = t.getMessage();
+        String cls = t.getClass().getSimpleName();
+
+        if (msg == null || msg.trim().isEmpty())
+            return cls;
+
+        return cls + ": " + msg.replace('\r', ' ').replace('\n', ' ').trim();
     }
-}
\ No newline at end of file
+}
diff --git a/ignite-tc-helper-web/src/main/webapp/js/common-1.7.js 
b/ignite-tc-helper-web/src/main/webapp/js/common-1.7.js
index 7d440a5c..1773bd17 100644
--- a/ignite-tc-helper-web/src/main/webapp/js/common-1.7.js
+++ b/ignite-tc-helper-web/src/main/webapp/js/common-1.7.js
@@ -110,7 +110,11 @@ function showErrInLoadStatus(jqXHR, exception) {
     } else if (jqXHR.status === 424) {
         $("#loadStatus").html('Dependency problem: [424]: ' + 
jqXHR.responseText);
     } else if (jqXHR.status === 500) {
-        $("#loadStatus").html('Internal Server Error [500].');
+        var serverMsg = isDefinedAndFilled(jqXHR.responseText)
+            ? jqXHR.responseText
+            : 'Internal Server Error [500].';
+
+        $("#loadStatus").text(serverMsg);
     } else if (exception === 'parsererror') {
         $("#loadStatus").html('Requested JSON parse failed.');
     } else if (exception === 'timeout') {
diff --git 
a/ignite-tc-helper-web/src/test/java/org/apache/ignite/ci/web/rest/exception/ExeptionsTraceLoggerTest.java
 
b/ignite-tc-helper-web/src/test/java/org/apache/ignite/ci/web/rest/exception/ExeptionsTraceLoggerTest.java
new file mode 100644
index 00000000..b75fedee
--- /dev/null
+++ 
b/ignite-tc-helper-web/src/test/java/org/apache/ignite/ci/web/rest/exception/ExeptionsTraceLoggerTest.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.ignite.ci.web.rest.exception;
+
+import java.net.SocketException;
+import javax.ws.rs.core.Response;
+import javax.xml.bind.UnmarshalException;
+import org.junit.Test;
+
+import static org.junit.Assert.assertTrue;
+
+/**
+ *
+ */
+public class ExeptionsTraceLoggerTest {
+    /** */
+    @Test
+    public void responseContainsRootCauseFromJaxbLinkedException() {
+        SocketException socketException = new SocketException("Connection 
reset");
+        UnmarshalException unmarshalException = new UnmarshalException("Failed 
to parse TeamCity XML", socketException);
+        RuntimeException exception = new RuntimeException("TeamCity request 
failed", unmarshalException);
+
+        Response response = new ExeptionsTraceLogger().toResponse(exception);
+
+        String msg = (String)response.getEntity();
+
+        assertTrue(msg.contains("Internal Server Error [500]."));
+        assertTrue(msg.contains("Reason: SocketException: Connection reset"));
+        assertTrue(msg.contains("RuntimeException: TeamCity request failed"));
+        assertTrue(msg.contains("UnmarshalException: Failed to parse TeamCity 
XML"));
+    }
+}

Reply via email to