3636import com.fasterxml.jackson.core.JsonProcessingException;
3737import com.fasterxml.jackson.jakarta.rs.json.JacksonJsonProvider;
3838
39+ import ch.qos.logback.classic.Level;
40+ import ch.qos.logback.classic.Logger;
41+ import ch.qos.logback.classic.spi.ILoggingEvent;
42+ import ch.qos.logback.classic.spi.IThrowableProxy;
43+ import ch.qos.logback.core.read.ListAppender;
3944import jakarta.ws.rs.client.Entity;
4045import jakarta.ws.rs.client.WebTarget;
4146import jakarta.ws.rs.core.HttpHeaders;
4651import jakarta.ws.rs.sse.InboundSseEvent;
4752import jakarta.ws.rs.sse.SseEventSource;
4853import jakarta.ws.rs.sse.SseEventSource.Builder;
54+ import org.slf4j.LoggerFactory;
55+
4956
5057import org.junit.Before;
5158import org.junit.Test;
6168import static org.junit.Assert.fail;
6269
6370
71+
72+
73+
74+
6475public abstract class AbstractSseTest extends AbstractSseBaseTest {
6576 @Before
6677 public void setUp() {
@@ -71,6 +82,9 @@ public void setUp() {
7182
7283 }
7384
85+
86+
87+
7488 @Test
7589 public void testBooksStreamIsReturnedFromLastEventId() throws InterruptedException {
7690 final WebTarget target = createWebTarget("/rest/api/bookstore/sse/" + UUID.randomUUID())
@@ -409,7 +423,98 @@ public void testBooksSseContainerResponseAddedHeaders() throws InterruptedExcept
409423 assertThat(response.getHeaderString("X-My-ProtocolHeader"), equalTo("protocol-headers"));
410424 }
411425 }
426+
427+
428+ @Test
429+ public void testSseEndpointExceptionIsLoggedToConsole() throws Exception {
430+ final Logger logger = (Logger) LoggerFactory.getLogger("org.apache.cxf.jaxrs.JAXRSInvoker");
431+
432+ final Level oldLevel = logger.getLevel();
433+ final ListAppender<ILoggingEvent> appender = new ListAppender<>();
434+ appender.start();
435+
436+ try {
437+ logger.setLevel(Level.ERROR);
438+ logger.addAppender(appender);
439+
440+ try (Response r = createWebTarget("/rest/api/bookstore/sse/fail/request")
441+ .request(MediaType.SERVER_SENT_EVENTS)
442+ .get()) {
443+ // Force the client to actually start consuming
444+ r.readEntity(String.class);
445+ } catch (Exception ex) {
446+ // expected
447+ }
448+
449+ // Wait until we have at least one ERROR from JAXRSInvoker
450+ awaitEvents(2000, appender.list, 1);
451+
452+ assertTrue("Expected SSE log event, got:\n" + dump(appender),
453+ hasUnhandledExceptionEvent(appender));
454+
455+ assertTrue("Expected SSE marker in throwable, got:\n" + dump(appender),
456+ hasMarkerInUnhandledExceptionEvent(appender, "CXF-9189-MARKER"));
457+ } finally {
458+ logger.detachAppender(appender);
459+ logger.setLevel(oldLevel);
460+ appender.stop();
461+ }
462+ }
463+
464+ private static boolean hasUnhandledExceptionEvent(ListAppender<ILoggingEvent> appender) {
465+ final String msgNeedle = "Unhandled exception from JAX-RS invocation (async/SSE path)";
466+ for (ILoggingEvent e : appender.list) {
467+ String msg = e.getFormattedMessage();
468+ if (msg != null && msg.contains(msgNeedle)) {
469+ return true;
470+ }
471+ }
472+ return false;
473+ }
474+
475+ private static boolean hasMarkerInUnhandledExceptionEvent(ListAppender<ILoggingEvent> appender, String marker) {
476+ final String msgNeedle = "Unhandled exception from JAX-RS invocation (async/SSE path)";
477+ for (ILoggingEvent e : appender.list) {
478+ String msg = e.getFormattedMessage();
479+ if (msg == null || !msg.contains(msgNeedle)) {
480+ continue;
481+ }
482+ // marker can be in message OR in throwable chain
483+ if (msg.contains(marker) || throwableChainContains(e.getThrowableProxy(), marker)) {
484+ return true;
485+ }
486+ }
487+ return false;
488+ }
412489
490+ private static boolean throwableChainContains(IThrowableProxy tp, String needle) {
491+ for (IThrowableProxy cur = tp; cur != null; cur = cur.getCause()) {
492+ String m = cur.getMessage();
493+ if (m != null && m.contains(needle)) {
494+ return true;
495+ }
496+ }
497+ return false;
498+ }
499+
500+ private static String dump(ListAppender<ILoggingEvent> appender) {
501+ StringBuilder sb = new StringBuilder();
502+ for (ILoggingEvent e : appender.list) {
503+ sb.append('[').append(e.getLevel()).append("] ")
504+ .append(e.getLoggerName()).append(" - ")
505+ .append(e.getFormattedMessage());
506+ if (e.getThrowableProxy() != null) {
507+ sb.append(" (thrown: ")
508+ .append(e.getThrowableProxy().getClassName())
509+ .append(": ")
510+ .append(e.getThrowableProxy().getMessage())
511+ .append(')');
512+ }
513+ sb.append('\n');
514+ }
515+ return sb.toString();
516+ }
517+
413518 /**
414519 * Jetty / Undertow do not propagate errors from the runnable passed to
415520 * AsyncContext::start() up to the AsyncEventListener::onError(). Tomcat however
@@ -427,4 +532,6 @@ private static Consumer<InboundSseEvent> collect(final Collection<Book> books) {
427532 private static Consumer<InboundSseEvent> collectRaw(final Collection<String> titles) {
428533 return event -> titles.add(event.readData(String.class, MediaType.TEXT_PLAIN_TYPE));
429534 }
535+
536+
430537}
0 commit comments