3232import java .util .concurrent .TimeUnit ;
3333import java .util .function .Consumer ;
3434
35-
35+ import ch .qos .logback .classic .Level ;
36+ import ch .qos .logback .classic .Logger ;
37+ import ch .qos .logback .classic .spi .ILoggingEvent ;
38+ import ch .qos .logback .classic .spi .IThrowableProxy ;
39+ import ch .qos .logback .core .read .ListAppender ;
3640import jakarta .ws .rs .client .Entity ;
3741import jakarta .ws .rs .client .WebTarget ;
3842import jakarta .ws .rs .core .HttpHeaders ;
4347import jakarta .ws .rs .sse .InboundSseEvent ;
4448import jakarta .ws .rs .sse .SseEventSource ;
4549import jakarta .ws .rs .sse .SseEventSource .Builder ;
50+ import org .slf4j .LoggerFactory ;
4651import tools .jackson .core .JacksonException ;
4752import tools .jackson .jakarta .rs .json .JacksonJsonProvider ;
4853
54+
4955import org .junit .Before ;
5056import org .junit .Test ;
5157
6066import static org .junit .Assert .fail ;
6167
6268
69+
70+
71+
72+
6373public abstract class AbstractSseTest extends AbstractSseBaseTest {
6474 @ Before
6575 public void setUp () {
@@ -70,6 +80,9 @@ public void setUp() {
7080
7181 }
7282
83+
84+
85+
7386 @ Test
7487 public void testBooksStreamIsReturnedFromLastEventId () throws InterruptedException {
7588 final WebTarget target = createWebTarget ("/rest/api/bookstore/sse/" + UUID .randomUUID ())
@@ -408,7 +421,98 @@ public void testBooksSseContainerResponseAddedHeaders() throws InterruptedExcept
408421 assertThat (response .getHeaderString ("X-My-ProtocolHeader" ), equalTo ("protocol-headers" ));
409422 }
410423 }
424+
425+
426+ @ Test
427+ public void testSseEndpointExceptionIsLoggedToConsole () throws Exception {
428+ final Logger logger = (Logger ) LoggerFactory .getLogger ("org.apache.cxf.jaxrs.JAXRSInvoker" );
429+
430+ final Level oldLevel = logger .getLevel ();
431+ final ListAppender <ILoggingEvent > appender = new ListAppender <>();
432+ appender .start ();
433+
434+ try {
435+ logger .setLevel (Level .ERROR );
436+ logger .addAppender (appender );
437+
438+ try (Response r = createWebTarget ("/rest/api/bookstore/sse/fail/request" )
439+ .request (MediaType .SERVER_SENT_EVENTS )
440+ .get ()) {
441+ // Force the client to actually start consuming
442+ r .readEntity (String .class );
443+ } catch (Exception ex ) {
444+ // expected
445+ }
446+
447+ // Wait until we have at least one ERROR from JAXRSInvoker
448+ awaitEvents (2000 , appender .list , 1 );
449+
450+ assertTrue ("Expected SSE log event, got:\n " + dump (appender ),
451+ hasUnhandledExceptionEvent (appender ));
452+
453+ assertTrue ("Expected SSE marker in throwable, got:\n " + dump (appender ),
454+ hasMarkerInUnhandledExceptionEvent (appender , "CXF-9189-MARKER" ));
455+ } finally {
456+ logger .detachAppender (appender );
457+ logger .setLevel (oldLevel );
458+ appender .stop ();
459+ }
460+ }
411461
462+ private static boolean hasUnhandledExceptionEvent (ListAppender <ILoggingEvent > appender ) {
463+ final String msgNeedle = "Unhandled exception from JAX-RS invocation (async/SSE path)" ;
464+ for (ILoggingEvent e : appender .list ) {
465+ String msg = e .getFormattedMessage ();
466+ if (msg != null && msg .contains (msgNeedle )) {
467+ return true ;
468+ }
469+ }
470+ return false ;
471+ }
472+
473+ private static boolean hasMarkerInUnhandledExceptionEvent (ListAppender <ILoggingEvent > appender , String marker ) {
474+ final String msgNeedle = "Unhandled exception from JAX-RS invocation (async/SSE path)" ;
475+ for (ILoggingEvent e : appender .list ) {
476+ String msg = e .getFormattedMessage ();
477+ if (msg == null || !msg .contains (msgNeedle )) {
478+ continue ;
479+ }
480+ // marker can be in message OR in throwable chain
481+ if (msg .contains (marker ) || throwableChainContains (e .getThrowableProxy (), marker )) {
482+ return true ;
483+ }
484+ }
485+ return false ;
486+ }
487+
488+ private static boolean throwableChainContains (IThrowableProxy tp , String needle ) {
489+ for (IThrowableProxy cur = tp ; cur != null ; cur = cur .getCause ()) {
490+ String m = cur .getMessage ();
491+ if (m != null && m .contains (needle )) {
492+ return true ;
493+ }
494+ }
495+ return false ;
496+ }
497+
498+ private static String dump (ListAppender <ILoggingEvent > appender ) {
499+ StringBuilder sb = new StringBuilder ();
500+ for (ILoggingEvent e : appender .list ) {
501+ sb .append ('[' ).append (e .getLevel ()).append ("] " )
502+ .append (e .getLoggerName ()).append (" - " )
503+ .append (e .getFormattedMessage ());
504+ if (e .getThrowableProxy () != null ) {
505+ sb .append (" (thrown: " )
506+ .append (e .getThrowableProxy ().getClassName ())
507+ .append (": " )
508+ .append (e .getThrowableProxy ().getMessage ())
509+ .append (')' );
510+ }
511+ sb .append ('\n' );
512+ }
513+ return sb .toString ();
514+ }
515+
412516 /**
413517 * Jetty / Undertow do not propagate errors from the runnable passed to
414518 * AsyncContext::start() up to the AsyncEventListener::onError(). Tomcat however
@@ -426,4 +530,6 @@ private static Consumer<InboundSseEvent> collect(final Collection<Book> books) {
426530 private static Consumer <InboundSseEvent > collectRaw (final Collection <String > titles ) {
427531 return event -> titles .add (event .readData (String .class , MediaType .TEXT_PLAIN_TYPE ));
428532 }
533+
534+
429535}
0 commit comments