I am throwing a org.springframework.web.server.ResponseStatusException
from a command handler that was invoked through a HTTP request in Spring. Usually I would expect the response to carry the HTTP status that is defined by the exception.
However, Spring returns status 200 while Axon logs:
Command 'com.my.Command' resulted in com.my.CustomResponseStatusException
.
It makes no difference whether I call send
or sendAndWait
on the command gateway. The controller directly returns the resulting CompletableFuture
.
Is there a configuration I am missing?
The exception I am throwing is an extended ResponseStatusException
that allows me to provide a custom domain error code in conjunction with a HTTP status error code:
public abstract class DomainException extends ResponseStatusException {
public final String errorCode;
public DomainException(HttpStatus status, String errorCode, String message) {
super(status, message);
this.errorCode = errorCode;
}
public DomainException(HttpStatus status, String errorCode, String message, Throwable cause) {
super(status, message, cause);
this.errorCode = errorCode;
}
}
One of these concrete implementations is:
public class MembershipAlreadyExistsException extends DomainException {
public MembershipAlreadyExistsException(Id<Member> memberId, Id<Club> clubId) {
super(HttpStatus.BAD_REQUEST, DomainError.MEMBERSHIP_ALREADY_EXISTS.name(), "There is already a membership of member with ID " + memberId + " and club with ID " + clubId);
}
}
Even though I am giving custom @ControllerAdvice
, none of the exception handler methods are invoked as the exception seems to be caught and logged within the command gateway.
@Order(Ordered.HIGHEST_PRECEDENCE)
@ControllerAdvice
public class DefaultExceptionHandler extends ResponseEntityExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(DefaultExceptionHandler.class);
@ExceptionHandler(UndeclaredThrowableException.class)
protected ResponseEntity<?> handleUndeclaredException(UndeclaredThrowableException exception) {
logger.error("Undeclared Exception", exception);
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
@ExceptionHandler(DomainException.class)
protected ResponseEntity<?> handleDomainException(DomainException exception) {
logger.error("Domain Exception", exception);
return new ResponseEntity<>(exception.errorCode, exception.getStatusCode());
}
@ExceptionHandler(JSR303ViolationException.class)
protected ResponseEntity<?> handleJSR303ViolationException(JSR303ViolationException exception) {
logger.debug("Request was violating one ore more constraints", exception);
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
}
Interestingly, if I do this:
@RequestMapping("/invite")
public CompletableFuture<Void> enrollMember(@RequestBody InviteMemberByEmail inviteMemberByEmail) {
try {
logger.trace("Before sending command");
CompletableFuture<Void> result = commandGateway.sendAndWait(inviteMemberByEmail);
logger.trace("After sending command");
return result;
} catch (Exception e) {
logger.trace("Exception caught", e);
throw e;
}
}
The output is:
2024-03-23T09:22:29.073+01:00 TRACE 20808 --- [nio-8081-exec-2] a.z.c.i.http.MemberController : Before sending command
2024-03-23T09:22:29.930+01:00 WARN 20808 --- [nio-8081-exec-2] o.s.c.annotation.AnnotationTypeMapping : Support for convention-based annotation attribute overrides is deprecated and will be removed in Spring Framework 6.2. Please annotate the following attributes in @org.axonframework.modelling.command.AggregateIdentifier with appropriate @AliasFor declarations: [routingKey]
2024-03-23T09:22:29.949+01:00 WARN 20808 --- [nio-8081-exec-2] o.a.c.gateway.DefaultCommandGateway : Command '--redacted--.api.InviteMemberToClub' resulted in --redacted--.domain.exceptions.MembershipAlreadyExistsException(400 BAD_REQUEST "There is already a membership of member with ID f6628294-4021-70d1-89cd-0ee5e7e99c8f and club with ID 780f0cc1-8eaf-4968-bcbd-8f92fb678260")
2024-03-23T09:22:29.950+01:00 TRACE 20808 --- [nio-8081-exec-2] a.z.c.i.http.MemberController : After sending command
I have confirmed that it is in fact an instance of SimpleCommandBus
, but noticed some form of FailureLoggingCallback
:
I've been checking the implementation of the SimpleCommandBus
and DefaultCommandGateway
, but the only scenario when the DefaultCommandGateway
would not rethrow the exception, is if the CommandResultMessage
is not exceptional.
Note that the CommandResultMessage
is the object Axon Framework creates to carry the result of command handling. Hence, if you throw an exception, it will be captured in the CommandResultMessage
. Upon doing so, its state is set to be exceptional.
Hence, at this moment, I can only conclude that the command handler may be doing something off concerning the exception throwing. Or, the aggregate is capturing the exception somewhere else. But, to be able to deduce that, I would need to ask you to either:
Even though I am throwing the exception in the command handler, changing from a tracking to a subscribing event processor fixes the issue (?)
Switching a TrackingEventProcessor
for a SubscribingEventProcessor
would result in the same thread to be used for handling your events. So, the only way how this would resolve it, is if the event handler throws an Exception that does cause the CommandResultMessage
to become exceptional.