spring-bootspring-webfluxspring-restspring-mongodb

Why isn't my user inserted in my mongoDB? spring webFlux reactive REST


I have a Springboot Reactive Web Rest API with a Mongo Database.

My Method in my UserHandler for adding a user looks like this:

public Mono<User> addUser(Mono<User> user) {
    return user
        .flatMap(u -> repo.findByEmail(u.getEmail())
            .flatMap(existingUser -> Mono.error(
                new UserAlreadyExistsException(String.format("User with email '%s' already exists.", existingUser.getEmail())))))
        .switchIfEmpty(user.flatMap(repo::insert))
        .cast(User.class);
}

The UserController looks like this.

@RestController
@RequestMapping("/users")
public class UserController {

  @Autowired
  private UserHandler userService;
  ...
  @PostMapping
  public Mono<User> addUser(@Valid @RequestBody Mono<User> user) {
    return userService.addUser(user);
  }
  ...
}

When I now make a post call with postman I get the expected UserAlreadyExists exception when I try to insert an existing one. But I get this exception when I try to insert a new one:

{
    "type": "about:blank",
    "title": "Bad Request",
    "status": 400,
    "detail": "Invalid request content",
    "instance": "/users"
}

And the user wont be inserted.

But when I simplify the method to not check, just insert, it works without any errors and the new user will be inserted (same body for the POST).

public Mono<User> addUser(Mono<User> user) {
   return user.flatMap(repo::insert);
}

For the sake of completeness, here the body of the request:

{
    "firstname": "Alice",
    "lastname": "Jones",
    "email": "alice.jones@example.com",
    "bio": "Lorem ipsum dolor sit amet",
    "address": {
        "street": "123 Main St",
        "city": "Anytown",
        "state": "CA",
        "zipCode": "12345",
        "country": "USA"
    },
    "phoneNumber": "123-456-7890"
}

The UserRepository Interface:

public interface UserRepository extends ReactiveMongoRepository<User, String> {

  Mono<User> findByEmail(String email);
  
}

And the UserEntity:

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "users")
public class User {

  @Id
  private String id;
  @NotBlank
  private String firstname;
  @NotBlank
  private String lastname;

  @Indexed(unique = true)
  @Email
  private String email;
  private String bio;
  private Address address;
  private String phoneNumber;
}

As well as my RestResponseEntityExceptionHandler:

@ControllerAdvice
@ResponseStatus
public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
  
  @ExceptionHandler(UserNotFoundException.class)
  public ResponseEntity<ErrorMessage> userNotFoundException(UserNotFoundException exception) {
    ErrorMessage message = new ErrorMessage(HttpStatus.NOT_FOUND, exception.getMessage());
    return ResponseEntity.status(HttpStatus.NOT_FOUND)
          .body(message);
  }

  @ExceptionHandler(UserAlreadyExistsException.class)
  public ResponseEntity<ErrorMessage> userAlreadyExsistsException(UserAlreadyExistsException exception) {
    ErrorMessage message = new ErrorMessage(HttpStatus.BAD_REQUEST, exception.getMessage());
    return ResponseEntity.status(HttpStatus.BAD_REQUEST)
          .body(message);
  }
}

What am I doing wrong?


Solution

  • You don't have to use Mono<User> user, you can just pass User directly. Don't worry, there will be no blocking code. My assumption is that you're trying to use your publisher twice within your reactive chain. Try the code below, and let me know if it works.

    Also, it is recommended to use save() method instead of insert(). From insert() docs:

    Inserts the given entity. Assumes the instance to be new to be able to apply insertion optimizations. Use the returned instance for further operations as the save operation might have changed the entity instance completely. Prefer using save(Object) instead to avoid the usage of store-specific API.

    Your controller:

    @RestController
    @RequestMapping("/users")
    public class UserController {
    
      @Autowired
      private UserHandler userService;
      ...
      @PostMapping
      public Mono<User> addUser(@Valid @RequestBody User user) {
        return userService.addUser(user);
      }
      ...
    }
    

    Your service method to add user:

    public Mono<User> addUser(User user) {
        return repo.findByEmail(user.getEmail())
                .flatMap(existingUser -> Mono.error(
                        new UserAlreadyExistsException(String.format("User with email '%s' already exists.", existingUser.getEmail())))
                )
                .cast(User.class)
                .switchIfEmpty(repo.save(user));
    }