Search code examples
javareactive-programmingspring-webflux

Getting a StackOverflowError when Using Flux and Mono in the same block


I am using WebFlux in my spring boot project. I have a piece of code that registers a user, it first checks if the submitted email exists in the database like this

userRepository.findByEmail(userDto.getEmail())
            .flatMap(t -> {
                UserAlreadyExistException userAlreadyExistException = new UserAlreadyExistException("There is an account with this email address: " + userDto.getEmail());
                Response<?> response = Response.duplicateEntity();
                response.addErrorMsgToResponse(userAlreadyExistException.getMessage(), userAlreadyExistException);
                return Mono.<ResponseEntity<?>>just(ResponseEntity.status(HttpStatus.NOT_ACCEPTABLE)
                        .body(response));
            })

If the provided email does exist it returns an error, if it doesn't the app proceeds and fetches the company and list of roles assigned to that user. Since this is a reactive app, I use zip so that if either the supplied company or the roles do not exist the app instead gives an error, I do it like this

 .switchIfEmpty(Mono.defer(() -> Mono.zip(companyRepository.findById(userDto.getCompanyUuid()), 
                    roleRepository.findAllById(userDto.getRolesUuids()).collectList()))
                    .flatMap(t -> userRepository.save(User.builder()
                            .firstName(userDto.getFirstName())
                            .lastName(userDto.getLastName())
                            .company(t.getT1())
                            .status(userDto.isEnabled() ? BaseModel.Status.ENABLED : BaseModel.Status.DISABLED)
                            .accountStatus(User.AccountStatus.ACCOUNT_ACTIVATED)
                            .email(userDto.getEmail())
                            .profilePhoto(userDto.getProfilePhoto())
                            .roles(t.getT2()).phone(userDto.getPhone())
                            .build()))
                    .flatMap(__ -> Mono.<ResponseEntity<?>>just(ResponseEntity.status(HttpStatus.OK).body(Response.ok())))
                    .switchIfEmpty(Mono.defer(() -> Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                            .body(Response.badRequest())))))

When I run this code it works up to the zip Mono.zip(companyRepository.findById(userDto.getCompanyUuid()), roleRepository.findAllById(userDto.getRolesUuids()).collectList())) where it enters an infinite loop; looping through the roles until I get a stackoverflow, I honestly do not understand this behavior, I tried changing it to something like

 .switchIfEmpty(Mono.defer(() -> companyRepository.findById(userDto.getCompanyUuid())
                    .flatMap(company -> roleRepository.findAllById(userDto.getRolesUuids())
                            .collectList()
                            .flatMap(roles -> userRepository.save(User.builder()
                                    .firstName(userDto.getFirstName())
                                    .lastName(userDto.getLastName())
                                    .company(company)
                                    .status(userDto.isEnabled() ? BaseModel.Status.ENABLED : BaseModel.Status.DISABLED)
                                    .accountStatus(User.AccountStatus.ACCOUNT_ACTIVATED)
                                    .email(userDto.getEmail())
                                    .profilePhoto(userDto.getProfilePhoto())
                                    .roles(roles)
                                    .phone(userDto.getPhone())
                                    .build()))

                    )
                    .flatMap(__ -> Mono.<ResponseEntity<?>>just(ResponseEntity.status(HttpStatus.OK).body(Response.ok())))
                    .switchIfEmpty(Mono.defer(() -> Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                            .body(Response.badRequest()))))))

To no avail, the same behavior pops up. What could be wrong here?


Solution

  • First of all, please try avoiding nested flatmaps. It's considered a bad practice.

    Secondly, you are using zip so that if either the supplied company or the roles do not exist the app instead gives an error.

    This you can achieve simply by:

    companyRepository.findById(userDto.getCompanyUuid()
       .switchIfEmpty(Mono.deferWithContext(context -> {
              //do some logging or something with context
              return Mono.error(new SomeException());
            }))
       .zipWith(...)
       ...