[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;
}
}