How to code custom validator on WebFlux that uses a reactive datasource
Asked Answered
O

3

6

In Spring MVC, I had a @UniqueEmail custom hibernate validator (to check for uniqueness of email when signup), which looked as below:

public class UniqueEmailValidator
implements ConstraintValidator<UniqueEmail, String> {

    private UserRepository userRepository;

    public UniqueEmailValidator(UserRepository userRepository) {

        this.userRepository = userRepository;
    }

    @Override
    public boolean isValid(String email, ConstraintValidatorContext context) {

        return !userRepository.findByEmail(email).isPresent();
    }
}

Now I'm migrating to WebFlux with reactive MongoDB, with my code as below:

public class UniqueEmailValidator
implements ConstraintValidator<UniqueEmail, String> {

    private MongoUserRepository userRepository;

    public UniqueEmailValidator(MongoUserRepository userRepository) {

        this.userRepository = userRepository;
    }

    @Override
    public boolean isValid(String email, ConstraintValidatorContext context) {

        return userRepository.findByEmail(email).block() == null;
    }
}

First of all, using block as above doesn't look good. Secondly, it's not working, and here is the error:

Caused by: java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-3

How to go about this? I can of course use a MongoTemplate blocking method, but is there a way to handle this reactively? I could do it manually in the service method, but I wished this error to be shown to the user along with other errors (e.g. "short" password).

Orobanchaceous answered 12/7, 2018 at 8:9 Comment(0)
L
3

As of Reactor 3.2.0, using blocking APIs inside a parallel or single Scheduler is forbidden and throws the exception you're seeing. So you got that right when you said it doesn't look good - not only it's really bad for your application (it might block the processing of new requests and crash the whole thing down), but it was so bad that the Reactor team decided to consider that as an error.

Now the problem is you'd like to do some I/O related work within a isValid call. THe complete signature of that method is:

boolean isValid(T value, ConstraintValidatorContext context)

The signature shows that it's blocking (it doesn't return a reactive type, nor provides the result as a callback). So you're not allowed to do I/O related or latency involved work in there. Here you'd like to check an entry against the database, which exactly falls into that category.

I don't think you can do that as part of this validation contract and I'm not aware of any alternative to that.

Lohr answered 12/7, 2018 at 15:48 Comment(0)
M
0

I had the same problem and finally I decided to check simple validations with ConstraintValidator and to check reactive validations in the application logic which is reactive. I don't know if there is other better solution, but it could be a good approach.

Montage answered 27/5, 2021 at 8:26 Comment(0)
D
0

I agree by now Spring Framework should have had a reactive validator, but until then I do this inside my controllers or services layers, this is for a simple file manager where I needed to check the permissions on the parent folder, and also if the parent exists:

  private Mono<Folder> validate(Folder folder, UserReference user) {
    
    // init
    var errors = new SimpleErrors(folder);
    
    // validate annotations
    this.validator.validate(folder, errors);
    if (errors.hasErrors()) {
      if (errors.getFieldError() != null) {
        return Mono.error(new BadRequestException(
            errors.getFieldError().getDefaultMessage()));
      }
      if (errors.getGlobalError() != null) {
        return Mono.error(new BadRequestException(
            errors.getGlobalError().getDefaultMessage()));
      }
      return Mono.error(new BadRequestException("Invalid folder"));
    }
    
    // validate parent
    if (folder.getParentId() != null) {
      return repository.findById(folder.getParentId())
          .switchIfEmpty(
              Mono.error(new BadRequestException("Parent folder not found.")))
          .flatMap(parent -> {
            var access = AccessControlList.AccessType.WRITE;
            if (!parent.getAcl().hasAccess(user.getId(), access)) {
              return Mono.error(new BadRequestException(
                  "User does not have write access to parent folder."));
            }
            return Mono.just(folder);
          });
    }
    
    // done
    return Mono.just(folder);
  }
Dyche answered 17/10 at 12:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.