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

ffang pushed a commit to branch 4.0.x-fixes
in repository https://gitbox.apache.org/repos/asf/cxf.git


The following commit(s) were added to refs/heads/4.0.x-fixes by this push:
     new a77a1a127a [CXF-9189]Exceptions Not Logged When Using SseEventSink in 
JAX-RS SSE… (#2919)
a77a1a127a is described below

commit a77a1a127a12a9ab54f67c0088f9b284f0ba9d63
Author: Freeman(Yue) Fang <[email protected]>
AuthorDate: Sat Mar 7 11:00:48 2026 -0500

    [CXF-9189]Exceptions Not Logged When Using SseEventSink in JAX-RS SSE… 
(#2919)
    
    * [CXF-9189]Exceptions Not Logged When Using SseEventSink in JAX-RS SSE 
Endpoint
    
    * [CXF-9189]use in memory log appender for the testcase
    
    (cherry picked from commit ee04e4a3bc7f28ead64408681ff8ed59e7bc3e4a)
    (cherry picked from commit b26d0805a48764c52628946314dc1c1e6cefce20)
---
 .../java/org/apache/cxf/jaxrs/JAXRSInvoker.java    |  17 ++++
 systests/rs-sse/rs-sse-base/pom.xml                |   4 +
 .../cxf/systest/jaxrs/sse/AbstractSseTest.java     | 107 +++++++++++++++++++++
 .../apache/cxf/systest/jaxrs/sse/BookStore.java    |   9 ++
 .../apache/cxf/systest/jaxrs/sse/BookStore2.java   |  10 ++
 5 files changed, 147 insertions(+)

diff --git 
a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/JAXRSInvoker.java 
b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/JAXRSInvoker.java
index 6eed037930..7c6c59f399 100644
--- a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/JAXRSInvoker.java
+++ b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/JAXRSInvoker.java
@@ -29,6 +29,7 @@ import java.util.ResourceBundle;
 import java.util.concurrent.CancellationException;
 import java.util.concurrent.CompletionException;
 import java.util.concurrent.CompletionStage;
+import java.util.logging.Level;
 import java.util.logging.Logger;
 
 import jakarta.ws.rs.WebApplicationException;
@@ -38,6 +39,7 @@ import jakarta.ws.rs.core.Application;
 import jakarta.ws.rs.core.MediaType;
 import jakarta.ws.rs.core.MultivaluedMap;
 import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.sse.SseEventSink;
 import org.apache.cxf.common.classloader.ClassLoaderUtils;
 import org.apache.cxf.common.classloader.ClassLoaderUtils.ClassLoaderHolder;
 import org.apache.cxf.common.i18n.BundleUtils;
@@ -144,6 +146,21 @@ public class JAXRSInvoker extends AbstractInvoker {
         try {
             return handleFault(new Fault(t), exchange.getInMessage(), null, 
null);
         } catch (Fault ex) {
+            //If we got here, the fault was effectively "unmapped" (no 
Response could be created)
+            // and we'd otherwise lose the usual fault logging (common with 
SSE sink / async paths).
+            Throwable cause = ex.getCause() == null ? ex : ex.getCause();
+            LOG.log(Level.SEVERE, "Unhandled exception from JAX-RS invocation 
(async/SSE path)", cause);
+            
+            // Best-effort: if this request is SSE, close the sink so the 
container can complete cleanly.
+            try {
+                SseEventSink sink = 
(SseEventSink)exchange.getInMessage().get(SseEventSink.class);
+                if (sink != null && !sink.isClosed()) {
+                    sink.close();
+                }
+            } catch (Exception ignore) {
+                // ignore secondary failures; primary goal is to log original 
cause
+            }
+            
             ar.setUnmappedThrowable(ex.getCause() == null ? ex : 
ex.getCause());
             if (isSuspended(exchange)) {
                 ar.reset();
diff --git a/systests/rs-sse/rs-sse-base/pom.xml 
b/systests/rs-sse/rs-sse-base/pom.xml
index 28a19875c9..20ecef25d3 100644
--- a/systests/rs-sse/rs-sse-base/pom.xml
+++ b/systests/rs-sse/rs-sse-base/pom.xml
@@ -75,5 +75,9 @@
             <artifactId>awaitility</artifactId>
             <scope>compile</scope>
         </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+        </dependency>
     </dependencies>
 </project>
diff --git 
a/systests/rs-sse/rs-sse-base/src/main/java/org/apache/cxf/systest/jaxrs/sse/AbstractSseTest.java
 
b/systests/rs-sse/rs-sse-base/src/main/java/org/apache/cxf/systest/jaxrs/sse/AbstractSseTest.java
index 3405752744..6bfc9f301c 100644
--- 
a/systests/rs-sse/rs-sse-base/src/main/java/org/apache/cxf/systest/jaxrs/sse/AbstractSseTest.java
+++ 
b/systests/rs-sse/rs-sse-base/src/main/java/org/apache/cxf/systest/jaxrs/sse/AbstractSseTest.java
@@ -36,6 +36,11 @@ import java.util.function.Consumer;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.jakarta.rs.json.JacksonJsonProvider;
 
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.classic.spi.IThrowableProxy;
+import ch.qos.logback.core.read.ListAppender;
 import jakarta.ws.rs.client.Entity;
 import jakarta.ws.rs.client.WebTarget;
 import jakarta.ws.rs.core.HttpHeaders;
@@ -46,6 +51,8 @@ import jakarta.ws.rs.ext.MessageBodyReader;
 import jakarta.ws.rs.sse.InboundSseEvent;
 import jakarta.ws.rs.sse.SseEventSource;
 import jakarta.ws.rs.sse.SseEventSource.Builder;
+import org.slf4j.LoggerFactory;
+
 
 import org.junit.Before;
 import org.junit.Test;
@@ -61,6 +68,10 @@ import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 
+
+
+
+
 public abstract class AbstractSseTest extends AbstractSseBaseTest {
     @Before
     public void setUp() {
@@ -71,6 +82,9 @@ public abstract class AbstractSseTest extends 
AbstractSseBaseTest {
 
     }
     
+    
+
+        
     @Test
     public void testBooksStreamIsReturnedFromLastEventId() throws 
InterruptedException {
         final WebTarget target = createWebTarget("/rest/api/bookstore/sse/" + 
UUID.randomUUID())
@@ -409,7 +423,98 @@ public abstract class AbstractSseTest extends 
AbstractSseBaseTest {
             assertThat(response.getHeaderString("X-My-ProtocolHeader"), 
equalTo("protocol-headers"));
         }
     }
+    
+ 
+    @Test
+    public void testSseEndpointExceptionIsLoggedToConsole() throws Exception {
+        final Logger logger = (Logger) 
LoggerFactory.getLogger("org.apache.cxf.jaxrs.JAXRSInvoker");
+
+        final Level oldLevel = logger.getLevel();
+        final ListAppender<ILoggingEvent> appender = new ListAppender<>();
+        appender.start();
+
+        try {
+            logger.setLevel(Level.ERROR);
+            logger.addAppender(appender);
+
+            try (Response r = 
createWebTarget("/rest/api/bookstore/sse/fail/request")
+                    .request(MediaType.SERVER_SENT_EVENTS)
+                    .get()) {
+                // Force the client to actually start consuming
+                r.readEntity(String.class);
+            } catch (Exception ex) {
+                // expected
+            }
+
+            // Wait until we have at least one ERROR from JAXRSInvoker
+            awaitEvents(2000, appender.list, 1);
+
+            assertTrue("Expected SSE log event, got:\n" + dump(appender),
+                    hasUnhandledExceptionEvent(appender));
+
+            assertTrue("Expected SSE marker in throwable, got:\n" + 
dump(appender),
+                    hasMarkerInUnhandledExceptionEvent(appender, 
"CXF-9189-MARKER"));
+        } finally {
+            logger.detachAppender(appender);
+            logger.setLevel(oldLevel);
+            appender.stop();
+        }
+    }
+
+    private static boolean 
hasUnhandledExceptionEvent(ListAppender<ILoggingEvent> appender) {
+        final String msgNeedle = "Unhandled exception from JAX-RS invocation 
(async/SSE path)";
+        for (ILoggingEvent e : appender.list) {
+            String msg = e.getFormattedMessage();
+            if (msg != null && msg.contains(msgNeedle)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static boolean 
hasMarkerInUnhandledExceptionEvent(ListAppender<ILoggingEvent> appender, String 
marker) {
+        final String msgNeedle = "Unhandled exception from JAX-RS invocation 
(async/SSE path)";
+        for (ILoggingEvent e : appender.list) {
+            String msg = e.getFormattedMessage();
+            if (msg == null || !msg.contains(msgNeedle)) {
+                continue;
+            }
+            // marker can be in message OR in throwable chain
+            if (msg.contains(marker) || 
throwableChainContains(e.getThrowableProxy(), marker)) {
+                return true;
+            }
+        }
+        return false;
+    }
 
+    private static boolean throwableChainContains(IThrowableProxy tp, String 
needle) {
+        for (IThrowableProxy cur = tp; cur != null; cur = cur.getCause()) {
+            String m = cur.getMessage();
+            if (m != null && m.contains(needle)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static String dump(ListAppender<ILoggingEvent> appender) {
+        StringBuilder sb = new StringBuilder();
+        for (ILoggingEvent e : appender.list) {
+            sb.append('[').append(e.getLevel()).append("] ")
+              .append(e.getLoggerName()).append(" - ")
+              .append(e.getFormattedMessage());
+            if (e.getThrowableProxy() != null) {
+                sb.append(" (thrown: ")
+                  .append(e.getThrowableProxy().getClassName())
+                  .append(": ")
+                  .append(e.getThrowableProxy().getMessage())
+                  .append(')');
+            }
+            sb.append('\n');
+        }
+        return sb.toString();
+    }
+    
     /**
      * Jetty / Undertow do not propagate errors from the runnable passed to
      * AsyncContext::start() up to the AsyncEventListener::onError(). Tomcat 
however
@@ -427,4 +532,6 @@ public abstract class AbstractSseTest extends 
AbstractSseBaseTest {
     private static Consumer<InboundSseEvent> collectRaw(final 
Collection<String> titles) {
         return event -> titles.add(event.readData(String.class, 
MediaType.TEXT_PLAIN_TYPE));
     }
+    
+    
 }
diff --git 
a/systests/rs-sse/rs-sse-base/src/main/java/org/apache/cxf/systest/jaxrs/sse/BookStore.java
 
b/systests/rs-sse/rs-sse-base/src/main/java/org/apache/cxf/systest/jaxrs/sse/BookStore.java
index 37a1f774eb..4708de7e91 100644
--- 
a/systests/rs-sse/rs-sse-base/src/main/java/org/apache/cxf/systest/jaxrs/sse/BookStore.java
+++ 
b/systests/rs-sse/rs-sse-base/src/main/java/org/apache/cxf/systest/jaxrs/sse/BookStore.java
@@ -236,6 +236,15 @@ public class BookStore extends BookStoreClientCloseable {
         return BookStoreResponseFilter.getInvocations();
     }
     
+    @GET
+    @Path("/sse/fail/request")
+    @Produces(MediaType.SERVER_SENT_EVENTS)
+    public void failOnRequestThread(@Context SseEventSink sink) {
+        throw new RuntimeException("CXF-9189-MARKER: exception from SSE 
resource method should be logged");
+    }
+
+    
+        
     @PUT
     @Path("/filtered/stats")
     public void clearStats() {
diff --git 
a/systests/rs-sse/rs-sse-base/src/main/java/org/apache/cxf/systest/jaxrs/sse/BookStore2.java
 
b/systests/rs-sse/rs-sse-base/src/main/java/org/apache/cxf/systest/jaxrs/sse/BookStore2.java
index e0061cdfa1..aad2bedf3a 100644
--- 
a/systests/rs-sse/rs-sse-base/src/main/java/org/apache/cxf/systest/jaxrs/sse/BookStore2.java
+++ 
b/systests/rs-sse/rs-sse-base/src/main/java/org/apache/cxf/systest/jaxrs/sse/BookStore2.java
@@ -218,6 +218,16 @@ public class BookStore2 extends BookStoreClientCloseable {
     public int filteredStats() {
         return BookStoreResponseFilter.getInvocations();
     }
+    
+    @GET
+    @Path("/sse/fail/request")
+    @Produces(MediaType.SERVER_SENT_EVENTS)
+    public void failOnRequestThread(@Context SseEventSink sink) {
+        throw new RuntimeException("CXF-9189-MARKER: exception from SSE 
resource method should be logged");
+    }
+
+    
+    
 
     @PUT
     @Path("/filtered/stats")

Reply via email to