Can't catch an exception thrown from a reactive chain involving a RouterFunction

25 views Asked by At

I can't catch an exception thrown by a reactive operator (at least, not in a concise way)

    @Bean
    public RouterFunction<ServerResponse> logInRoute() {
        return RouterFunctions.route()
                .POST("/login", tokenHandler::logIn)
                .onError(BadCredentialsException.class,
                        (t,r) -> ServerResponse.status(HttpStatus.UNAUTHORIZED).bodyValue(t.getMessage()))
                .build();
    }
    public Mono<ServerResponse> logIn(ServerRequest request) {
        return request.bodyToMono(UserDto.class) // Mono<UserDto>
                .map(this::toUnauthenticatedUpat) // Mono<UsernamePasswordAuthenticationToken>
                .flatMap(authenticationManager::authenticate) // it throws
                .map(tokenService::generateTokenFor) // Mono<String>
                .transform(jwt -> ServerResponse.status(HttpStatus.OK).body(jwt, String.class)) // Mono<ServerResponse>
                .onErrorResume(t ->
                        ServerResponse.status(HttpStatus.UNAUTHORIZED).bodyValue(t.getMessage()));
    }

    private UsernamePasswordAuthenticationToken toUnauthenticatedUpat(UserDto userDto) {
        return UsernamePasswordAuthenticationToken.unauthenticated(
                userDto.getUsername(), userDto.getPassword());
    }

As you see, I put an error handling lambda both in the handler and in the function itself. None of them worked

ERROR 13444 --- [token-service] [oundedElastic-1] a.w.r.e.AbstractErrorWebExceptionHandler : [70d874e3-1]  500 Server Error for HTTP POST "/login"

org.springframework.security.authentication.BadCredentialsException: Invalid Credentials
    at org.springframework.security.authentication.AbstractUserDetailsReactiveAuthenticationManager.lambda$authenticate$1(AbstractUserDetailsReactiveAuthenticationManager.java:102) ~[spring-security-core-6.2.1.jar:6.2.1]
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
    *__checkpoint ⇢ Handler org.springframework.web.reactive.function.server.HandlerFilterFunction$$Lambda$1592/0x00000008016608a0@433e5f3 [DispatcherHandler]
    *__checkpoint ⇢ org.springframework.security.web.server.WebFilterChainProxy [DefaultWebFilterChain]
    *__checkpoint ⇢ HTTP POST "/login" [ExceptionHandlingWebHandler]

To be clear, the problem is not the exception itself (I send bad credentials on purpose), but the fact that I have trouble catching it and mapping to a 401 response. 500 is very misleading

The exception is thrown from org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager, specifically from the authenticate() it inherits from its abstract parent

    @Override
    public Mono<Authentication> authenticate(Authentication authentication) {
        String username = authentication.getName();
        String presentedPassword = (String) authentication.getCredentials();
        // @formatter:off
        return retrieveUser(username)
                .doOnNext(this.preAuthenticationChecks::check)
                .publishOn(this.scheduler)
                .filter((userDetails) -> this.passwordEncoder.matches(presentedPassword, userDetails.getPassword()))
                .switchIfEmpty(Mono.defer(() -> Mono.error(new BadCredentialsException("Invalid Credentials"))))
                .flatMap((userDetails) -> upgradeEncodingIfNecessary(userDetails, presentedPassword))
                .doOnNext(this.postAuthenticationChecks::check)
                .map(this::createUsernamePasswordAuthenticationToken);
        // @formatter:on
    }

A chat bot suggested this abomination, but it doesn't work too

                .map(this::toUnauthenticatedUpat)
                .flatMap(user -> {
                    try {
                        return authenticationManager.authenticate(user);
                    } catch (BadCredentialsException ex) {
                        return Mono.error(ex);
                    }
                })

I also took this advice and tried chaining a filter:

    @Bean
    public RouterFunction<ServerResponse> logInRoute() {
        return RouterFunctions.route()
                .POST("/login", tokenHandler::logIn)
                .filter((request, next) -> next.handle(request)
                        .onErrorResume(BadCredentialsException.class,
                                t -> ServerResponse.status(HttpStatus.UNAUTHORIZED).bodyValue(t.getMessage())))
                .build();
    }

Again, no effect

Of all the things I tried, only this ugly WebFilter works

    @Bean
    public WebFilter badCredentialsToBadRequest() {
        return (exchange, next) -> next.filter(exchange)
                .onErrorResume(BadCredentialsException.class, e -> {
                    ServerHttpResponse response = exchange.getResponse();
                    response.setStatusCode(HttpStatus.UNAUTHORIZED);
                    DefaultDataBufferFactory defaultDataBufferFactory = new DefaultDataBufferFactory();
                    DataBuffer dataBuffer = defaultDataBufferFactory.wrap(e.getMessage().getBytes());
                    return response.writeWith(Mono.fromSupplier(() -> dataBuffer));
                });
    }

So what is my mistake and how do I catch the exception properly¹?

¹ that is, concisely, without a ton of code and all that low-level "buffer-whatever" stuff that I'm sure is too much for such a trivial task

UPD: I just want to share my guess about why that onErrorResume() in logIn() didn't work. My tacit understanding is that such handlers handle exceptions thrown from the Mono they are chained to, Mono<ServerResponse> in that case. However, the exception is thrown from Mono<UsernamePasswordAuthenticationToken>. If you chain a handler directly to flatMap(authenticationManager::authenticate) and put a breakpoint there, the breakpoint is hit. On the other hand, the WebFilter solution relies on chaining onErrorResume() to Mono<Void> which is also not a Mono<UsernamePasswordAuthenticationToken>... Confusing

0

There are 0 answers