Skip to content

Commit 0e58932

Browse files
Copilotbrunoborges
andcommitted
Update EventErrorHandler to return boolean for error propagation control
Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com>
1 parent 19545cf commit 0e58932

3 files changed

Lines changed: 393 additions & 56 deletions

File tree

src/main/java/com/github/copilot/sdk/CopilotSession.java

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -158,26 +158,45 @@ public String getWorkspacePath() {
158158
* <p>
159159
* When an event handler registered via {@link #on(Consumer)} or
160160
* {@link #on(Class, Consumer)} throws an exception during event dispatch, the
161-
* error handler is invoked instead of the default behavior (logging at
162-
* {@link Level#SEVERE}).
161+
* error handler is invoked to handle the error. The handler's return value
162+
* controls whether dispatch continues to remaining handlers:
163+
* <ul>
164+
* <li>Return {@code true} to continue dispatching (default behavior for
165+
* independent listeners)</li>
166+
* <li>Return {@code false} to stop dispatching (short-circuit for validation
167+
* or critical errors)</li>
168+
* </ul>
163169
*
164170
* <p>
165-
* If the error handler itself throws an exception, that exception is silently
166-
* caught and logged to prevent cascading failures.
171+
* When no error handler is set, exceptions are silently caught and dispatch
172+
* continues. Applications should set an error handler to log, track, or respond
173+
* to handler failures.
174+
*
175+
* <p>
176+
* If the error handler itself throws an exception, that exception is caught,
177+
* logged at {@link Level#SEVERE}, and dispatch is stopped to prevent cascading
178+
* failures.
167179
*
168180
* <p>
169181
* <b>Example:</b>
170182
*
171183
* <pre>{@code
184+
* // Continue on error (log and keep dispatching)
185+
* session.setEventErrorHandler((event, exception) -> {
186+
* logger.error("Handler failed: {}", exception.getMessage(), exception);
187+
* return true; // keep dispatching
188+
* });
189+
*
190+
* // Short-circuit on error (stop at first failure)
172191
* session.setEventErrorHandler((event, exception) -> {
173-
* metrics.increment("handler.errors");
174-
* logger.error("Handler failed on {}: {}", event.getType(), exception.getMessage());
192+
* logger.error("Handler failed, stopping: {}", exception.getMessage(), exception);
193+
* return false; // stop dispatching
175194
* });
176195
* }</pre>
177196
*
178197
* @param handler
179-
* the error handler, or {@code null} to restore default logging
180-
* behavior
198+
* the error handler, or {@code null} to restore default silent
199+
* continuation behavior
181200
* @see EventErrorHandler
182201
* @since 1.0.8
183202
*/
@@ -330,8 +349,10 @@ public CompletableFuture<AssistantMessageEvent> sendAndWait(MessageOptions optio
330349
* instead.
331350
*
332351
* <p>
333-
* <b>Exception isolation:</b> If a handler throws an exception, the error is
334-
* logged and remaining handlers still execute.
352+
* <b>Exception handling:</b> If a handler throws an exception, the error is
353+
* routed to the configured {@link EventErrorHandler} (if set), which can choose
354+
* to continue or stop dispatching. When no error handler is set, exceptions are
355+
* silently caught and remaining handlers still execute.
335356
*
336357
* <p>
337358
* <b>Example:</b>
@@ -347,6 +368,7 @@ public CompletableFuture<AssistantMessageEvent> sendAndWait(MessageOptions optio
347368
* @return a Closeable that, when closed, unsubscribes the handler
348369
* @see #on(Class, Consumer)
349370
* @see AbstractSessionEvent
371+
* @see #setEventErrorHandler(EventErrorHandler)
350372
*/
351373
public Closeable on(Consumer<AbstractSessionEvent> handler) {
352374
eventHandlers.add(handler);
@@ -361,8 +383,10 @@ public Closeable on(Consumer<AbstractSessionEvent> handler) {
361383
* matching the specified type.
362384
*
363385
* <p>
364-
* <b>Exception isolation:</b> If a handler throws an exception, the error is
365-
* logged and remaining handlers still execute.
386+
* <b>Exception handling:</b> If a handler throws an exception, the error is
387+
* routed to the configured {@link EventErrorHandler} (if set), which can choose
388+
* to continue or stop dispatching. When no error handler is set, exceptions are
389+
* silently caught and remaining handlers still execute.
366390
*
367391
* <p>
368392
* <b>Example Usage</b>
@@ -394,6 +418,7 @@ public Closeable on(Consumer<AbstractSessionEvent> handler) {
394418
* @return a Closeable that unsubscribes the handler when closed
395419
* @see #on(Consumer)
396420
* @see AbstractSessionEvent
421+
* @see #setEventErrorHandler(EventErrorHandler)
397422
*/
398423
public <T extends AbstractSessionEvent> Closeable on(Class<T> eventType, Consumer<T> handler) {
399424
Consumer<AbstractSessionEvent> wrapper = event -> {
@@ -410,11 +435,20 @@ public <T extends AbstractSessionEvent> Closeable on(Class<T> eventType, Consume
410435
* <p>
411436
* This is called internally when events are received from the server. Each
412437
* handler is invoked in its own try/catch block so that an exception thrown by
413-
* one handler does not prevent subsequent handlers from executing.
438+
* one handler does not prevent subsequent handlers from executing by default.
414439
* <p>
415440
* If a custom {@link EventErrorHandler} has been set via
416441
* {@link #setEventErrorHandler(EventErrorHandler)}, it is called with the event
417-
* and exception. Otherwise, exceptions are logged at {@link Level#SEVERE}.
442+
* and exception. The error handler's return value controls whether dispatch
443+
* continues:
444+
* <ul>
445+
* <li>{@code true} - continue dispatching to remaining handlers</li>
446+
* <li>{@code false} - stop dispatching (short-circuit)</li>
447+
* </ul>
448+
* <p>
449+
* When no error handler is set, exceptions are silently caught and dispatch
450+
* continues. If the error handler itself throws an exception, dispatch is
451+
* stopped and the error is logged at {@link Level#SEVERE}.
418452
*
419453
* @param event
420454
* the event to dispatch
@@ -428,13 +462,15 @@ void dispatchEvent(AbstractSessionEvent event) {
428462
EventErrorHandler errorHandler = this.eventErrorHandler;
429463
if (errorHandler != null) {
430464
try {
431-
errorHandler.handleError(event, e);
465+
if (!errorHandler.handleError(event, e)) {
466+
break; // stop dispatching
467+
}
432468
} catch (Exception errorHandlerException) {
433469
LOG.log(Level.SEVERE, "Error in event error handler", errorHandlerException);
470+
break; // error handler itself failed — stop to be safe
434471
}
435-
} else {
436-
LOG.log(Level.SEVERE, "Error in event handler", e);
437472
}
473+
// No error handler set: continue silently (no logging)
438474
}
439475
}
440476
}

src/main/java/com/github/copilot/sdk/EventErrorHandler.java

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,48 @@
1616
* being dispatched and the exception that was thrown.
1717
*
1818
* <p>
19-
* The default behavior logs errors at {@link java.util.logging.Level#SEVERE}.
20-
* You can override this to integrate with your own logging, metrics, or
21-
* error-reporting systems:
19+
* The handler's return value controls whether event dispatch continues to
20+
* remaining handlers:
21+
* <ul>
22+
* <li>Return {@code true} to continue dispatching to remaining handlers
23+
* (default behavior for independent listeners)</li>
24+
* <li>Return {@code false} to stop dispatching (short-circuit behavior for
25+
* validation or critical errors)</li>
26+
* </ul>
27+
*
28+
* <p>
29+
* When no error handler is set, exceptions are silently caught and dispatch
30+
* continues to remaining handlers. This makes the SDK non-intrusive by default.
31+
* Applications should set an error handler to log, track, or respond to handler
32+
* failures.
33+
*
34+
* <p>
35+
* Example configurations:
2236
*
2337
* <pre>{@code
38+
* // Continue on error (log and keep dispatching)
39+
* session.setEventErrorHandler((event, exception) -> {
40+
* logger.error("Handler failed: {}", exception.getMessage(), exception);
41+
* return true; // keep dispatching
42+
* });
43+
*
44+
* // Short-circuit on error (stop at first failure)
45+
* session.setEventErrorHandler((event, exception) -> {
46+
* logger.error("Handler failed, stopping: {}", exception.getMessage(), exception);
47+
* return false; // stop dispatching
48+
* });
49+
*
50+
* // Selective: short-circuit only for critical events
2451
* session.setEventErrorHandler((event, exception) -> {
25-
* metrics.increment("handler.errors");
26-
* logger.error("Handler failed on {}: {}", event.getType(), exception.getMessage());
52+
* logger.error("Handler failed: {}", exception.getMessage(), exception);
53+
* return !(event instanceof SessionErrorEvent); // stop only for error events
2754
* });
2855
* }</pre>
2956
*
3057
* <p>
31-
* If the error handler itself throws an exception, that exception is silently
32-
* caught and logged to prevent cascading failures.
58+
* If the error handler itself throws an exception, that exception is caught,
59+
* logged at {@link java.util.logging.Level#SEVERE}, and dispatch is stopped to
60+
* prevent cascading failures.
3361
*
3462
* @see CopilotSession#setEventErrorHandler(EventErrorHandler)
3563
* @since 1.0.8
@@ -44,6 +72,8 @@ public interface EventErrorHandler {
4472
* the event that was being dispatched when the error occurred
4573
* @param exception
4674
* the exception thrown by the event handler
75+
* @return {@code true} to continue dispatching to remaining handlers,
76+
* {@code false} to stop dispatching (the exception is not rethrown)
4777
*/
48-
void handleError(AbstractSessionEvent event, Exception exception);
78+
boolean handleError(AbstractSessionEvent event, Exception exception);
4979
}

0 commit comments

Comments
 (0)