[UPDATE 2021-10-11] Added MCVE
https://github.com/SalathielGenese/issue-spring-webflux-reactive-error-advice
For reusability concerns, I run my validation on the service layer, which returns Mono.error( constraintViolationException )
...
So that my web handlers merely forward the unmarshalled domain to the service layer.
So far, so great.
But how do I advise (AOP) my web handlers so that it returns HTTP 422
with the formatted constraint violations ?
WebExchangeBindException
only handle exceptions thrown synchronously (I don't want synchronous validation to break the reactive flow).
My AOP advice trigger and error b/c :
- my web handler return
Mono<DataType>
- but my advice return a
ResponseEntity
And if I wrap my response entity (from the advice) into a Mono<ResponseEntity>
, I an HTTP 200 OK
with the response entity serialized :(
Code Excerpt
@Aspect
@Component
class CoreWebAspect {
@Pointcut("withinApiCorePackage() && @annotation(org.springframework.web.bind.annotation.PostMapping)")
public void postMappingWebHandler() {
}
@Pointcut("within(project.package.prefix.*)")
public void withinApiCorePackage() {
}
@Around("postMappingWebHandler()")
public Object aroundWebHandler(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
try {
final var proceed = proceedingJoinPoint.proceed();
if (proceed instanceof Mono<?> mono) {
try {
return Mono.just(mono.toFuture().get());
} catch (ExecutionException exception) {
if (exception.getCause() instanceof ConstraintViolationException constraintViolationException) {
return Mono.just(getResponseEntity(constraintViolationException));
}
throw exception.getCause();
}
}
return proceed;
} catch (ConstraintViolationException constraintViolationException) {
return getResponseEntity(constraintViolationException);
}
}
private ResponseEntity<Set<Violation>> getResponseEntity(final ConstraintViolationException constraintViolationException) {
final var violations = constraintViolationException.getConstraintViolations().stream().map(violation -> new Violation(
stream(violation.getPropertyPath().spliterator(), false).map(Node::getName).collect(toList()),
violation.getMessageTemplate().replaceFirst("^\\{(.*)\\}$", "$1"))
).collect(Collectors.toSet());
return status(UNPROCESSABLE_ENTITY).body(violations);
}
@Getter
@AllArgsConstructor
private static class Violation {
private final List<String> path;
private final String template;
}
}